Update 'water_heater' service implementation

This commit is contained in:
Adrian Jagielak 2025-07-25 01:53:16 +02:00
parent dbc8b1d800
commit 51693d05c3
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
2 changed files with 149 additions and 156 deletions

View File

@ -131,10 +131,12 @@ export function thermostat__components(
// Add current temperature from paired sensor_temp service // Add current temperature from paired sensor_temp service
const sensorTempAddr = replaceSvcInAddr(svc.addr, 'sensor_temp'); const sensorTempAddr = replaceSvcInAddr(svc.addr, 'sensor_temp');
climateComponent.current_temperature_topic = stateTopic;
climateComponent.current_temperature_template = `{{ value_json['${sensorTempAddr}'].sensor | default(0) }}`; climateComponent.current_temperature_template = `{{ value_json['${sensorTempAddr}'].sensor | default(0) }}`;
// Add action/state reporting if supported // Add action/state reporting if supported
if (supStates.length > 0) { if (supStates.length > 0) {
climateComponent.action_topic = stateTopic;
climateComponent.action_template = `{% set state = value_json['${svc.addr}'].state | default('idle') %}{{ {'idle': 'idle', 'heat': 'heating', 'cool': 'cooling', 'fan': 'fan'}.get(state, 'off') }}`; climateComponent.action_template = `{% set state = value_json['${svc.addr}'].state | default('idle') %}{{ {'idle': 'idle', 'heat': 'heating', 'cool': 'cooling', 'fan': 'fan'}.get(state, 'off') }}`;
} }

View File

@ -1,17 +1,15 @@
import { sendFimpMsg } from '../fimp/fimp'; import { sendFimpMsg } from '../fimp/fimp';
import { replaceSvcInAddr } from '../fimp/helpers';
import { import {
VinculumPd7Device, VinculumPd7Device,
VinculumPd7Service, VinculumPd7Service,
} from '../fimp/vinculum_pd7_device'; } from '../fimp/vinculum_pd7_device';
import { HaMqttComponent } from '../ha/mqtt_components/_component'; import { HaMqttComponent } from '../ha/mqtt_components/_component';
import { SelectComponent } from '../ha/mqtt_components/select';
import { NumberComponent } from '../ha/mqtt_components/number';
import { SensorComponent } from '../ha/mqtt_components/sensor';
import { import {
CommandHandlers, CommandHandlers,
ServiceComponentsCreationResult, ServiceComponentsCreationResult,
} from '../ha/publish_device'; } from '../ha/publish_device';
import { SwitchComponent } from '../ha/mqtt_components/switch'; import { haGetCachedState } from '../ha/update_state';
export function water_heater__components( export function water_heater__components(
topicPrefix: string, topicPrefix: string,
@ -21,186 +19,179 @@ export function water_heater__components(
const components: Record<string, HaMqttComponent> = {}; const components: Record<string, HaMqttComponent> = {};
const commandHandlers: CommandHandlers = {}; const commandHandlers: CommandHandlers = {};
// Extract supported modes, setpoints, and states from service properties // Extract supported properties
const supportedModes = svc.props?.sup_modes || [ const supModes = svc.props?.sup_modes || [];
'off', const supSetpoints = svc.props?.sup_setpoints || [];
'normal', const supStates = svc.props?.sup_states || [];
'boost',
'eco',
];
const supportedSetpoints = svc.props?.sup_setpoints || [];
const supportedStates = svc.props?.sup_states || ['idle', 'heat'];
const supRange = svc.props?.sup_range; const supRange = svc.props?.sup_range;
const supRanges = svc.props?.sup_ranges; const supRanges = svc.props?.sup_ranges;
const supStep = svc.props?.sup_step || 1.0; const supStep = svc.props?.sup_step || 1.0;
// Determine temperature range and unit // Determine temperature range
let minTemp = 0; // Default minimum let minTemp = 0; // Default minimum
let maxTemp = 100; // Default maximum let maxTemp = 100; // Default maximum
const temperatureUnit = 'C'; // Default unit
if (supRange) { if (supRange) {
minTemp = supRange.min ?? minTemp; minTemp = supRange.min;
maxTemp = supRange.max ?? maxTemp; maxTemp = supRange.max;
} } else if (supRanges) {
// Find the broadest range from all setpoint ranges
// 1. Mode Control (Select Component) for (const setpoint of supSetpoints) {
if (supportedModes.length > 0) { const range = supRanges[setpoint];
const modeCommandTopic = `${topicPrefix}${svc.addr}/mode_command`; if (range) {
if (range.min < minTemp) minTemp = range.min;
const selectComponent: SelectComponent = { if (range.max > maxTemp) maxTemp = range.max;
unique_id: `${svc.addr}_mode`,
platform: 'select',
name: 'mode',
options: supportedModes,
command_topic: modeCommandTopic,
optimistic: false,
value_template: `{{ value_json['${svc.addr}'].mode }}`,
};
components[`${svc.addr}_mode`] = selectComponent;
commandHandlers[modeCommandTopic] = async (payload: string) => {
if (!supportedModes.includes(payload)) {
return;
} }
await sendFimpMsg({
address: svc.addr,
service: 'water_heater',
cmd: 'cmd.mode.set',
val_t: 'string',
val: payload,
});
};
}
// 2. Setpoint Controls (Number Components)
if (supportedSetpoints.length > 0) {
for (const setpointType of supportedSetpoints) {
const setpointCommandTopic = `${topicPrefix}${svc.addr}/setpoint_${setpointType}_command`;
// Determine range for this specific setpoint
let setpointMinTemp = minTemp;
let setpointMaxTemp = maxTemp;
if (supRanges && supRanges[setpointType]) {
setpointMinTemp = supRanges[setpointType].min ?? minTemp;
setpointMaxTemp = supRanges[setpointType].max ?? maxTemp;
}
const numberComponent: NumberComponent = {
unique_id: `${svc.addr}_setpoint_${setpointType}`,
platform: 'number',
entity_category: 'config',
name: `${setpointType} setpoint`,
min: setpointMinTemp,
max: setpointMaxTemp,
step: supStep,
unit_of_measurement: temperatureUnit === 'C' ? '°C' : '°F',
command_topic: setpointCommandTopic,
optimistic: false,
value_template: `{{ value_json['${svc.addr}'].setpoint.${setpointType}.temp if value_json['${svc.addr}'].setpoint.${setpointType} else 0 }}`,
};
components[`${svc.addr}_setpoint_${setpointType}`] = numberComponent;
commandHandlers[setpointCommandTopic] = async (payload: string) => {
const temperature = parseFloat(payload);
if (
Number.isNaN(temperature) ||
temperature < setpointMinTemp ||
temperature > setpointMaxTemp
) {
return;
}
await sendFimpMsg({
address: svc.addr,
service: 'water_heater',
cmd: 'cmd.setpoint.set',
val_t: 'object',
val: {
type: setpointType,
temp: temperature,
unit: temperatureUnit,
},
});
};
} }
} }
// 3. Operational State Sensor // Main water heater component
if (svc.intf?.includes('evt.state.report') && supportedStates.length > 0) { const stateTopic = `${topicPrefix}/state`;
const sensorComponent: SensorComponent = { const modeCommandTopic = `${topicPrefix}${svc.addr}/mode/command`;
unique_id: `${svc.addr}_state`, const tempCommandTopic = `${topicPrefix}${svc.addr}/temperature/command`;
platform: 'sensor',
entity_category: 'diagnostic', // Map FIMP modes to appropriate Home Assistant water heater modes
name: 'operational state', const mapFimpToHaMode = (fimpMode: string): string => {
value_template: `{{ value_json['${svc.addr}'].state }}`, const modeMap: Record<string, string> = {
off: 'off',
normal: 'heat_pump',
boost: 'high_demand',
eco: 'eco',
}; };
return modeMap[fimpMode] || fimpMode;
components[`${svc.addr}_state`] = sensorComponent;
}
// 4. Current Setpoint Temperature Sensor (shows active setpoint value)
// This provides a way to see what temperature the water heater is currently targeting
const currentSetpointSensorComponent: SensorComponent = {
unique_id: `${svc.addr}_current_setpoint`,
platform: 'sensor',
entity_category: 'diagnostic',
name: 'current setpoint',
unit_of_measurement: temperatureUnit === 'C' ? '°C' : '°F',
device_class: 'temperature',
// Template to extract current setpoint based on active mode
value_template: `{% set mode = value_json['${svc.addr}'].mode %}{% set setpoints = value_json['${svc.addr}'].setpoint %}{% if setpoints and setpoints[mode] %}{{ setpoints[mode].temp }}{% else %}unknown{% endif %}`,
}; };
components[`${svc.addr}_current_setpoint`] = currentSetpointSensorComponent; // Map Home Assistant modes back to FIMP modes
const mapHaToFimpMode = (haMode: string): string => {
const reverseMap: Record<string, string> = {
off: 'off',
heat_pump: 'normal',
high_demand: 'boost',
eco: 'eco',
};
// 5. Power Control Switch (maps to mode on/off) // If we have the exact mode in supported modes, use it
const powerCommandTopic = `${topicPrefix}${svc.addr}/power_command`; if (supModes.includes(haMode)) {
return haMode;
}
const switchComponent: SwitchComponent = { // Otherwise try to find a suitable FIMP mode
unique_id: `${svc.addr}_power`, const fimpMode = reverseMap[haMode];
platform: 'switch', if (fimpMode && supModes.includes(fimpMode)) {
name: 'power', return fimpMode;
command_topic: powerCommandTopic, }
return haMode; // Fallback to original
};
// Get supported HA modes
const haModes = supModes
.map(mapFimpToHaMode)
.filter(
(mode: string, index: number, arr: string[]) =>
arr.indexOf(mode) === index,
);
// Water heater component configuration
const waterHeaterComponent: HaMqttComponent = {
unique_id: svc.addr,
platform: 'water_heater',
modes: haModes.length > 0 ? haModes : ['off', 'heat_pump'],
mode_command_topic: modeCommandTopic,
mode_state_topic: stateTopic,
mode_state_template: `{{ value_json['${svc.addr}'].mode | default('off') }}`,
optimistic: false, optimistic: false,
value_template: `{{ 'ON' if value_json['${svc.addr}'].mode != 'off' else 'OFF' }}`, min_temp: minTemp,
payload_on: 'ON', max_temp: maxTemp,
payload_off: 'OFF', temperature_unit: 'C',
precision: supStep >= 1 ? 1 : 0.1,
}; };
components[`${svc.addr}_power`] = switchComponent; // Add current temperature from paired sensor_wattemp service
const sensorWattempAddr = replaceSvcInAddr(svc.addr, 'sensor_wattemp');
waterHeaterComponent.current_temperature_topic = stateTopic;
waterHeaterComponent.current_temperature_template = `{{ value_json['${sensorWattempAddr}'].sensor | default(0) }}`;
commandHandlers[powerCommandTopic] = async (payload: string) => { // Add operation state if supported
let targetMode: string; if (supStates.length > 0) {
// Map FIMP states to Home Assistant water heater states
waterHeaterComponent.value_template = `{% set state = value_json['${svc.addr}'].state | default('idle') %}{{ {'idle': 'idle', 'heat': 'heating'}.get(state, 'idle') }}`;
}
switch (payload) { // Handle temperature control based on available setpoints
case 'ON': if (supSetpoints.length > 0) {
// Turn on to normal mode if available, otherwise first non-off mode waterHeaterComponent.temperature_command_topic = tempCommandTopic;
targetMode = supportedModes.includes('normal') waterHeaterComponent.temperature_state_topic = stateTopic;
? 'normal'
: supportedModes.find((mode: any) => mode !== 'off') || 'normal'; // Create template to get current temperature setpoint based on mode
break; // Priority: current mode setpoint > normal setpoint > first available setpoint
case 'OFF': const setpointTemplate = `{% set mode = value_json['${svc.addr}'].mode | default('normal') %}{% set setpoint = value_json['${svc.addr}'].setpoint %}{% if setpoint[mode] %}{{ setpoint[mode].temp | default(${minTemp}) }}{% elif setpoint['normal'] %}{{ setpoint['normal'].temp | default(${minTemp}) }}{% elif setpoint %}{{ setpoint['${supSetpoints[0]}'].temp | default(${minTemp}) }}{% else %}{{ ${minTemp} }}{% endif %}`;
targetMode = 'off';
break; waterHeaterComponent.temperature_state_template = setpointTemplate;
default: }
return;
components[svc.addr] = waterHeaterComponent;
// Command handlers
// Mode command handler
commandHandlers[modeCommandTopic] = async (payload: string) => {
const fimpMode = mapHaToFimpMode(payload);
if (!supModes.includes(fimpMode)) {
return;
} }
if (supportedModes.includes(targetMode)) { await sendFimpMsg({
address: svc.addr,
service: 'water_heater',
cmd: 'cmd.mode.set',
val_t: 'string',
val: fimpMode,
});
};
// Temperature command handler
if (waterHeaterComponent.temperature_command_topic) {
commandHandlers[tempCommandTopic] = async (payload: string) => {
const temp = parseFloat(payload);
if (Number.isNaN(temp)) {
return;
}
// Validate temperature is within supported range
if (temp < minTemp || temp > maxTemp) {
return;
}
// Get current mode to determine which setpoint to set
const currentState = haGetCachedState({
topic: `${topicPrefix}/state`,
})?.[svc.addr];
const currentMode = currentState?.mode || 'normal';
// Use the current mode as setpoint type if it's in sup_setpoints
// Otherwise use 'normal' as default, or first available setpoint
let setpointType = 'normal';
if (supSetpoints.includes(currentMode)) {
setpointType = currentMode;
} else if (!supSetpoints.includes('normal') && supSetpoints.length > 0) {
setpointType = supSetpoints[0];
}
await sendFimpMsg({ await sendFimpMsg({
address: svc.addr, address: svc.addr,
service: 'water_heater', service: 'water_heater',
cmd: 'cmd.mode.set', cmd: 'cmd.setpoint.set',
val_t: 'string', val_t: 'object',
val: targetMode, val: {
type: setpointType,
temp: temp,
unit: 'C',
},
}); });
} };
}; }
return { return {
components, components,