mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 15:47:08 +00:00
Add support for 'thermostat' service
This commit is contained in:
parent
8af915cebc
commit
5ab64128c6
@ -127,7 +127,7 @@ todo periodical refresh of state
|
|||||||
| sensor_wind | | ✅ |
|
| sensor_wind | | ✅ |
|
||||||
| siren | | |
|
| siren | | |
|
||||||
| siren_ctrl | | |
|
| siren_ctrl | | |
|
||||||
| thermostat | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | |
|
| thermostat | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | ✅ |
|
||||||
| user_code | | |
|
| user_code | | |
|
||||||
| virtual_meter_elec | | |
|
| virtual_meter_elec | | |
|
||||||
| water_heater | | |
|
| water_heater | | |
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
||||||
name: Futurehome
|
name: Futurehome
|
||||||
version: "0.0.18"
|
version: "0.0.19"
|
||||||
slug: futurehome
|
slug: futurehome
|
||||||
description: Local Futurehome Smarthub integration
|
description: Local Futurehome Smarthub integration
|
||||||
url: "https://github.com/adrianjagielak/home-assistant-futurehome"
|
url: "https://github.com/adrianjagielak/home-assistant-futurehome"
|
||||||
|
@ -24,7 +24,7 @@ function makeClient(url: string, port: number, username: string, password: strin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type RetainedMessage = { topic: string; message: string };
|
export type RetainedMessage = { topic: string; message: string };
|
||||||
|
|
||||||
async function waitForHARetainedMessages(
|
async function waitForHARetainedMessages(
|
||||||
client: IMqttClient,
|
client: IMqttClient,
|
||||||
|
@ -44,6 +44,7 @@ import { sensor_watflow__components } from "../services/sensor_watflow";
|
|||||||
import { sensor_watpressure__components } from "../services/sensor_watpressure";
|
import { sensor_watpressure__components } from "../services/sensor_watpressure";
|
||||||
import { sensor_wattemp__components } from "../services/sensor_wattemp";
|
import { sensor_wattemp__components } from "../services/sensor_wattemp";
|
||||||
import { sensor_weight__components } from "../services/sensor_weight";
|
import { sensor_weight__components } from "../services/sensor_weight";
|
||||||
|
import { thermostat__components } from "../services/thermostat";
|
||||||
import { ha } from "./globals";
|
import { ha } from "./globals";
|
||||||
|
|
||||||
type HaDeviceConfig = {
|
type HaDeviceConfig = {
|
||||||
@ -78,13 +79,13 @@ type HaDeviceConfig = {
|
|||||||
qos: number,
|
qos: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HaComponent = SensorComponent | BinarySensorComponent | SwitchComponent | NumberComponent;
|
export type HaComponent = SensorComponent | BinarySensorComponent | SwitchComponent | NumberComponent | ClimateComponent;
|
||||||
|
|
||||||
// Device class supported values: https://www.home-assistant.io/integrations/homeassistant/#device-class
|
// Device class supported values: https://www.home-assistant.io/integrations/homeassistant/#device-class
|
||||||
|
|
||||||
/// https://www.home-assistant.io/integrations/sensor.mqtt/
|
/// https://www.home-assistant.io/integrations/sensor.mqtt/
|
||||||
/// https://www.home-assistant.io/integrations/sensor/#device-class
|
/// https://www.home-assistant.io/integrations/sensor/#device-class
|
||||||
type SensorComponent = {
|
export type SensorComponent = {
|
||||||
unique_id: string;
|
unique_id: string;
|
||||||
// platform
|
// platform
|
||||||
p: 'sensor';
|
p: 'sensor';
|
||||||
@ -95,7 +96,7 @@ type SensorComponent = {
|
|||||||
|
|
||||||
/// https://www.home-assistant.io/integrations/binary_sensor.mqtt/
|
/// https://www.home-assistant.io/integrations/binary_sensor.mqtt/
|
||||||
/// https://www.home-assistant.io/integrations/binary_sensor/#device-class
|
/// https://www.home-assistant.io/integrations/binary_sensor/#device-class
|
||||||
type BinarySensorComponent = {
|
export type BinarySensorComponent = {
|
||||||
unique_id: string;
|
unique_id: string;
|
||||||
// platform
|
// platform
|
||||||
p: 'binary_sensor';
|
p: 'binary_sensor';
|
||||||
@ -105,7 +106,7 @@ type BinarySensorComponent = {
|
|||||||
|
|
||||||
/// https://www.home-assistant.io/integrations/switch.mqtt/
|
/// https://www.home-assistant.io/integrations/switch.mqtt/
|
||||||
/// https://www.home-assistant.io/integrations/switch/#device-class
|
/// https://www.home-assistant.io/integrations/switch/#device-class
|
||||||
type SwitchComponent = {
|
export type SwitchComponent = {
|
||||||
unique_id: string;
|
unique_id: string;
|
||||||
// platform
|
// platform
|
||||||
p: 'switch';
|
p: 'switch';
|
||||||
@ -116,7 +117,7 @@ type SwitchComponent = {
|
|||||||
|
|
||||||
/// https://www.home-assistant.io/integrations/number.mqtt/
|
/// https://www.home-assistant.io/integrations/number.mqtt/
|
||||||
/// https://www.home-assistant.io/integrations/number/#device-class
|
/// https://www.home-assistant.io/integrations/number/#device-class
|
||||||
type NumberComponent = {
|
export type NumberComponent = {
|
||||||
unique_id: string;
|
unique_id: string;
|
||||||
// platform
|
// platform
|
||||||
p: 'number';
|
p: 'number';
|
||||||
@ -128,6 +129,24 @@ type NumberComponent = {
|
|||||||
value_template: string;
|
value_template: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://www.home-assistant.io/integrations/climate.mqtt/
|
||||||
|
export type ClimateComponent = {
|
||||||
|
unique_id: string;
|
||||||
|
// platform
|
||||||
|
p: 'climate';
|
||||||
|
modes: string[];
|
||||||
|
mode_command_topic: string;
|
||||||
|
mode_state_topic: string;
|
||||||
|
mode_state_template: string;
|
||||||
|
temperature_command_topic: string;
|
||||||
|
temperature_state_topic: string;
|
||||||
|
temperature_state_template: string;
|
||||||
|
min_temp: number;
|
||||||
|
max_temp: number;
|
||||||
|
temp_step: number;
|
||||||
|
optimistic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type ServiceComponentsCreationResult = {
|
export type ServiceComponentsCreationResult = {
|
||||||
components: { [key: string]: HaComponent };
|
components: { [key: string]: HaComponent };
|
||||||
commandHandlers?: CommandHandlers;
|
commandHandlers?: CommandHandlers;
|
||||||
@ -181,6 +200,7 @@ const serviceHandlers: {
|
|||||||
sensor_watpressure: sensor_watpressure__components,
|
sensor_watpressure: sensor_watpressure__components,
|
||||||
sensor_wattemp: sensor_wattemp__components,
|
sensor_wattemp: sensor_wattemp__components,
|
||||||
sensor_weight: sensor_weight__components,
|
sensor_weight: sensor_weight__components,
|
||||||
|
thermostat: thermostat__components,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function haPublishDevice(parameters: { hubId: string, vinculumDeviceData: VinculumPd7Device, deviceInclusionReport: InclusionReport | undefined }): { commandHandlers: CommandHandlers } {
|
export function haPublishDevice(parameters: { hubId: string, vinculumDeviceData: VinculumPd7Device, deviceInclusionReport: InclusionReport | undefined }): { commandHandlers: CommandHandlers } {
|
||||||
|
@ -142,3 +142,7 @@ export function haUpdateStateSensorReport(parameters: { topic: string; value: an
|
|||||||
haStateCache[stateTopic] = payload;
|
haStateCache[stateTopic] = payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function haGetCachedState(parameters: { topic: string }) {
|
||||||
|
return haStateCache[parameters.topic];
|
||||||
|
}
|
||||||
|
114
futurehome/src/services/thermostat.ts
Normal file
114
futurehome/src/services/thermostat.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 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
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { sendFimpMsg } from "../fimp/fimp";
|
||||||
|
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
|
||||||
|
import {
|
||||||
|
ClimateComponent,
|
||||||
|
CommandHandlers,
|
||||||
|
ServiceComponentsCreationResult,
|
||||||
|
} from "../ha/publish_device";
|
||||||
|
import { haGetCachedState } from "../ha/update_state";
|
||||||
|
|
||||||
|
export function thermostat__components(
|
||||||
|
topicPrefix: string,
|
||||||
|
_device: VinculumPd7Device,
|
||||||
|
svc: VinculumPd7Service
|
||||||
|
): 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
|
||||||
|
|
||||||
|
const defaultSpType = supSetpoints[0] ?? "heat";
|
||||||
|
|
||||||
|
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,
|
||||||
|
p: "climate",
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
optimistic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ───────────── command handlers ─────────────
|
||||||
|
const handlers: CommandHandlers = {
|
||||||
|
[modeCmdTopic]: async (payload: string) => {
|
||||||
|
if (!supModes.includes(payload)) return;
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "thermostat",
|
||||||
|
cmd: "cmd.mode.set",
|
||||||
|
val_t: "string",
|
||||||
|
val: payload,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[tempCmdTopic]: async (payload: string) => {
|
||||||
|
const t = parseFloat(payload);
|
||||||
|
if (Number.isNaN(t)) return;
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "thermostat",
|
||||||
|
cmd: "cmd.setpoint.set",
|
||||||
|
val_t: "str_map",
|
||||||
|
val: {
|
||||||
|
type: haGetCachedState({ topic: `${topicPrefix}/state` })?.[svc.addr]?.mode ?? defaultSpType,
|
||||||
|
temp: payload,
|
||||||
|
unit: "C",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
components: { [svc.addr]: climate },
|
||||||
|
commandHandlers: handlers,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user