mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 15:47:08 +00:00
Add support for 'color_ctrl' service
This commit is contained in:
parent
de14075234
commit
34c8bd6624
@ -56,7 +56,7 @@ todo links to the .ts service implementations below
|
|||||||
| blinds | | |
|
| blinds | | |
|
||||||
| boiler | | |
|
| boiler | | |
|
||||||
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | |
|
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | |
|
||||||
| color_ctrl | | |
|
| color_ctrl | | ✅ |
|
||||||
| complex_alarm_system | | |
|
| complex_alarm_system | | |
|
||||||
| door_lock | | |
|
| door_lock | | |
|
||||||
| doorman | | |
|
| doorman | | |
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->
|
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0 (release notes from the future!)
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
## 0.0.3
|
|
||||||
|
|
||||||
- Added initial version of the add-on code.
|
|
||||||
|
@ -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.24"
|
version: "0.0.25"
|
||||||
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"
|
||||||
|
@ -3,6 +3,7 @@ import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_devi
|
|||||||
import { log } from "../logger";
|
import { log } from "../logger";
|
||||||
import { basic__components } from "../services/basic";
|
import { basic__components } from "../services/basic";
|
||||||
import { battery__components } from "../services/battery";
|
import { battery__components } from "../services/battery";
|
||||||
|
import { color_ctrl__components } from "../services/color_ctrl";
|
||||||
import { fan_ctrl__components } from "../services/fan_ctrl";
|
import { fan_ctrl__components } from "../services/fan_ctrl";
|
||||||
import { out_bin_switch__components } from "../services/out_bin_switch";
|
import { out_bin_switch__components } from "../services/out_bin_switch";
|
||||||
import { out_lvl_switch__components } from "../services/out_lvl_switch";
|
import { out_lvl_switch__components } from "../services/out_lvl_switch";
|
||||||
@ -82,7 +83,7 @@ type HaDeviceConfig = {
|
|||||||
qos: number,
|
qos: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HaComponent = SensorComponent | BinarySensorComponent | SwitchComponent | NumberComponent | ClimateComponent | SelectComponent | FanComponent;
|
export type HaComponent = SensorComponent | BinarySensorComponent | SwitchComponent | NumberComponent | ClimateComponent | SelectComponent | FanComponent | LightComponent;
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
@ -173,7 +174,28 @@ export type FanComponent = {
|
|||||||
preset_mode_state_template: string;
|
preset_mode_state_template: string;
|
||||||
state_value_template: string;
|
state_value_template: string;
|
||||||
preset_mode_value_template: string;
|
preset_mode_value_template: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/// https://www.home-assistant.io/integrations/light.mqtt/
|
||||||
|
export type LightComponent = {
|
||||||
|
unique_id: string;
|
||||||
|
// platform
|
||||||
|
p: "light";
|
||||||
|
command_topic?: string;
|
||||||
|
state_topic?: string;
|
||||||
|
state_value_template?: string;
|
||||||
|
rgb_command_topic?: string;
|
||||||
|
rgb_state_topic?: string;
|
||||||
|
rgb_value_template?: string;
|
||||||
|
brightness_state_topic?: string;
|
||||||
|
brightness_value_template?: string;
|
||||||
|
optimistic?: boolean;
|
||||||
|
color_temp_command_topic?: string;
|
||||||
|
color_temp_state_topic?: string;
|
||||||
|
color_temp_value_template?: string;
|
||||||
|
min_mireds?: number;
|
||||||
|
max_mireds?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ServiceComponentsCreationResult = {
|
export type ServiceComponentsCreationResult = {
|
||||||
components: { [key: string]: HaComponent };
|
components: { [key: string]: HaComponent };
|
||||||
@ -187,6 +209,7 @@ const serviceHandlers: {
|
|||||||
} = {
|
} = {
|
||||||
basic: basic__components,
|
basic: basic__components,
|
||||||
battery: battery__components,
|
battery: battery__components,
|
||||||
|
color_ctrl: color_ctrl__components,
|
||||||
fan_ctrl: fan_ctrl__components,
|
fan_ctrl: fan_ctrl__components,
|
||||||
out_bin_switch: out_bin_switch__components,
|
out_bin_switch: out_bin_switch__components,
|
||||||
out_lvl_switch: out_lvl_switch__components,
|
out_lvl_switch: out_lvl_switch__components,
|
||||||
|
206
futurehome/src/services/color_ctrl.ts
Normal file
206
futurehome/src/services/color_ctrl.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { sendFimpMsg } from "../fimp/fimp";
|
||||||
|
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
|
||||||
|
import { ServiceComponentsCreationResult, CommandHandlers, LightComponent } from "../ha/publish_device";
|
||||||
|
|
||||||
|
export function color_ctrl__components(
|
||||||
|
topicPrefix: string,
|
||||||
|
device: VinculumPd7Device,
|
||||||
|
svc: VinculumPd7Service
|
||||||
|
): ServiceComponentsCreationResult | undefined {
|
||||||
|
const supComponents: string[] = svc.props?.sup_components ?? [];
|
||||||
|
|
||||||
|
if (!supComponents.length) {
|
||||||
|
return undefined; // No supported components, nothing to expose
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have RGB support (minimum requirement for a useful light)
|
||||||
|
const hasRgb = supComponents.includes('red') && supComponents.includes('green') && supComponents.includes('blue');
|
||||||
|
if (!hasRgb) {
|
||||||
|
return undefined; // No RGB support, skip this service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine supported color modes based on available components
|
||||||
|
const supportedColorModes: string[] = [];
|
||||||
|
|
||||||
|
if (hasRgb) {
|
||||||
|
supportedColorModes.push('rgb');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for color temperature support (Zigbee style with 'temp' component)
|
||||||
|
if (supComponents.includes('temp')) {
|
||||||
|
supportedColorModes.push('color_temp');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dual-white support (Z-Wave style with warm_w and cold_w)
|
||||||
|
if (supComponents.includes('warm_w') && supComponents.includes('cold_w')) {
|
||||||
|
supportedColorModes.push('color_temp');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command topics
|
||||||
|
const commandTopic = `${topicPrefix}${svc.addr}/command`;
|
||||||
|
const rgbCommandTopic = `${topicPrefix}${svc.addr}/rgb/command`;
|
||||||
|
const colorTempCommandTopic = `${topicPrefix}${svc.addr}/color_temp/command`;
|
||||||
|
|
||||||
|
// State topic (shared with other components of the same device)
|
||||||
|
const stateTopic = `${topicPrefix}/state`;
|
||||||
|
|
||||||
|
// Create the light component configuration
|
||||||
|
const lightComponent: LightComponent = {
|
||||||
|
unique_id: svc.addr,
|
||||||
|
p: 'light',
|
||||||
|
|
||||||
|
// Basic on/off control
|
||||||
|
command_topic: commandTopic,
|
||||||
|
state_topic: stateTopic,
|
||||||
|
state_value_template: `{{ 'ON' if (value_json['${svc.addr}'].red > 0 or value_json['${svc.addr}'].green > 0 or value_json['${svc.addr}'].blue > 0) else 'OFF' }}`,
|
||||||
|
|
||||||
|
// RGB color control
|
||||||
|
rgb_command_topic: rgbCommandTopic,
|
||||||
|
rgb_state_topic: stateTopic,
|
||||||
|
rgb_value_template: `{{ value_json['${svc.addr}'].red }},{{ value_json['${svc.addr}'].green }},{{ value_json['${svc.addr}'].blue }}`,
|
||||||
|
|
||||||
|
// Brightness support (derived from RGB values)
|
||||||
|
brightness_state_topic: stateTopic,
|
||||||
|
brightness_value_template: `{{ [value_json['${svc.addr}'].red, value_json['${svc.addr}'].green, value_json['${svc.addr}'].blue] | max }}`,
|
||||||
|
|
||||||
|
optimistic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add color temperature support if available
|
||||||
|
if (supportedColorModes.includes('color_temp')) {
|
||||||
|
if (supComponents.includes('temp')) {
|
||||||
|
// Zigbee style - direct temperature value in Kelvin
|
||||||
|
lightComponent.color_temp_command_topic = colorTempCommandTopic;
|
||||||
|
lightComponent.color_temp_state_topic = stateTopic;
|
||||||
|
lightComponent.color_temp_value_template = `{{ (1000000 / value_json['${svc.addr}'].temp) | round(0) }}`; // Convert Kelvin to mireds
|
||||||
|
lightComponent.min_mireds = 153; // ~6500K
|
||||||
|
lightComponent.max_mireds = 370; // ~2700K
|
||||||
|
} else if (supComponents.includes('warm_w') && supComponents.includes('cold_w')) {
|
||||||
|
// Z-Wave style - warm/cold white mix
|
||||||
|
lightComponent.color_temp_command_topic = colorTempCommandTopic;
|
||||||
|
lightComponent.color_temp_state_topic = stateTopic;
|
||||||
|
// Estimate color temperature from warm_w/cold_w ratio
|
||||||
|
lightComponent.color_temp_value_template = `{{ (153 + (217 * (value_json['${svc.addr}'].warm_w / (value_json['${svc.addr}'].warm_w + value_json['${svc.addr}'].cold_w + 0.001)))) | round(0) }}`;
|
||||||
|
lightComponent.min_mireds = 153; // ~6500K (cold)
|
||||||
|
lightComponent.max_mireds = 370; // ~2700K (warm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command handlers
|
||||||
|
const commandHandlers: CommandHandlers = {
|
||||||
|
// Basic on/off command
|
||||||
|
[commandTopic]: async (payload: string) => {
|
||||||
|
if (payload === 'ON') {
|
||||||
|
// Turn on with white color (all components at max)
|
||||||
|
const colorMap: Record<string, number> = {
|
||||||
|
red: 255,
|
||||||
|
green: 255,
|
||||||
|
blue: 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "color_ctrl",
|
||||||
|
cmd: "cmd.color.set",
|
||||||
|
val_t: "int_map",
|
||||||
|
val: colorMap,
|
||||||
|
});
|
||||||
|
} else if (payload === 'OFF') {
|
||||||
|
// Turn off (all components to 0)
|
||||||
|
const colorMap: Record<string, number> = {};
|
||||||
|
supComponents.forEach(component => {
|
||||||
|
colorMap[component] = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "color_ctrl",
|
||||||
|
cmd: "cmd.color.set",
|
||||||
|
val_t: "int_map",
|
||||||
|
val: colorMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// RGB color command
|
||||||
|
[rgbCommandTopic]: async (payload: string) => {
|
||||||
|
const parts = payload.split(',');
|
||||||
|
if (parts.length !== 3) return;
|
||||||
|
|
||||||
|
const red = parseInt(parts[0], 10);
|
||||||
|
const green = parseInt(parts[1], 10);
|
||||||
|
const blue = parseInt(parts[2], 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue)) return;
|
||||||
|
if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) return;
|
||||||
|
|
||||||
|
const colorMap: Record<string, number> = {
|
||||||
|
red,
|
||||||
|
green,
|
||||||
|
blue,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "color_ctrl",
|
||||||
|
cmd: "cmd.color.set",
|
||||||
|
val_t: "int_map",
|
||||||
|
val: colorMap,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add color temperature command handler if supported
|
||||||
|
if (supportedColorModes.includes('color_temp')) {
|
||||||
|
commandHandlers[colorTempCommandTopic] = async (payload: string) => {
|
||||||
|
const mireds = parseInt(payload, 10);
|
||||||
|
if (Number.isNaN(mireds) || mireds < 153 || mireds > 370) return;
|
||||||
|
|
||||||
|
if (supComponents.includes('temp')) {
|
||||||
|
// Zigbee style - convert mireds to Kelvin
|
||||||
|
const kelvin = Math.round(1000000 / mireds);
|
||||||
|
|
||||||
|
const colorMap: Record<string, number> = {
|
||||||
|
temp: kelvin,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "color_ctrl",
|
||||||
|
cmd: "cmd.color.set",
|
||||||
|
val_t: "int_map",
|
||||||
|
val: colorMap,
|
||||||
|
});
|
||||||
|
} else if (supComponents.includes('warm_w') && supComponents.includes('cold_w')) {
|
||||||
|
// Z-Wave style - convert mireds to warm/cold white mix
|
||||||
|
// Linear interpolation between cold (153 mireds) and warm (370 mireds)
|
||||||
|
const warmRatio = (mireds - 153) / (370 - 153);
|
||||||
|
const coldRatio = 1 - warmRatio;
|
||||||
|
|
||||||
|
const colorMap: Record<string, number> = {
|
||||||
|
warm_w: Math.round(warmRatio * 255),
|
||||||
|
cold_w: Math.round(coldRatio * 255),
|
||||||
|
// Turn off RGB when using white
|
||||||
|
red: 0,
|
||||||
|
green: 0,
|
||||||
|
blue: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr!,
|
||||||
|
service: "color_ctrl",
|
||||||
|
cmd: "cmd.color.set",
|
||||||
|
val_t: "int_map",
|
||||||
|
val: colorMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
[svc.addr]: lightComponent,
|
||||||
|
},
|
||||||
|
commandHandlers,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user