Fix Daikin driver: use node:http instead of fetch() — undici gets 0-byte bodies
Live-tested against the real unit (10.4.2.6, 2026-08-25): a full-subnet sweep came back with 0 units found despite the device being reachable and fast (~150ms). Root cause isolated via a series of in-container diagnostics, not a timeout or CIDR-math bug — Node's fetch() (undici) reliably read a 0-byte body from this adapter's embedded webserver, which sends a connection-close-delimited response (no Content-Length, HTTP/1.0-style) rather than a Content-Length/chunked body. wget, curl, and node:http all read the real ~400-byte body fine; fetch() alone did not. Switched every request in lib/drivers/daikin.js (probe, get_control_info, get_sensor_info, set_control_info) from fetch() to a small node:http-based httpGet() helper with Connection: close, and left a comment warning not to "simplify" this back to fetch(). Bumped SWEEP_TIMEOUT_MS 500ms -> 800ms for headroom (confirmed real round-trip is ~150-180ms, so this was never the actual problem, but a little slack is cheap). node --check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
373f66ef5c
commit
1a06a860f5
1 changed files with 34 additions and 12 deletions
|
|
@ -15,12 +15,19 @@
|
|||
// whenever a lease changes. This only needs plain outbound HTTP, which already works
|
||||
// through the container's normal NAT/routing (same path proven by the MHI Modbus TCP
|
||||
// driver) — no macvlan/broadcast plumbing required.
|
||||
//
|
||||
// Uses node:http, NOT fetch() — confirmed live 2026-08-25 that this adapter's embedded
|
||||
// webserver sends a connection-close-delimited body (no Content-Length, HTTP/1.0-style)
|
||||
// and undici's fetch() reliably reads it as 0 bytes even though the connection itself
|
||||
// succeeds fast (~150ms). wget/curl and node:http both read it correctly by consuming
|
||||
// until the socket closes. Do not "simplify" this back to fetch() — it silently breaks.
|
||||
import http from 'node:http'
|
||||
import { pool } from '../../db.js'
|
||||
|
||||
export const DEVICE_TYPE = 'daikin'
|
||||
|
||||
const SWEEP_CONCURRENCY = 50
|
||||
const SWEEP_TIMEOUT_MS = 500
|
||||
const SWEEP_TIMEOUT_MS = 800
|
||||
|
||||
// Mode/fan-rate encodings per the widely-used community reverse-engineering of Daikin's
|
||||
// local BRP-adapter API (same wire format across the BRP069/BRP072 firmware family) —
|
||||
|
|
@ -76,6 +83,21 @@ function cidrToHosts(cidr) {
|
|||
return hosts
|
||||
}
|
||||
|
||||
// Connection: close forces the adapter to actually close the socket at the end of the
|
||||
// body (rather than us guessing when a keep-alive stream is "done") — read to 'end'.
|
||||
function httpGet(url, timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.get(url, { headers: { Connection: 'close' }, timeout: timeoutMs }, res => {
|
||||
let body = ''
|
||||
res.setEncoding('utf8')
|
||||
res.on('data', chunk => { body += chunk })
|
||||
res.on('end', () => resolve({ statusCode: res.statusCode, body }))
|
||||
})
|
||||
req.on('timeout', () => req.destroy(new Error(`timeout after ${timeoutMs}ms`)))
|
||||
req.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function mapWithConcurrency(items, limit, fn) {
|
||||
const results = new Array(items.length)
|
||||
let idx = 0
|
||||
|
|
@ -91,9 +113,9 @@ async function mapWithConcurrency(items, limit, fn) {
|
|||
|
||||
async function probeOne(ip, timeoutMs) {
|
||||
try {
|
||||
const res = await fetch(`http://${ip}/common/basic_info`, { signal: AbortSignal.timeout(timeoutMs) })
|
||||
if (!res.ok) return null
|
||||
const info = parseKV(await res.text())
|
||||
const { statusCode, body } = await httpGet(`http://${ip}/common/basic_info`, timeoutMs)
|
||||
if (statusCode !== 200) return null
|
||||
const info = parseKV(body)
|
||||
if (info.ret !== 'OK' || info.type !== 'aircon' || !info.mac) return null
|
||||
return {
|
||||
external_ref: info.mac,
|
||||
|
|
@ -154,11 +176,11 @@ export async function getStatus(externalRef) {
|
|||
|
||||
try {
|
||||
const [controlRes, sensorRes] = await Promise.all([
|
||||
fetch(`http://${device.device_ip}/aircon/get_control_info`, { signal: AbortSignal.timeout(5000) }),
|
||||
fetch(`http://${device.device_ip}/aircon/get_sensor_info`, { signal: AbortSignal.timeout(5000) }),
|
||||
httpGet(`http://${device.device_ip}/aircon/get_control_info`, 5000),
|
||||
httpGet(`http://${device.device_ip}/aircon/get_sensor_info`, 5000),
|
||||
])
|
||||
const control = parseKV(await controlRes.text())
|
||||
const sensor = parseKV(await sensorRes.text())
|
||||
const control = parseKV(controlRes.body)
|
||||
const sensor = parseKV(sensorRes.body)
|
||||
if (control.ret !== 'OK') throw new Error(`Unit returned ${control.ret || 'no response'}`)
|
||||
|
||||
return {
|
||||
|
|
@ -184,8 +206,8 @@ export async function setTarget(externalRef, { onOff, mode, tempC, fanSpeed } =
|
|||
if (!device?.device_ip) return false
|
||||
|
||||
try {
|
||||
const curRes = await fetch(`http://${device.device_ip}/aircon/get_control_info`, { signal: AbortSignal.timeout(5000) })
|
||||
const current = parseKV(await curRes.text())
|
||||
const curRes = await httpGet(`http://${device.device_ip}/aircon/get_control_info`, 5000)
|
||||
const current = parseKV(curRes.body)
|
||||
if (current.ret !== 'OK') throw new Error(`Unit returned ${current.ret || 'no response'}`)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
|
|
@ -196,8 +218,8 @@ export async function setTarget(externalRef, { onOff, mode, tempC, fanSpeed } =
|
|||
f_rate: fanSpeed ? (FAN_TO_DAIKIN[fanSpeed] ?? current.f_rate) : current.f_rate,
|
||||
f_dir: current.f_dir ?? '0',
|
||||
})
|
||||
const setRes = await fetch(`http://${device.device_ip}/aircon/set_control_info?${params}`, { signal: AbortSignal.timeout(5000) })
|
||||
const result = parseKV(await setRes.text())
|
||||
const setRes = await httpGet(`http://${device.device_ip}/aircon/set_control_info?${params}`, 5000)
|
||||
const result = parseKV(setRes.body)
|
||||
return result.ret === 'OK'
|
||||
} catch (err) {
|
||||
console.error(`[daikin driver] setTarget failed for ${externalRef}:`, err.message)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue