Add support for 'water_heater' service

This commit is contained in:
Adrian Jagielak
2025-07-25 01:03:19 +02:00
parent 58e4ea228c
commit ae10c2fb5f
6 changed files with 402 additions and 29 deletions

View File

@@ -54,6 +54,7 @@ import { sensor_watpressure__components } from '../services/sensor_watpressure';
import { sensor_wattemp__components } from '../services/sensor_wattemp';
import { sensor_weight__components } from '../services/sensor_weight';
import { thermostat__components } from '../services/thermostat';
import { water_heater__components } from '../services/water_heater';
import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
import { ha } from './globals';
import { HaMqttComponent } from './mqtt_components/_component';
@@ -209,6 +210,7 @@ const serviceHandlers: {
sensor_wattemp: sensor_wattemp__components,
sensor_weight: sensor_weight__components,
thermostat: thermostat__components,
water_heater: water_heater__components,
};
export function haPublishDevice(parameters: {

View File

@@ -14,6 +14,11 @@ import { ha } from './globals';
{
"name": "presence",
"values": [
{
"ts": "2025-07-22 16:21:31 +0200",
"val": true,
"val_t": "bool"
},
{
"ts": "2025-07-22 16:21:30 +0200",
"val": false,
@@ -52,18 +57,85 @@ import { ha } from './globals';
}
],
"name": "battery"
},
{
"addr": "/rt:dev/rn:hoiax/ad:1/sv:water_heater/ad:2",
"attributes": [
{
"name": "state",
"values": [
{
"ts": "2023-04-03 13:37:22 +0200",
"val": "idle",
"val_t": "string"
}
]
},
{
"name": "setpoint",
"values": [
{
"ts": "2023-03-27 14:19:52 +0200",
"val": {
"temp": 49,
"type": "vacation",
"unit": "C"
},
"val_t": "object"
},
{
"ts": "2023-03-27 14:19:52 +0200",
"val": {
"temp": 60,
"type": "normal",
"unit": "C"
},
"val_t": "object"
},
{
"ts": "2023-12-21 09:44:28 +0100",
"val": {
"temp": 85.0,
"type": "boost",
"unit": "C"
},
"val_t": "object"
},
{
"ts": "2023-03-27 14:19:52 +0200",
"val": {
"temp": 60,
"type": "external",
"unit": "C"
},
"val_t": "object"
}
]
},
{
"name": "mode",
"values": [
{
"ts": "2023-04-05 16:08:43 +0200",
"val": "off",
"val_t": "string"
}
]
}
],
"name": "water_heater"
}
]
}
```
Output (assuming hub ID 123456):
Saved state (assuming hub ID 123456):
```
topic: homeassistant/device/futurehome_123456_1/state
{
"/rt:dev/rn:zigbee/ad:1/sv:sensor_presence/ad:1_1": {
"presence": false
"presence": true
},
"/rt:dev/rn:zigbee/ad:1/sv:battery/ad:1_1": {
"lvl": 1,
@@ -71,6 +143,28 @@ topic: homeassistant/device/futurehome_123456_1/state
"event": "low_battery",
"status": "deactiv"
}
},
"/rt:dev/rn:hoiax/ad:1/sv:water_heater/ad:2": {
"state": "idle",
"setpoint": {
"vacation": {
"temp": 49,
"unit": "C"
},
"normal": {
"temp": 60,
"unit": "C"
},
"boost": {
"temp": 85.0,
"unit": "C"
},
"external": {
"temp": 60,
"unit": "C"
}
},
"mode": "off"
}
}
```
@@ -81,6 +175,48 @@ const haStateCache: Record<
Record<string, Record<string, any>> // payload (addr → { attr → value })
> = {};
/**
* Helper function to process multiple values for an attribute, handling typed values
*/
function processAttributeValues(values: any[]): any {
if (!values || values.length === 0) {
return undefined;
}
// Sort by timestamp to get the latest values first
const sortedValues = [...values].sort((a, b) => {
const tsA = new Date(a.ts).getTime();
const tsB = new Date(b.ts).getTime();
return tsB - tsA; // Latest first
});
// Check if any value has a 'type' property in its val object
const hasTypedValues = sortedValues.some(
(v) => v.val && typeof v.val === 'object' && v.val.type,
);
if (!hasTypedValues) {
// No typed values, return the latest value
return sortedValues[0].val;
}
// Group by type, keeping only the latest value for each type
const typeMap: Record<string, any> = {};
for (const value of sortedValues) {
if (value.val && typeof value.val === 'object' && value.val.type) {
const type = value.val.type;
if (!typeMap[type]) {
// Create a copy without the 'type' property
const { type: _, ...valueWithoutType } = value.val;
typeMap[type] = valueWithoutType;
}
}
}
return typeMap;
}
/**
* Publishes the full state of a Futurehome device to Home Assistant and
* stores a copy in the private cache above.
@@ -102,8 +238,10 @@ export function haUpdateState(parameters: {
const serviceState: Record<string, any> = {};
for (const attr of service.attributes || []) {
const value = attr.values?.[0]?.val;
serviceState[attr.name] = value;
const processedValue = processAttributeValues(attr.values || []);
if (processedValue !== undefined) {
serviceState[attr.name] = processedValue;
}
}
haState[service.addr] = serviceState;
@@ -123,13 +261,13 @@ export function haUpdateState(parameters: {
*
* @param topic Full FIMP event topic, e.g.
* "pt:j1/mt:evt/rt:dev/rn:zigbee/ad:1/sv:sensor_temp/ad:3_1"
* @param value The new sensor reading (number, boolean, string, …)
* @param value The new sensor reading (number, boolean, string, object with type, …)
* @param attrName Attribute name to store the reading to
*
* The prefix "pt:j1/mt:evt" is removed before matching so that the remainder
* exactly matches the address keys stored in the cached HA payloads.
*/
export function haUpdateStateSensorReport(parameters: {
export function haUpdateStateValueReport(parameters: {
topic: string;
value: any;
attrName: string;
@@ -140,8 +278,39 @@ export function haUpdateStateSensorReport(parameters: {
for (const [stateTopic, payload] of Object.entries(haStateCache)) {
if (!payload[sensorAddr]) continue;
// Update the reading inplace
payload[sensorAddr][parameters.attrName] = parameters.value;
// Check if the new value has a type property
if (
parameters.value &&
typeof parameters.value === 'object' &&
parameters.value.type
) {
// Handle typed value update
const type = parameters.value.type;
const { type: _, ...valueWithoutType } = parameters.value;
// Get current attribute value
const currentAttrValue = payload[sensorAddr][parameters.attrName];
if (
currentAttrValue &&
typeof currentAttrValue === 'object' &&
!Array.isArray(currentAttrValue)
) {
// Current value is already a type map, update the specific type
payload[sensorAddr][parameters.attrName] = {
...currentAttrValue,
[type]: valueWithoutType,
};
} else {
// Current value is not a type map, convert it to one
payload[sensorAddr][parameters.attrName] = {
[type]: valueWithoutType,
};
}
} else {
// Handle regular value update (non-typed)
payload[sensorAddr][parameters.attrName] = parameters.value;
}
log.debug(
`Publishing updated sensor value for "${sensorAddr}" to "${stateTopic}"`,