Add Daikin local-API driver — MAC-matched subnet sweep instead of UDP broadcast

Daikin BRP069/BRP072-class units are WiFi/DHCP with no fixed IP. hvac's backend
runs as a Docker container on its own bridge network (not directly on the LXC's
admin-VLAN NIC), so UDP broadcast discovery risked silently failing to egress —
instead, every unit is identified by MAC (external_ref), and a background poller
periodically HTTP-sweeps the configured subnet CIDR and matches replies by MAC,
self-healing zone_devices.device_ip whenever a lease changes. Only needs plain
outbound HTTP, same path already proven by the MHI Modbus TCP driver.

- lib/drivers/daikin.js: discover()/getStatus()/setTarget() against the local
  /common/basic_info + /aircon/get_control_info + /aircon/set_control_info API
  (confirmed live against firmware 4_2_303, 2026-08-25). Mode/fan-rate encoding
  taken from community reverse-engineering, not yet verified against a live
  setTarget() call — flagged in comments for first real control test.
- lib/daikin-poller.js: 60s status poll (cached IP) + 5min subnet resweep
  (self-heal IP + auto-register new units, never auto-assigned to a zone).
- lib/device-registry.js: shared discover-upsert, extracted so both the manual
  Discover button and the background sweep record device_ip the same way.
- ac-control.js / routes/override.js: daikin-status/daikin-control endpoints
  and commandAcDevice() branch, mirroring the existing MHI control path.
- Settings page: new "Daikin subnet (CIDR)" field (daikin_subnet_cidr config
  key) — sweeps do nothing until this is set.
- Frontend: DaikinControlPanel.tsx (on/off/mode/setpoint/fan), wired into
  ZoneDetailModal; daikin moved from NOT_YET_IMPLEMENTED to a real driver.

Backend node --check and frontend tsc --noEmit both clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 12:01:58 +00:00
parent 5fc576aabd
commit 373f66ef5c
12 changed files with 559 additions and 27 deletions

View file

@ -22,9 +22,9 @@ export const DEVICE_TYPE_LABELS: Record<DeviceType, string> = {
home_assistant: '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']
// Device types with an implemented driver — everything else in DeviceType is an enum
// value only, ready for its driver (currently just 'midea').
export const IMPLEMENTED_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'mhi_mqtt', 'home_assistant', 'daikin']
// TRV temps/offsets and aircon template fields are all nullable on a zone — NULL means
// "inherit the matching field from hvac_defaults" (see HvacDefaults below); a non-null
@ -172,6 +172,23 @@ export interface MhiStatus {
export const MHI_MODES = ['cool', 'heat', 'fan', 'auto', 'dry'] as const
export const MHI_FAN_SPEEDS = ['low', 'medium', 'high', 'powerful'] as const
// Daikin local-API mode/fan enums — see backend/src/lib/drivers/daikin.js for the
// encode/decode against the unit's actual wire values (unverified against live control
// commands as of 2026-08-25, same "verify on first real write" caveat as MHI's early days).
export const DAIKIN_MODES = ['cool', 'heat', 'fan', 'auto', 'dry'] as const
export const DAIKIN_FAN_SPEEDS = ['auto', 'low', 'medium', 'high', 'powerful'] as const
export interface DaikinStatus {
external_ref: string
onOff?: string
mode?: string
setpoint?: number
fanSpeed?: string
roomTemp?: number
status_updated_at?: string | null
error?: string
}
export interface DevicePhoto {
id: number
device_id: number
@ -260,6 +277,7 @@ export interface AppConfig {
default_departure_time: string
maintenance_url: string
maintenance_api_key: string
daikin_subnet_cidr?: string
excluded_site_ids?: string[]
hvac_defaults?: HvacDefaults
}