mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 07:37:09 +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 | | |
|
||||
| boiler | | |
|
||||
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | |
|
||||
| color_ctrl | | |
|
||||
| color_ctrl | | ✅ |
|
||||
| complex_alarm_system | | |
|
||||
| door_lock | | |
|
||||
| doorman | | |
|
||||
|
@ -1,9 +1,5 @@
|
||||
<!-- 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
|
||||
|
||||
## 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
|
||||
name: Futurehome
|
||||
version: "0.0.24"
|
||||
version: "0.0.25"
|
||||
slug: futurehome
|
||||
description: Local Futurehome Smarthub integration
|
||||
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 { basic__components } from "../services/basic";
|
||||
import { battery__components } from "../services/battery";
|
||||
import { color_ctrl__components } from "../services/color_ctrl";
|
||||
import { fan_ctrl__components } from "../services/fan_ctrl";
|
||||
import { out_bin_switch__components } from "../services/out_bin_switch";
|
||||
import { out_lvl_switch__components } from "../services/out_lvl_switch";
|
||||
@ -82,7 +83,7 @@ type HaDeviceConfig = {
|
||||
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
|
||||
|
||||
@ -173,7 +174,28 @@ export type FanComponent = {
|
||||
preset_mode_state_template: string;
|
||||
state_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 = {
|
||||
components: { [key: string]: HaComponent };
|
||||
@ -187,6 +209,7 @@ const serviceHandlers: {
|
||||
} = {
|
||||
basic: basic__components,
|
||||
battery: battery__components,
|
||||
color_ctrl: color_ctrl__components,
|
||||
fan_ctrl: fan_ctrl__components,
|
||||
out_bin_switch: out_bin_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