From ac435719a58fe96e84b5cf89034fe74bfc92c4f9 Mon Sep 17 00:00:00 2001 From: Adrian Jagielak Date: Sat, 26 Jul 2025 23:59:57 +0200 Subject: [PATCH] Add support for 'doorman' service --- README.md | 3 +- futurehome/CHANGELOG.md | 1 + futurehome/README.md | 3 +- futurehome/src/ha/publish_device.ts | 2 + futurehome/src/services/doorman.ts | 428 ++++++++++++++++++++++++++++ 5 files changed, 433 insertions(+), 4 deletions(-) create mode 100644 futurehome/src/services/doorman.ts diff --git a/README.md b/README.md index 2237b7d..6f4b67d 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,7 @@ todo add info about factory reset hub to restore 30 day trial | Chargepoint | [chargepoint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/chargepoint.ts) | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Select](https://www.home-assistant.io/integrations/select/) | | Color control | [color_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/color_ctrl.ts) | | ✅ | [Light](https://www.home-assistant.io/integrations/light/) | | Complex Alarm System | [complex_alarm_system](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/complex_alarm_system.ts) | | | -| Door lock | [door_lock](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/door_lock.ts) | | ✅ | [Lock](https://www.home-assistant.io/integrations/lock/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Select](https://www.home-assistant.io/integrations/select/) | -| ??? | [doorman](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/doorman.ts) | | | +| Door lock | [door_lock](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/door_lock.ts) | [Yale Doorman](https://www.assaabloy.com/ee/en/solutions/products/smart-locks/yale-doorman), [doorman](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/doorman.ts) | ✅ | [Lock](https://www.home-assistant.io/integrations/lock/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Select](https://www.home-assistant.io/integrations/select/), [Sensor](https://www.home-assistant.io/integrations/sensor/), [Text](https://www.home-assistant.io/integrations/text/) | | Fan | [fan_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/fan_ctrl.ts) | | ✅ | [Fan](https://www.home-assistant.io/integrations/fan/) | | Light | [light](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/light.ts) | | | | Media player | [media_player](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/media_player.ts) | | ✅ | [Select](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Switch](https://www.home-assistant.io/integrations/switch/), [Image](https://www.home-assistant.io/integrations/image/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | diff --git a/futurehome/CHANGELOG.md b/futurehome/CHANGELOG.md index c0a822c..0fd3c1e 100644 --- a/futurehome/CHANGELOG.md +++ b/futurehome/CHANGELOG.md @@ -9,6 +9,7 @@ - Added support for 'door_lock' service (door locks). - Added support for 'user_code' service (keypads). - Added support for 'schedule_entry' service (for scheduling access). +- Added support for 'doorman' service (Yale door locks). ## 0.1.5 (25.07.2025) diff --git a/futurehome/README.md b/futurehome/README.md index 9e339cd..bcfbab0 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -45,8 +45,7 @@ todo add info about factory reset hub to restore 30 day trial | Chargepoint | [chargepoint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/chargepoint.ts) | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Select](https://www.home-assistant.io/integrations/select/) | | Color control | [color_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/color_ctrl.ts) | | ✅ | [Light](https://www.home-assistant.io/integrations/light/) | | Complex Alarm System | [complex_alarm_system](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/complex_alarm_system.ts) | | | -| Door lock | [door_lock](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/door_lock.ts) | | ✅ | [Lock](https://www.home-assistant.io/integrations/lock/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Select](https://www.home-assistant.io/integrations/select/) | -| ??? | [doorman](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/doorman.ts) | | | +| Door lock | [door_lock](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/door_lock.ts) | [Yale Doorman](https://www.assaabloy.com/ee/en/solutions/products/smart-locks/yale-doorman), [doorman](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/doorman.ts) | ✅ | [Lock](https://www.home-assistant.io/integrations/lock/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Switch](https://www.home-assistant.io/integrations/switch/), [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Select](https://www.home-assistant.io/integrations/select/), [Sensor](https://www.home-assistant.io/integrations/sensor/), [Text](https://www.home-assistant.io/integrations/text/) | | Fan | [fan_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/fan_ctrl.ts) | | ✅ | [Fan](https://www.home-assistant.io/integrations/fan/) | | Light | [light](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/light.ts) | | | | Media player | [media_player](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/media_player.ts) | | ✅ | [Select](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Switch](https://www.home-assistant.io/integrations/switch/), [Image](https://www.home-assistant.io/integrations/image/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | diff --git a/futurehome/src/ha/publish_device.ts b/futurehome/src/ha/publish_device.ts index 9e08c5f..3e633ec 100644 --- a/futurehome/src/ha/publish_device.ts +++ b/futurehome/src/ha/publish_device.ts @@ -14,6 +14,7 @@ import { battery__components } from '../services/battery'; import { chargepoint__components } from '../services/chargepoint'; import { color_ctrl__components } from '../services/color_ctrl'; import { door_lock__components } from '../services/door_lock'; +import { doorman__components } from '../services/doorman'; import { fan_ctrl__components } from '../services/fan_ctrl'; import { indicator_ctrl__components } from '../services/indicator_ctrl'; import { media_player__components } from '../services/media_player'; @@ -153,6 +154,7 @@ const serviceHandlers: { chargepoint: chargepoint__components, color_ctrl: color_ctrl__components, door_lock: door_lock__components, + doorman: doorman__components, fan_ctrl: fan_ctrl__components, indicator_ctrl: indicator_ctrl__components, media_player: media_player__components, diff --git a/futurehome/src/services/doorman.ts b/futurehome/src/services/doorman.ts new file mode 100644 index 0000000..af16002 --- /dev/null +++ b/futurehome/src/services/doorman.ts @@ -0,0 +1,428 @@ +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'; + +export function doorman__components( + topicPrefix: string, + device: VinculumPd7Device, + svc: VinculumPd7Service, + _svcName: string, +): ServiceComponentsCreationResult | undefined { + const components: Record = {}; + const commandHandlers: CommandHandlers = {}; + + // Integration session sensor - shows when the lock is in integration mode + if (svc.intf?.includes('evt.doorman_session.report')) { + components[`${svc.addr}_session`] = { + unique_id: `${svc.addr}_session`, + platform: 'binary_sensor', + name: 'Integration Session', + device_class: 'running', + entity_category: 'diagnostic', + value_template: `{{ value_json['${svc.addr}'].session_active | default(false) | iif('ON', 'OFF') }}`, + }; + } + + // User management buttons and sensors + if (svc.intf?.includes('cmd.doorman_user.get_all')) { + // Button to refresh user list + const getUsersCommandTopic = `${topicPrefix}${svc.addr}/get_users/command`; + + components[`${svc.addr}_get_users`] = { + unique_id: `${svc.addr}_get_users`, + platform: 'button', + name: 'Refresh Users', + entity_category: 'config', + command_topic: getUsersCommandTopic, + }; + + commandHandlers[getUsersCommandTopic] = async (_payload: string) => { + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_user.get_all', + val_t: 'null', + val: null, + }); + }; + } + + // User count sensor + components[`${svc.addr}_user_count`] = { + unique_id: `${svc.addr}_user_count`, + platform: 'sensor', + name: 'User Count', + entity_category: 'diagnostic', + icon: 'mdi:account-multiple', + value_template: `{{ value_json['${svc.addr}'].users.slots | length if value_json['${svc.addr}'].users.slots is defined else 0 }}`, + }; + + // Parameter management + if (svc.intf?.includes('cmd.doorman_param.get_report')) { + // Button to refresh parameters + const getParamsCommandTopic = `${topicPrefix}${svc.addr}/get_params/command`; + + components[`${svc.addr}_get_params`] = { + unique_id: `${svc.addr}_get_params`, + platform: 'button', + name: 'Refresh Parameters', + entity_category: 'config', + command_topic: getParamsCommandTopic, + }; + + commandHandlers[getParamsCommandTopic] = async (_payload: string) => { + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.get_report', + val_t: 'null', + val: null, + }); + }; + } + + // Configuration parameters as select entities + if (svc.intf?.includes('cmd.doorman_param.set')) { + // Silent mode (Parameter ID 1) + const silentModeCommandTopic = `${topicPrefix}${svc.addr}/silent_mode/command`; + + components[`${svc.addr}_silent_mode`] = { + unique_id: `${svc.addr}_silent_mode`, + platform: 'select', + name: 'Silent Mode', + entity_category: 'config', + options: ['Silent', 'Volume 1 (Low)', 'Volume 2 (High)'], + command_topic: silentModeCommandTopic, + value_template: `{% set val = value_json['${svc.addr}'].params['1'] | default('3') %}{{ {'1': 'Silent', '2': 'Volume 1 (Low)', '3': 'Volume 2 (High)'}.get(val, 'Volume 2 (High)') }}`, + }; + + commandHandlers[silentModeCommandTopic] = async (payload: string) => { + const valueMap: Record = { + Silent: '1', + 'Volume 1 (Low)': '2', + 'Volume 2 (High)': '3', + }; + + const value = valueMap[payload]; + if (!value) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '1', + value: value, + }, + }); + }; + + // Auto Relock (Parameter ID 2) + const autoRelockCommandTopic = `${topicPrefix}${svc.addr}/auto_relock/command`; + + components[`${svc.addr}_auto_relock`] = { + unique_id: `${svc.addr}_auto_relock`, + platform: 'switch', + name: 'Auto Relock', + entity_category: 'config', + command_topic: autoRelockCommandTopic, + value_template: `{{ (value_json['${svc.addr}'].params['2'] | default('0') == '255') | iif('ON', 'OFF') }}`, + }; + + commandHandlers[autoRelockCommandTopic] = async (payload: string) => { + const value = payload === 'ON' ? '255' : '0'; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '2', + value: value, + }, + }); + }; + + // Language (Parameter ID 5) + const languageCommandTopic = `${topicPrefix}${svc.addr}/language/command`; + + components[`${svc.addr}_language`] = { + unique_id: `${svc.addr}_language`, + platform: 'select', + name: 'Language', + entity_category: 'config', + options: ['English', 'Danish', 'Norwegian', 'Swedish'], + command_topic: languageCommandTopic, + value_template: `{% set val = value_json['${svc.addr}'].params['5'] | default('1') %}{{ {'1': 'English', '4': 'Danish', '5': 'Norwegian', '6': 'Swedish'}.get(val, 'English') }}`, + }; + + commandHandlers[languageCommandTopic] = async (payload: string) => { + const valueMap: Record = { + English: '1', + Danish: '4', + Norwegian: '5', + Swedish: '6', + }; + + const value = valueMap[payload]; + if (!value) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '5', + value: value, + }, + }); + }; + + // Home/Away Alarm Mode (Parameter ID 17) + const alarmModeCommandTopic = `${topicPrefix}${svc.addr}/alarm_mode/command`; + + components[`${svc.addr}_alarm_mode`] = { + unique_id: `${svc.addr}_alarm_mode`, + platform: 'select', + name: 'Alarm Mode', + entity_category: 'config', + options: ['Off', 'Home Alarm Mode', 'Away Alarm Mode'], + command_topic: alarmModeCommandTopic, + value_template: `{% set val = value_json['${svc.addr}'].params['17'] | default('0') %}{{ {'0': 'Off', '1': 'Home Alarm Mode', '2': 'Away Alarm Mode'}.get(val, 'Off') }}`, + }; + + commandHandlers[alarmModeCommandTopic] = async (payload: string) => { + const valueMap: Record = { + Off: '0', + 'Home Alarm Mode': '1', + 'Away Alarm Mode': '2', + }; + + const value = valueMap[payload]; + if (!value) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '17', + value: value, + }, + }); + }; + + // Part of Alarm System (Parameter ID 18) + const alarmSystemCommandTopic = `${topicPrefix}${svc.addr}/alarm_system/command`; + + components[`${svc.addr}_alarm_system`] = { + unique_id: `${svc.addr}_alarm_system`, + platform: 'switch', + name: 'Part of Alarm System', + entity_category: 'config', + command_topic: alarmSystemCommandTopic, + value_template: `{{ (value_json['${svc.addr}'].params['18'] | default('1') == '1') | iif('ON', 'OFF') }}`, + }; + + commandHandlers[alarmSystemCommandTopic] = async (payload: string) => { + const value = payload === 'ON' ? '1' : '0'; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '18', + value: value, + }, + }); + }; + + // User Code Blocking (Parameter ID 19) + const codeBlockingCommandTopic = `${topicPrefix}${svc.addr}/code_blocking/command`; + + components[`${svc.addr}_code_blocking`] = { + unique_id: `${svc.addr}_code_blocking`, + platform: 'switch', + name: 'User Code Blocking', + entity_category: 'config', + command_topic: codeBlockingCommandTopic, + value_template: `{{ (value_json['${svc.addr}'].params['19'] | default('0') == '1') | iif('ON', 'OFF') }}`, + }; + + commandHandlers[codeBlockingCommandTopic] = async (payload: string) => { + const value = payload === 'ON' ? '1' : '0'; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '19', + value: value, + }, + }); + }; + + // System Arm Hold Time (Parameter ID 16) - Number input in milliseconds + const armHoldTimeCommandTopic = `${topicPrefix}${svc.addr}/arm_hold_time/command`; + + components[`${svc.addr}_arm_hold_time`] = { + unique_id: `${svc.addr}_arm_hold_time`, + platform: 'number', + name: 'System Arm Hold Time', + entity_category: 'config', + unit_of_measurement: 'ms', + min: 1000, + max: 20000, + step: 100, + command_topic: armHoldTimeCommandTopic, + value_template: `{{ value_json['${svc.addr}'].params['16'] | default(3000) | int }}`, + }; + + commandHandlers[armHoldTimeCommandTopic] = async (payload: string) => { + const value = parseInt(payload, 10); + if (isNaN(value) || value < 1000 || value > 20000) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_param.set', + val_t: 'str_map', + val: { + parameter_id: '16', + value: value.toString(), + }, + }); + }; + } + + // Activity sensor - shows last activity + if (svc.intf?.includes('evt.doorman_activity.report')) { + components[`${svc.addr}_last_activity`] = { + unique_id: `${svc.addr}_last_activity`, + platform: 'sensor', + name: 'Last Activity', + entity_category: 'diagnostic', + icon: 'mdi:history', + value_template: `{% set activity = value_json['${svc.addr}'].activity %}{% if activity %}{{ activity.event_type | default('unknown') }}{% else %}unknown{% endif %}`, + }; + + // Activity details as attributes sensor + components[`${svc.addr}_activity_details`] = { + unique_id: `${svc.addr}_activity_details`, + platform: 'sensor', + name: 'Activity Details', + entity_category: 'diagnostic', + icon: 'mdi:information', + value_template: `{% set activity = value_json['${svc.addr}'].activity %}{% if activity %}{{ activity | tojson }}{% else %}{}{% endif %}`, + }; + } + + // User management functions - these would typically be handled through automations or scripts + // But we can expose basic clear user functionality + if (svc.intf?.includes('cmd.doorman_user.clear')) { + // We'll create a text input for slot number to clear + const clearUserCommandTopic = `${topicPrefix}${svc.addr}/clear_user/command`; + + components[`${svc.addr}_clear_user_slot`] = { + unique_id: `${svc.addr}_clear_user_slot`, + platform: 'text', + name: 'Clear User Slot', + entity_category: 'config', + command_topic: clearUserCommandTopic, + min: 1, + max: 2, + pattern: '^[0-9]{1,2}$', + }; + + commandHandlers[clearUserCommandTopic] = async (payload: string) => { + const slotNumber = parseInt(payload, 10); + if (isNaN(slotNumber) || slotNumber < 0 || slotNumber > 20) return; + + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman_user.clear', + val_t: 'str_map', + val: { + slot_number: slotNumber.toString(), + }, + }); + }; + } + + // Integration command support + if (svc.intf?.includes('cmd.doorman.integration')) { + // This is typically handled automatically by the hub, but we can expose manual trigger + const integrationCommandTopic = `${topicPrefix}${svc.addr}/start_integration/command`; + + components[`${svc.addr}_start_integration`] = { + unique_id: `${svc.addr}_start_integration`, + platform: 'button', + name: 'Start Integration', + entity_category: 'config', + device_class: 'restart', + command_topic: integrationCommandTopic, + }; + + commandHandlers[integrationCommandTopic] = async (_payload: string) => { + // Start integration with default PIN code (this should be customized) + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman.integration', + val_t: 'str_map', + val: { + slot_number: '0', + code_type: 'pin', + code: '123456', // Default code - should be configurable + }, + }); + }; + } + + // Arm confirmation support + if (svc.intf?.includes('cmd.doorman.arm_confirm')) { + const armConfirmCommandTopic = `${topicPrefix}${svc.addr}/arm_confirm/command`; + + components[`${svc.addr}_arm_confirm`] = { + unique_id: `${svc.addr}_arm_confirm`, + platform: 'button', + name: 'Arm Confirm', + entity_category: 'config', + command_topic: armConfirmCommandTopic, + }; + + commandHandlers[armConfirmCommandTopic] = async (_payload: string) => { + await sendFimpMsg({ + address: svc.addr, + service: 'doorman', + cmd: 'cmd.doorman.arm_confirm', + val_t: 'str_map', + val: { + sequence_number: '0', + operating_parameter: '0', + }, + }); + }; + } + + return { + components, + commandHandlers, + }; +}