hvac: native MQTT driver for MHI aircon (replaces Modbus for control/status)

The Intesis MHI gateway was switched from Modbus TCP to native MQTT mode
(2026-08-13), publishing retained per-signal JSON status to
hvac/mhi/IU<nn>/status/<signal> and subscribing to commands at
hvac/mhi/IU<nn>/cmd/<signal>Cmd. This adds an MQTT ingest + command path
alongside the now-dormant Modbus driver, under a new mhi_mqtt device type.

Backend:
- lib/mqtt.js: subscribe hvac/mhi/+/status/+, parse JSON payloads, auto-
  register each unit as an unassigned mhi_mqtt zone_devices row and cache
  live status (power/mode/setpoint/room temp/fan/health). Add
  publishMhiCommand() for on/off/mode/setpoint/fan/vanes writes (QoS 1,
  retain off — a retained command would replay stale on gateway reconnect).
  Numeric mode/fan enums map index-for-index to the frontend label arrays.
- routes/override.js: mhi-status/mhi-control branch on device_type —
  mhi_mqtt serves from the push-populated cache and publishes commands;
  mhi_modbus keeps its synchronous register path unchanged.
- routes/devices.js: discover() note for mhi_mqtt (units self-register).
- No DB migration: existing status columns cover it.

Credentials: getCredentials() now prefers env-injected creds
(MQTT_BROKER_HOST/USERNAME/PASSWORD) over the settings fetch — the broker
hashes each dynsec client password show-once, so it can't be re-served from
settings at runtime. Compose passes the new env through.

Frontend: add mhi_mqtt to DeviceType/labels/implemented list, render
MhiControlPanel and count it as an AC device type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-13 13:31:01 +00:00
parent d43ed903f8
commit c873e4a270
8 changed files with 235 additions and 24 deletions

View file

@ -2,7 +2,7 @@ import { Thermometer, BatteryLow, TriangleAlert, Snowflake, Flame, Fan, Droplet,
import type { ZoneStatus } from '../types'
import { ROOM_STATE_LABELS, MHI_FAN_SPEEDS } from '../types'
const AC_DEVICE_TYPES = ['mhi_modbus', 'midea', 'daikin', 'home_assistant']
const AC_DEVICE_TYPES = ['mhi_modbus', 'mhi_mqtt', 'midea', 'daikin', 'home_assistant']
const MODE_ICONS: Record<string, typeof Snowflake> = {
cool: Snowflake,

View file

@ -100,7 +100,7 @@ export default function ZoneDetailModal({ zone, onClose, onSaved }: {
</div>
<span className={`badge badge-health-${d.health_state}`}>{d.health_state.replace('_', ' ')}</span>
</div>
{d.device_type === 'mhi_modbus' && <MhiControlPanel deviceId={d.id} canControl={canControl} />}
{(d.device_type === 'mhi_modbus' || d.device_type === 'mhi_mqtt') && <MhiControlPanel deviceId={d.id} canControl={canControl} />}
</div>
))}

View file

@ -10,7 +10,7 @@ import GatewaysPanel from '../components/GatewaysPanel'
import { useAuth } from '../components/AuthGate'
import { can } from '../types'
const ALL_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'mhi_modbus', 'midea', 'daikin', 'home_assistant']
const ALL_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'mhi_mqtt', 'mhi_modbus', 'midea', 'daikin', 'home_assistant']
export default function Devices() {
const { user } = useAuth()

View file

@ -1,7 +1,7 @@
export type ZoneType = 'room' | 'public_area'
export type ZoneSource = 'newbook' | 'manual'
export type RoomState = 'vacant' | 'booked' | 'heating_up' | 'occupied' | 'cooling_down'
export type DeviceType = 'shelly_trv' | 'mhi_modbus' | 'midea' | 'daikin' | 'home_assistant'
export type DeviceType = 'shelly_trv' | 'mhi_modbus' | 'mhi_mqtt' | 'midea' | 'daikin' | 'home_assistant'
export type HealthState = 'healthy' | 'degraded' | 'poor' | 'unresponsive' | 'calibration_error'
export type PhotoType = 'device' | 'serial_plate'
@ -16,14 +16,15 @@ export const ROOM_STATE_LABELS: Record<RoomState, string> = {
export const DEVICE_TYPE_LABELS: Record<DeviceType, string> = {
shelly_trv: 'Shelly TRV',
mhi_modbus: 'MHI Aircon (Modbus)',
mhi_mqtt: 'MHI Aircon (MQTT)',
midea: 'Midea Split',
daikin: 'Daikin Split',
home_assistant: 'Home Assistant',
}
// Device types with an implemented Phase 1 driver — everything else in
// DeviceType is an enum value only, ready for its Phase 2/3 driver.
export const IMPLEMENTED_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'home_assistant']
// Device types with an implemented Phase 1/2 driver — everything else in
// DeviceType is an enum value only, ready for its Phase 3 driver.
export const IMPLEMENTED_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'mhi_mqtt', 'home_assistant']
export interface Zone {
id: number