Add support for 'color_ctrl' service

This commit is contained in:
Adrian Jagielak 2025-07-24 01:59:56 +02:00
parent de14075234
commit 34c8bd6624
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
5 changed files with 234 additions and 9 deletions

View File

@ -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 | | |

View File

@ -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.

View File

@ -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"

View File

@ -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,

View 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,
};
}