2025-07-23 22:32:23 +02:00
|
|
|
|
// Maps a Futurehome “thermostat” service to one MQTT *climate* entity.
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// FIMP ➞ HA state path used by the templates
|
|
|
|
|
// value_json[svc.addr].mode – current HVAC mode
|
|
|
|
|
// value_json[svc.addr].setpoint.temp – set-point temperature (string)
|
|
|
|
|
//
|
|
|
|
|
// HA ➞ FIMP commands
|
|
|
|
|
// mode_command_topic → cmd.mode.set
|
|
|
|
|
// temperature_command_topic → cmd.setpoint.set
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2025-07-24 16:34:58 +02:00
|
|
|
|
import { sendFimpMsg } from '../fimp/fimp';
|
|
|
|
|
import {
|
|
|
|
|
VinculumPd7Device,
|
|
|
|
|
VinculumPd7Service,
|
|
|
|
|
} from '../fimp/vinculum_pd7_device';
|
|
|
|
|
import { ClimateComponent } from '../ha/mqtt_components/climate';
|
2025-07-23 22:32:23 +02:00
|
|
|
|
import {
|
|
|
|
|
CommandHandlers,
|
|
|
|
|
ServiceComponentsCreationResult,
|
2025-07-24 16:34:58 +02:00
|
|
|
|
} from '../ha/publish_device';
|
|
|
|
|
import { haGetCachedState } from '../ha/update_state';
|
2025-07-23 22:32:23 +02:00
|
|
|
|
|
|
|
|
|
export function thermostat__components(
|
|
|
|
|
topicPrefix: string,
|
|
|
|
|
_device: VinculumPd7Device,
|
2025-07-24 16:34:58 +02:00
|
|
|
|
svc: VinculumPd7Service,
|
2025-07-23 22:32:23 +02:00
|
|
|
|
): ServiceComponentsCreationResult | undefined {
|
|
|
|
|
const supModes: string[] = svc.props?.sup_modes ?? [];
|
|
|
|
|
const supSetpoints: string[] = svc.props?.sup_setpoints ?? [];
|
|
|
|
|
|
|
|
|
|
if (!supModes.length) return undefined; // nothing useful to expose
|
|
|
|
|
|
2025-07-24 16:34:58 +02:00
|
|
|
|
const defaultSpType = supSetpoints[0] ?? 'heat';
|
2025-07-23 22:32:23 +02:00
|
|
|
|
|
|
|
|
|
const ranges: Record<string, { min?: number; max?: number }> =
|
|
|
|
|
svc.props?.sup_temperatures ?? {};
|
|
|
|
|
const step: number = svc.props?.sup_step ?? 0.5;
|
|
|
|
|
|
|
|
|
|
// Determine overall min/max temp from all advertised ranges
|
|
|
|
|
let minTemp = 1000;
|
|
|
|
|
let maxTemp = -1000;
|
|
|
|
|
for (const sp of supSetpoints) {
|
|
|
|
|
minTemp = Math.min(minTemp, ranges[sp]?.min ?? minTemp);
|
|
|
|
|
maxTemp = Math.max(maxTemp, ranges[sp]?.max ?? maxTemp);
|
|
|
|
|
}
|
|
|
|
|
if (minTemp === 1000) minTemp = 7;
|
|
|
|
|
if (maxTemp === -1000) maxTemp = 35;
|
|
|
|
|
|
|
|
|
|
// Shared JSON blob
|
|
|
|
|
const stateTopic = `${topicPrefix}/state`;
|
|
|
|
|
|
|
|
|
|
// ───────────── command topics ─────────────
|
|
|
|
|
const modeCmdTopic = `${topicPrefix}${svc.addr}/mode/command`;
|
|
|
|
|
const tempCmdTopic = `${topicPrefix}${svc.addr}/temperature/command`;
|
|
|
|
|
|
|
|
|
|
// ───────────── MQTT climate component ─────────────
|
|
|
|
|
const climate: ClimateComponent = {
|
|
|
|
|
unique_id: svc.addr,
|
2025-07-24 15:54:47 +02:00
|
|
|
|
platform: 'climate',
|
2025-07-23 22:32:23 +02:00
|
|
|
|
|
|
|
|
|
// HVAC modes
|
|
|
|
|
modes: supModes,
|
|
|
|
|
mode_command_topic: modeCmdTopic,
|
|
|
|
|
// Even though state topic is often optional as it's already defined by the device object this component is in, the 'climate' expects it
|
|
|
|
|
mode_state_topic: stateTopic,
|
|
|
|
|
mode_state_template: `{{ value_json['${svc.addr}'].mode }}`,
|
|
|
|
|
|
|
|
|
|
// Temperature
|
|
|
|
|
temperature_command_topic: tempCmdTopic,
|
|
|
|
|
temperature_state_topic: stateTopic,
|
|
|
|
|
temperature_state_template: `{{ value_json['${svc.addr}'].setpoint.temp }}`,
|
|
|
|
|
|
|
|
|
|
// Limits / resolution
|
|
|
|
|
min_temp: minTemp,
|
|
|
|
|
max_temp: maxTemp,
|
|
|
|
|
temp_step: step,
|
|
|
|
|
|
2025-07-24 16:48:33 +02:00
|
|
|
|
optimistic: false,
|
2025-07-23 22:32:23 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ───────────── command handlers ─────────────
|
|
|
|
|
const handlers: CommandHandlers = {
|
|
|
|
|
[modeCmdTopic]: async (payload: string) => {
|
|
|
|
|
if (!supModes.includes(payload)) return;
|
|
|
|
|
await sendFimpMsg({
|
|
|
|
|
address: svc.addr!,
|
2025-07-24 16:34:58 +02:00
|
|
|
|
service: 'thermostat',
|
|
|
|
|
cmd: 'cmd.mode.set',
|
|
|
|
|
val_t: 'string',
|
2025-07-23 22:32:23 +02:00
|
|
|
|
val: payload,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
[tempCmdTopic]: async (payload: string) => {
|
|
|
|
|
const t = parseFloat(payload);
|
|
|
|
|
if (Number.isNaN(t)) return;
|
|
|
|
|
|
|
|
|
|
await sendFimpMsg({
|
|
|
|
|
address: svc.addr!,
|
2025-07-24 16:34:58 +02:00
|
|
|
|
service: 'thermostat',
|
|
|
|
|
cmd: 'cmd.setpoint.set',
|
|
|
|
|
val_t: 'str_map',
|
2025-07-23 22:32:23 +02:00
|
|
|
|
val: {
|
2025-07-24 16:34:58 +02:00
|
|
|
|
type:
|
|
|
|
|
haGetCachedState({ topic: `${topicPrefix}/state` })?.[svc.addr]
|
|
|
|
|
?.mode ?? defaultSpType,
|
2025-07-23 22:32:23 +02:00
|
|
|
|
temp: payload,
|
2025-07-24 16:34:58 +02:00
|
|
|
|
unit: 'C',
|
2025-07-23 22:32:23 +02:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
components: { [svc.addr]: climate },
|
|
|
|
|
commandHandlers: handlers,
|
|
|
|
|
};
|
|
|
|
|
}
|