From f01494caa84d98e178e5c1354667db9c379bba40 Mon Sep 17 00:00:00 2001 From: Adrian Jagielak Date: Sat, 26 Jul 2025 23:53:49 +0200 Subject: [PATCH] Add support for 'schedule_entry' service --- README.md | 2 +- futurehome/CHANGELOG.md | 1 + futurehome/README.md | 2 +- futurehome/src/ha/publish_device.ts | 2 + futurehome/src/services/schedule_entry.ts | 451 ++++++++++++++++++++++ 5 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 futurehome/src/services/schedule_entry.ts diff --git a/README.md b/README.md index 84e1747..2237b7d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ todo add info about factory reset hub to restore 30 day trial | Binary switch | [out_bin_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_bin_switch.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ | [Switch](https://www.home-assistant.io/integrations/switch/) | | Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Number](https://www.home-assistant.io/integrations/number/) | | Button | [scene_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/scene_ctrl.ts) | [Modusbryter](https://www.futurehome.io/en_no/shop/modeswitch-white) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Select](https://www.home-assistant.io/integrations/select/) | +| Schedule entry | schedule_entry | | ✅ | [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Binary sensor | [sensor_contact](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts), [sensor_presence](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts) | | ✅ | [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) | | Numeric sensor | [sensor_accelx](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accely](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accelz](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_anglepos](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_atmo](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_baro](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co2](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_current](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_dew](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_direct](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_distance](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_elresist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_freq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gust](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_humid](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_lumin](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_moist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_noise](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_power](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rain](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rotation](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicmag](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_solarrad](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tank](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_temp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tidelvl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_uv](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_veloc](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_voltage](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watpressure](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wattemp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_weight](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wind](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Siren | [siren_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/siren_ctrl.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Siren](https://www.home-assistant.io/integrations/siren/), [Select](https://www.home-assistant.io/integrations/siren/), [Siren](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | @@ -68,7 +69,6 @@ todo add info about factory reset hub to restore 30 day trial | Service | Description | | --- | --- | | schedule | No devices or hub support this stub service. | -| schedule_entry | | | battery_charge_ctrl | No devices or hub support this stub service. | | inverter_consumer_conn | No devices or hub support this stub service. | | inverter_grid_conn | No devices or hub support this stub service. | diff --git a/futurehome/CHANGELOG.md b/futurehome/CHANGELOG.md index de92087..c0a822c 100644 --- a/futurehome/CHANGELOG.md +++ b/futurehome/CHANGELOG.md @@ -8,6 +8,7 @@ - Added support for 'sound_switch' service (sound emitters). - Added support for 'door_lock' service (door locks). - Added support for 'user_code' service (keypads). +- Added support for 'schedule_entry' service (for scheduling access). ## 0.1.5 (25.07.2025) diff --git a/futurehome/README.md b/futurehome/README.md index c2a8adc..9e339cd 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -54,6 +54,7 @@ todo add info about factory reset hub to restore 30 day trial | Binary switch | [out_bin_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_bin_switch.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ | [Switch](https://www.home-assistant.io/integrations/switch/) | | Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Number](https://www.home-assistant.io/integrations/number/) | | Button | [scene_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/scene_ctrl.ts) | [Modusbryter](https://www.futurehome.io/en_no/shop/modeswitch-white) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Select](https://www.home-assistant.io/integrations/select/) | +| Schedule entry | schedule_entry | | ✅ | [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Binary sensor | [sensor_contact](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts), [sensor_presence](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts) | | ✅ | [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) | | Numeric sensor | [sensor_accelx](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accely](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accelz](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_anglepos](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_atmo](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_baro](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co2](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_current](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_dew](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_direct](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_distance](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_elresist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_freq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gust](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_humid](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_lumin](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_moist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_noise](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_power](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rain](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rotation](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicmag](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_solarrad](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tank](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_temp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tidelvl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_uv](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_veloc](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_voltage](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watpressure](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wattemp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_weight](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wind](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Siren | [siren_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/siren_ctrl.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Siren](https://www.home-assistant.io/integrations/siren/), [Select](https://www.home-assistant.io/integrations/siren/), [Siren](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | @@ -67,7 +68,6 @@ todo add info about factory reset hub to restore 30 day trial | Service | Description | | --- | --- | | schedule | No devices or hub support this stub service. | -| schedule_entry | | | battery_charge_ctrl | No devices or hub support this stub service. | | inverter_consumer_conn | No devices or hub support this stub service. | | inverter_grid_conn | No devices or hub support this stub service. | diff --git a/futurehome/src/ha/publish_device.ts b/futurehome/src/ha/publish_device.ts index 3df441e..9e08c5f 100644 --- a/futurehome/src/ha/publish_device.ts +++ b/futurehome/src/ha/publish_device.ts @@ -20,6 +20,7 @@ import { media_player__components } from '../services/media_player'; import { out_bin_switch__components } from '../services/out_bin_switch'; import { out_lvl_switch__components } from '../services/out_lvl_switch'; import { scene_ctrl__components } from '../services/scene_ctrl'; +import { schedule_entry__components } from '../services/schedule_entry'; import { siren_ctrl__components } from '../services/siren_ctrl'; import { sound_switch__components } from '../services/sound_switch'; import { thermostat__components } from '../services/thermostat'; @@ -163,6 +164,7 @@ const serviceHandlers: { out_bin_switch: out_bin_switch__components, out_lvl_switch: out_lvl_switch__components, scene_ctrl: scene_ctrl__components, + schedule_entry: schedule_entry__components, sensor_accelx: _sensor_numeric__components, sensor_accely: _sensor_numeric__components, sensor_accelz: _sensor_numeric__components, diff --git a/futurehome/src/services/schedule_entry.ts b/futurehome/src/services/schedule_entry.ts new file mode 100644 index 0000000..ea2134a --- /dev/null +++ b/futurehome/src/services/schedule_entry.ts @@ -0,0 +1,451 @@ +import { sendFimpMsg } from '../fimp/fimp'; +import { + VinculumPd7Device, + VinculumPd7Service, +} from '../fimp/vinculum_pd7_device'; +import { HaMqttComponent } from '../ha/mqtt_components/_component'; +import { + CommandHandlers, + ServiceComponentsCreationResult, +} from '../ha/publish_device'; + +interface ScheduleEntry { + slot: number; + user_id: number; + year_start?: number; + month_start?: number; + day_start?: number; + hour_start?: number; + minute_start?: number; + year_end?: number; + month_end?: number; + day_end?: number; + hour_end?: number; + minute_end?: number; +} + +export function schedule_entry__components( + topicPrefix: string, + device: VinculumPd7Device, + svc: VinculumPd7Service, + _svcName: string, +): ServiceComponentsCreationResult | undefined { + const components: Record = {}; + const commandHandlers: CommandHandlers = {}; + + const slots = svc.props?.slots || 1; + + // For each slot, create management controls + for (let slot = 1; slot <= slots; slot++) { + const slotPrefix = `${svc.addr}_slot_${slot}`; + + // User ID selector for this slot + const userIdCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/user_id/command`; + components[`${slotPrefix}_user_id`] = { + unique_id: `${slotPrefix}_user_id`, + platform: 'number', + name: `Schedule Slot ${slot} User ID`, + entity_category: 'config', + min: 1, + max: 254, // Typical user code range + step: 1, + mode: 'box', + command_topic: userIdCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.user_id }}{% endif %}{% endfor %}`, + icon: 'mdi:account-key', + }; + + // Year start selector + const yearStartCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/year_start/command`; + components[`${slotPrefix}_year_start`] = { + unique_id: `${slotPrefix}_year_start`, + platform: 'number', + name: `Schedule Slot ${slot} Start Year`, + entity_category: 'config', + min: 0, + max: 99, + step: 1, + mode: 'box', + command_topic: yearStartCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.year_start | default(0) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-start', + }; + + // Month start selector + const monthStartCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/month_start/command`; + components[`${slotPrefix}_month_start`] = { + unique_id: `${slotPrefix}_month_start`, + platform: 'number', + name: `Schedule Slot ${slot} Start Month`, + entity_category: 'config', + min: 1, + max: 12, + step: 1, + mode: 'box', + command_topic: monthStartCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.month_start | default(1) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-month', + }; + + // Day start selector + const dayStartCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/day_start/command`; + components[`${slotPrefix}_day_start`] = { + unique_id: `${slotPrefix}_day_start`, + platform: 'number', + name: `Schedule Slot ${slot} Start Day`, + entity_category: 'config', + min: 1, + max: 31, + step: 1, + mode: 'box', + command_topic: dayStartCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.day_start | default(1) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-today', + }; + + // Hour start selector + const hourStartCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/hour_start/command`; + components[`${slotPrefix}_hour_start`] = { + unique_id: `${slotPrefix}_hour_start`, + platform: 'number', + name: `Schedule Slot ${slot} Start Hour`, + entity_category: 'config', + min: 0, + max: 23, + step: 1, + mode: 'box', + command_topic: hourStartCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.hour_start | default(0) }}{% endif %}{% endfor %}`, + icon: 'mdi:clock-start', + }; + + // Minute start selector + const minuteStartCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/minute_start/command`; + components[`${slotPrefix}_minute_start`] = { + unique_id: `${slotPrefix}_minute_start`, + platform: 'number', + name: `Schedule Slot ${slot} Start Minute`, + entity_category: 'config', + min: 0, + max: 59, + step: 1, + mode: 'box', + command_topic: minuteStartCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.minute_start | default(0) }}{% endif %}{% endfor %}`, + icon: 'mdi:clock-start', + }; + + // Year end selector + const yearEndCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/year_end/command`; + components[`${slotPrefix}_year_end`] = { + unique_id: `${slotPrefix}_year_end`, + platform: 'number', + name: `Schedule Slot ${slot} End Year`, + entity_category: 'config', + min: 0, + max: 99, + step: 1, + mode: 'box', + command_topic: yearEndCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.year_end | default(99) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-end', + }; + + // Month end selector + const monthEndCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/month_end/command`; + components[`${slotPrefix}_month_end`] = { + unique_id: `${slotPrefix}_month_end`, + platform: 'number', + name: `Schedule Slot ${slot} End Month`, + entity_category: 'config', + min: 1, + max: 12, + step: 1, + mode: 'box', + command_topic: monthEndCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.month_end | default(12) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-month', + }; + + // Day end selector + const dayEndCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/day_end/command`; + components[`${slotPrefix}_day_end`] = { + unique_id: `${slotPrefix}_day_end`, + platform: 'number', + name: `Schedule Slot ${slot} End Day`, + entity_category: 'config', + min: 1, + max: 31, + step: 1, + mode: 'box', + command_topic: dayEndCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.day_end | default(31) }}{% endif %}{% endfor %}`, + icon: 'mdi:calendar-today', + }; + + // Hour end selector + const hourEndCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/hour_end/command`; + components[`${slotPrefix}_hour_end`] = { + unique_id: `${slotPrefix}_hour_end`, + platform: 'number', + name: `Schedule Slot ${slot} End Hour`, + entity_category: 'config', + min: 0, + max: 23, + step: 1, + mode: 'box', + command_topic: hourEndCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.hour_end | default(23) }}{% endif %}{% endfor %}`, + icon: 'mdi:clock-end', + }; + + // Minute end selector + const minuteEndCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/minute_end/command`; + components[`${slotPrefix}_minute_end`] = { + unique_id: `${slotPrefix}_minute_end`, + platform: 'number', + name: `Schedule Slot ${slot} End Minute`, + entity_category: 'config', + min: 0, + max: 59, + step: 1, + mode: 'box', + command_topic: minuteEndCommandTopic, + optimistic: false, + value_template: `{% set entries = value_json['${svc.addr}'] %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} %}{{ entry.minute_end | default(59) }}{% endif %}{% endfor %}`, + icon: 'mdi:clock-end', + }; + + // Clear schedule button for this slot + const clearCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/clear/command`; + components[`${slotPrefix}_clear`] = { + unique_id: `${slotPrefix}_clear`, + platform: 'button', + name: `Clear Schedule Slot ${slot}`, + entity_category: 'config', + command_topic: clearCommandTopic, + icon: 'mdi:delete-sweep', + }; + + // Get report button for this slot + const getReportCommandTopic = `${topicPrefix}${svc.addr}/slot_${slot}/get_report/command`; + components[`${slotPrefix}_get_report`] = { + unique_id: `${slotPrefix}_get_report`, + platform: 'button', + name: `Get Schedule Slot ${slot} Report`, + entity_category: 'diagnostic', + command_topic: getReportCommandTopic, + icon: 'mdi:file-document-outline', + }; + + // Schedule active sensor for this slot + components[`${slotPrefix}_active`] = { + unique_id: `${slotPrefix}_active`, + platform: 'binary_sensor', + name: `Schedule Slot ${slot} Active`, + entity_category: 'diagnostic', + device_class: 'connectivity', + value_template: `{% set entries = value_json['${svc.addr}'] %}{% set found = false %}{% for key, entry in entries.items() %}{% if entry.slot == ${slot} and entry.year_start is defined %}{% set found = true %}{% endif %}{% endfor %}{{ found | iif('ON', 'OFF') }}`, + icon: 'mdi:calendar-clock', + }; + + // Command handlers for this slot + + // Helper function to get current schedule entry for a slot + const getCurrentScheduleEntry = async ( + slotNum: number, + ): Promise => { + // This would need to be implemented to get the current state from the cached state + // For now, we'll construct a basic entry with required fields + return { + slot: slotNum, + user_id: 1, // Default user ID + }; + }; + + // Helper function to update a schedule entry field + const updateScheduleField = async ( + slotNum: number, + field: keyof ScheduleEntry, + value: number, + ) => { + const currentEntry = await getCurrentScheduleEntry(slotNum); + if (!currentEntry) return; + + const updatedEntry: ScheduleEntry = { + ...currentEntry, + [field]: value, + }; + + // Only send the set command if we have minimum required fields + if (updatedEntry.user_id && updatedEntry.slot) { + await sendFimpMsg({ + address: svc.addr, + service: 'schedule_entry', + cmd: 'cmd.schedule_entry.set', + val_t: 'int_map', + val: updatedEntry, + }); + } + }; + + // User ID command handler + commandHandlers[userIdCommandTopic] = async (payload: string) => { + const userId = parseInt(payload, 10); + if (Number.isNaN(userId) || userId < 1 || userId > 254) { + return; + } + await updateScheduleField(slot, 'user_id', userId); + }; + + // Year start command handler + commandHandlers[yearStartCommandTopic] = async (payload: string) => { + const year = parseInt(payload, 10); + if (Number.isNaN(year) || year < 0 || year > 99) { + return; + } + await updateScheduleField(slot, 'year_start', year); + }; + + // Month start command handler + commandHandlers[monthStartCommandTopic] = async (payload: string) => { + const month = parseInt(payload, 10); + if (Number.isNaN(month) || month < 1 || month > 12) { + return; + } + await updateScheduleField(slot, 'month_start', month); + }; + + // Day start command handler + commandHandlers[dayStartCommandTopic] = async (payload: string) => { + const day = parseInt(payload, 10); + if (Number.isNaN(day) || day < 1 || day > 31) { + return; + } + await updateScheduleField(slot, 'day_start', day); + }; + + // Hour start command handler + commandHandlers[hourStartCommandTopic] = async (payload: string) => { + const hour = parseInt(payload, 10); + if (Number.isNaN(hour) || hour < 0 || hour > 23) { + return; + } + await updateScheduleField(slot, 'hour_start', hour); + }; + + // Minute start command handler + commandHandlers[minuteStartCommandTopic] = async (payload: string) => { + const minute = parseInt(payload, 10); + if (Number.isNaN(minute) || minute < 0 || minute > 59) { + return; + } + await updateScheduleField(slot, 'minute_start', minute); + }; + + // Year end command handler + commandHandlers[yearEndCommandTopic] = async (payload: string) => { + const year = parseInt(payload, 10); + if (Number.isNaN(year) || year < 0 || year > 99) { + return; + } + await updateScheduleField(slot, 'year_end', year); + }; + + // Month end command handler + commandHandlers[monthEndCommandTopic] = async (payload: string) => { + const month = parseInt(payload, 10); + if (Number.isNaN(month) || month < 1 || month > 12) { + return; + } + await updateScheduleField(slot, 'month_end', month); + }; + + // Day end command handler + commandHandlers[dayEndCommandTopic] = async (payload: string) => { + const day = parseInt(payload, 10); + if (Number.isNaN(day) || day < 1 || day > 31) { + return; + } + await updateScheduleField(slot, 'day_end', day); + }; + + // Hour end command handler + commandHandlers[hourEndCommandTopic] = async (payload: string) => { + const hour = parseInt(payload, 10); + if (Number.isNaN(hour) || hour < 0 || hour > 23) { + return; + } + await updateScheduleField(slot, 'hour_end', hour); + }; + + // Minute end command handler + commandHandlers[minuteEndCommandTopic] = async (payload: string) => { + const minute = parseInt(payload, 10); + if (Number.isNaN(minute) || minute < 0 || minute > 59) { + return; + } + await updateScheduleField(slot, 'minute_end', minute); + }; + + // Clear command handler + commandHandlers[clearCommandTopic] = async (_payload: string) => { + const currentEntry = await getCurrentScheduleEntry(slot); + if (!currentEntry) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'schedule_entry', + cmd: 'cmd.schedule_entry.clear', + val_t: 'int_map', + val: { + slot: slot, + user_id: currentEntry.user_id, + }, + }); + }; + + // Get report command handler + commandHandlers[getReportCommandTopic] = async (_payload: string) => { + const currentEntry = await getCurrentScheduleEntry(slot); + if (!currentEntry) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'schedule_entry', + cmd: 'cmd.schedule_entry.get_report', + val_t: 'int_map', + val: { + slot: slot, + user_id: currentEntry.user_id, + }, + }); + }; + } + + // Global schedule sensor showing total active schedules + components[`${svc.addr}_total_active`] = { + unique_id: `${svc.addr}_total_active`, + platform: 'sensor', + name: 'Active Schedule Count', + entity_category: 'diagnostic', + state_class: 'measurement', + value_template: `{% set entries = value_json['${svc.addr}'] %}{% set count = 0 %}{% for key, entry in entries.items() %}{% if entry.year_start is defined %}{% set count = count + 1 %}{% endif %}{% endfor %}{{ count }}`, + icon: 'mdi:calendar-check', + }; + + return { + components, + commandHandlers, + }; +}