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:
parent
5fc576aabd
commit
373f66ef5c
12 changed files with 559 additions and 27 deletions
|
|
@ -6,24 +6,29 @@ import { requireAuth, requireCap } from '../auth.js'
|
|||
import { pool, logActivity } from '../db.js'
|
||||
import * as trvDriver from '../lib/drivers/trv.js'
|
||||
import * as haDriver from '../lib/drivers/homeassistant.js'
|
||||
import * as daikinDriver from '../lib/drivers/daikin.js'
|
||||
import { createMaintenanceAsset } from '../lib/maintenance-client.js'
|
||||
import { upsertDiscovered } from '../lib/device-registry.js'
|
||||
|
||||
const ALLOWED_IMAGES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||
const PHOTO_TYPES = ['device', 'serial_plate']
|
||||
|
||||
// device_type -> driver implementing discover()/getStatus()/setTarget(). Phase 3
|
||||
// types (midea, daikin) are enum values only — no driver yet. mhi_modbus DOES
|
||||
// have a driver (lib/drivers/mhi-modbus.js) but it isn't wired into this generic
|
||||
// no-args discover flow — Modbus has no broadcast scan, and discover() there
|
||||
// needs a specific already-configured gateway, so it's only ever called from
|
||||
// routes/mhi-gateways.js's test-connection route. Units are onboarded via
|
||||
// "Gateways" (Devices page): configure a gateway, import its MAPS register map,
|
||||
// then assign units to zones — never via this Discover button.
|
||||
// device_type -> driver implementing discover()/getStatus()/setTarget(). midea is
|
||||
// still an enum value only — no driver yet. mhi_modbus DOES have a driver
|
||||
// (lib/drivers/mhi-modbus.js) but it isn't wired into this generic no-args discover
|
||||
// flow — Modbus has no broadcast scan, and discover() there needs a specific
|
||||
// already-configured gateway, so it's only ever called from routes/mhi-gateways.js's
|
||||
// test-connection route. Units are onboarded via "Gateways" (Devices page): configure
|
||||
// a gateway, import its MAPS register map, then assign units to zones — never via
|
||||
// this Discover button. daikin's discover() does a full-subnet HTTP sweep (no
|
||||
// broadcast — see lib/drivers/daikin.js) so it fits this generic flow fine, but
|
||||
// needs "Daikin subnet (CIDR)" set on the Settings page first.
|
||||
const DRIVERS = {
|
||||
shelly_trv: trvDriver,
|
||||
home_assistant: haDriver,
|
||||
daikin: daikinDriver,
|
||||
}
|
||||
const NOT_YET_IMPLEMENTED = ['midea', 'daikin']
|
||||
const NOT_YET_IMPLEMENTED = ['midea']
|
||||
const MHI_MODBUS_NOTE = "MHI aircon units aren't found via Discover — configure a gateway and import its register " +
|
||||
'map under Devices -> Gateways, then assign units to zones there.'
|
||||
const MHI_MQTT_NOTE = 'MHI aircon units on native MQTT self-register as they publish status — they appear ' +
|
||||
|
|
@ -71,21 +76,10 @@ export async function deviceRoutes(app, opts) {
|
|||
if (!driver) return reply.status(400).send({ error: `Unknown device_type: ${device_type}` })
|
||||
|
||||
const result = await driver.discover()
|
||||
if (!result.ok) return reply.status(200).send(result) // e.g. broker/HA not reachable — not a hard error
|
||||
if (!result.ok) return reply.status(200).send(result) // e.g. broker/HA/CIDR not configured — not a hard error
|
||||
|
||||
let discoveredCount = 0
|
||||
for (const d of result.devices) {
|
||||
const res = await pool.query(
|
||||
`INSERT INTO zone_devices (device_type, external_ref, discovered_name)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (device_type, external_ref) DO UPDATE SET discovered_name = EXCLUDED.discovered_name, last_seen = NOW()
|
||||
RETURNING (xmax = 0) AS inserted`,
|
||||
[device_type, d.external_ref, d.discovered_name || d.external_ref]
|
||||
)
|
||||
if (res.rows[0].inserted) discoveredCount++
|
||||
}
|
||||
|
||||
return { ok: true, found: result.devices.length, new: discoveredCount }
|
||||
const discoveredCount = await upsertDiscovered(device_type, result.devices)
|
||||
return { ok: true, found: result.devices.length, new: discoveredCount, note: result.note }
|
||||
})
|
||||
|
||||
// PATCH /api/devices/:id — zone/location assignment (a living mapping, not a one-time wizard)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue