Add MHI aircon manual control (Phase 2, Modbus only)

Manual on/off/mode/setpoint/fan control for MHI indoor units via the
already-verified Intesis Modbus TCP gateway, deliberately NOT wired into
the NewBook-driven scheduler yet — that stays deferred. Modbus doesn't
depend on the MQTT broker (separate infra, still unbuilt), so this can be
deployed and tested standalone.

- mhi_gateways table (connection config: host/port/slave id/address base)
  + zone_devices columns for MHI devices (gateway id, unit index, IU hint,
  and the per-device register map staged from a MAPS import — ground
  truth, never re-derived from room/IU at runtime)
- drivers/mhi-modbus.js: discover/getStatus/setTarget over modbus-serial,
  same interface as trv.js/homeassistant.js; per-gateway request queue
  since Modbus TCP requires serialised requests; falls back to the
  profile's stride formula (with a loud warning) only if a unit has never
  been through an import
- routes/mhi-gateways.js: gateway CRUD, live test-connection, xlsx import
  (stages the parsed result for review — never auto-creates devices),
  per-unit 'assign to zone' as the explicit commit step
- routes/override.js: GET .../mhi-status, POST .../mhi-control (control cap)
- frontend: GatewaysPanel (add gateway, test connection, import + preview,
  assign units) and MhiControlPanel (on/off, mode, setpoint, fan) wired
  into Devices.tsx / ZoneDetailModal.tsx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-27 14:43:38 +00:00
parent a9b5703c57
commit e5c40bbb8c
14 changed files with 935 additions and 20 deletions

View file

@ -73,8 +73,71 @@ export interface Device extends DeviceSummary {
photo_count: number
created_at: string
updated_at: string
mhi_gateway_id?: number | null
mhi_unit_index?: number | null
mhi_iu_address?: number | null
mhi_register_map?: Record<string, MhiRegisterField> | null
}
// ── MHI Modbus gateways (manual aircon control) ──────────────────────────
export interface MhiRegisterField {
address: number
active: boolean
readWrite: string | null
}
export interface MhiImportedUnit {
unitIndex: number
iu: number | null
ou: number | null
fields: Record<string, MhiRegisterField>
}
export interface MhiImportResult {
meta: {
projectName: string | null
mapsVersion: string | null
internalProtocol: string | null
externalProtocol: string | null
timestamp: string | null
}
global: Record<string, MhiRegisterField>
units: MhiImportedUnit[]
unmatchedRows: { row: number; description: string }[]
}
export interface MhiGateway {
id: number
name: string
host: string
port: number
slave_id: number
address_base: number
imported_filename: string | null
imported_units: MhiImportResult | null
imported_at: string | null
created_at: string
updated_at: string
assigned_count: number
}
export interface MhiStatus {
external_ref: string
onOff?: string
mode?: string
setpoint?: number
fanSpeed?: string
roomTemp?: number
errorCode?: number
compressor?: string
commStatus?: string
filterSign?: string
error?: string
}
export const MHI_MODES = ['cool', 'heat', 'fan', 'auto', 'dry'] as const
export const MHI_FAN_SPEEDS = ['low', 'medium', 'high', 'powerful'] as const
export interface DevicePhoto {
id: number
device_id: number