mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 15:47:08 +00:00
Update 'water_heater' service implementation
This commit is contained in:
parent
dbc8b1d800
commit
51693d05c3
@ -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') }}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user