mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 07:37:09 +00:00
Add support for 'user_code' service
This commit is contained in:
parent
77096d6732
commit
5264f84fbf
@ -60,7 +60,7 @@ todo add info about factory reset hub to restore 30 day trial
|
||||
| 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/) |
|
||||
| Sound emitter | sound_switch | | ✅ | [Siren](https://www.home-assistant.io/integrations/siren/) |
|
||||
| Thermostat | [thermostat](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/thermostat.ts) | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | ✅ | [Climate](https://www.home-assistant.io/integrations/climate/) |
|
||||
| ??? | [user_code](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/user_code.ts) | | | |
|
||||
| Keypad | [user_code](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/user_code.ts) | | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Button](https://www.home-assistant.io/integrations/button/), [Text](https://www.home-assistant.io/integrations/text/)|
|
||||
| Water heater | [water_heater](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/water_heater.ts) | | ✅ | [Water heater](https://www.home-assistant.io/integrations/water_heater/) |
|
||||
|
||||
## Problematic services
|
||||
|
@ -7,6 +7,7 @@
|
||||
- Added support for 'meter_*' services (electricity meters, gas meters, water meters, heating meters, cooling meters).
|
||||
- Added support for 'sound_switch' service (sound emitters).
|
||||
- Added support for 'door_lock' service (door locks).
|
||||
- Added support for 'user_code' service (keypads).
|
||||
|
||||
## 0.1.5 (25.07.2025)
|
||||
|
||||
|
@ -59,7 +59,7 @@ todo add info about factory reset hub to restore 30 day trial
|
||||
| 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/) |
|
||||
| Sound emitter | sound_switch | | ✅ | [Siren](https://www.home-assistant.io/integrations/siren/) |
|
||||
| Thermostat | [thermostat](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/thermostat.ts) | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | ✅ | [Climate](https://www.home-assistant.io/integrations/climate/) |
|
||||
| ??? | [user_code](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/user_code.ts) | | | |
|
||||
| Keypad | [user_code](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/user_code.ts) | | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Button](https://www.home-assistant.io/integrations/button/), [Text](https://www.home-assistant.io/integrations/text/)|
|
||||
| Water heater | [water_heater](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/water_heater.ts) | | ✅ | [Water heater](https://www.home-assistant.io/integrations/water_heater/) |
|
||||
|
||||
## Problematic services
|
||||
|
@ -23,6 +23,7 @@ import { scene_ctrl__components } from '../services/scene_ctrl';
|
||||
import { siren_ctrl__components } from '../services/siren_ctrl';
|
||||
import { sound_switch__components } from '../services/sound_switch';
|
||||
import { thermostat__components } from '../services/thermostat';
|
||||
import { user_code__components } from '../services/user_code';
|
||||
import { water_heater__components } from '../services/water_heater';
|
||||
import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
|
||||
import { ha } from './globals';
|
||||
@ -206,6 +207,7 @@ const serviceHandlers: {
|
||||
siren_ctrl: siren_ctrl__components,
|
||||
sound_switch: sound_switch__components,
|
||||
thermostat: thermostat__components,
|
||||
user_code: user_code__components,
|
||||
water_heater: water_heater__components,
|
||||
};
|
||||
|
||||
|
311
futurehome/src/services/user_code.ts
Normal file
311
futurehome/src/services/user_code.ts
Normal file
@ -0,0 +1,311 @@
|
||||
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 user_code__components(
|
||||
topicPrefix: string,
|
||||
device: VinculumPd7Device,
|
||||
svc: VinculumPd7Service,
|
||||
_svcName: string,
|
||||
): ServiceComponentsCreationResult | undefined {
|
||||
const components: Record<string, HaMqttComponent> = {};
|
||||
const commandHandlers: CommandHandlers = {};
|
||||
|
||||
// Extract supported properties
|
||||
const supUsercodes = svc.props?.sup_usercodes || [];
|
||||
const supUserstatus = svc.props?.sup_userstatus || [];
|
||||
const supUsertypes = svc.props?.sup_usertypes || [];
|
||||
const supUsers = svc.props?.sup_users || 10;
|
||||
const minCodeLength = svc.props?.min_code_length || 4;
|
||||
const maxCodeLength = svc.props?.max_code_length || 8;
|
||||
|
||||
// Create sensors for user code configuration and status
|
||||
if (svc.intf?.includes('evt.usercode.users_report')) {
|
||||
// Create a sensor to show the number of configured users
|
||||
components[`${svc.addr}_user_count`] = {
|
||||
unique_id: `${svc.addr}_user_count`,
|
||||
platform: 'sensor',
|
||||
entity_category: 'diagnostic',
|
||||
name: 'User Count',
|
||||
icon: 'mdi:account-multiple',
|
||||
value_template: `{% set users = value_json['${svc.addr}'].users %}{% if users %}{{ users.pin | length + (users.rfid | length if users.rfid else 0) }}{% else %}0{% endif %}`,
|
||||
};
|
||||
|
||||
// Create a sensor to show supported user codes types
|
||||
if (supUsercodes.length > 0) {
|
||||
components[`${svc.addr}_supported_types`] = {
|
||||
unique_id: `${svc.addr}_supported_types`,
|
||||
platform: 'sensor',
|
||||
entity_category: 'diagnostic',
|
||||
name: 'Supported Code Types',
|
||||
icon: 'mdi:key-variant',
|
||||
value_template: `{{ '${supUsercodes.join(', ')}' }}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a sensor to show max users
|
||||
components[`${svc.addr}_max_users`] = {
|
||||
unique_id: `${svc.addr}_max_users`,
|
||||
platform: 'sensor',
|
||||
entity_category: 'diagnostic',
|
||||
name: 'Max Users',
|
||||
icon: 'mdi:account-group',
|
||||
value_template: `{{ ${supUsers} }}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Create binary sensor for access reports (if supported)
|
||||
if (svc.intf?.includes('evt.usercode.access_report')) {
|
||||
components[`${svc.addr}_access_granted`] = {
|
||||
unique_id: `${svc.addr}_access_granted`,
|
||||
platform: 'binary_sensor',
|
||||
device_class: 'lock',
|
||||
name: 'Access Granted',
|
||||
value_template: `{{ (value_json['${svc.addr}'].access_report.permission == 'granted') | iif('ON', 'OFF') }}`,
|
||||
};
|
||||
|
||||
// Create sensor for last access event details
|
||||
components[`${svc.addr}_last_access`] = {
|
||||
unique_id: `${svc.addr}_last_access`,
|
||||
platform: 'sensor',
|
||||
name: 'Last Access',
|
||||
icon: 'mdi:account-clock',
|
||||
value_template: `{% set access = value_json['${svc.addr}'].access_report %}{% if access %}{{ access.event | default('unknown') }} by {{ access.alias | default('unknown') }} ({{ access.identification | default('unknown') }}){% else %}No access{% endif %}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Create button for clearing all users (if supported)
|
||||
if (svc.intf?.includes('cmd.usercode.clear_all')) {
|
||||
const clearAllTopic = `${topicPrefix}${svc.addr}/clear_all/command`;
|
||||
|
||||
components[`${svc.addr}_clear_all`] = {
|
||||
unique_id: `${svc.addr}_clear_all`,
|
||||
platform: 'button',
|
||||
entity_category: 'config',
|
||||
device_class: null,
|
||||
name: 'Clear All Users',
|
||||
icon: 'mdi:account-remove',
|
||||
command_topic: clearAllTopic,
|
||||
};
|
||||
|
||||
commandHandlers[clearAllTopic] = async (_payload: string) => {
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'user_code',
|
||||
cmd: 'cmd.usercode.clear_all',
|
||||
val_t: 'null',
|
||||
val: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Create button for requesting user list (if supported)
|
||||
if (svc.intf?.includes('cmd.usercode.get')) {
|
||||
const getUsersTopic = `${topicPrefix}${svc.addr}/get_users/command`;
|
||||
|
||||
components[`${svc.addr}_get_users`] = {
|
||||
unique_id: `${svc.addr}_get_users`,
|
||||
platform: 'button',
|
||||
entity_category: 'diagnostic',
|
||||
device_class: null,
|
||||
name: 'Refresh Users',
|
||||
icon: 'mdi:account-search',
|
||||
command_topic: getUsersTopic,
|
||||
};
|
||||
|
||||
commandHandlers[getUsersTopic] = async (_payload: string) => {
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'user_code',
|
||||
cmd: 'cmd.usercode.get',
|
||||
val_t: 'str_map',
|
||||
val: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Create text inputs for user management (if supported)
|
||||
if (svc.intf?.includes('cmd.usercode.set')) {
|
||||
// Text input for adding PIN users
|
||||
if (supUsercodes.includes('pin')) {
|
||||
const addPinUserTopic = `${topicPrefix}${svc.addr}/add_pin_user/command`;
|
||||
|
||||
components[`${svc.addr}_add_pin_user`] = {
|
||||
unique_id: `${svc.addr}_add_pin_user`,
|
||||
platform: 'text',
|
||||
entity_category: 'config',
|
||||
name: 'Add PIN User (slot:alias:code)',
|
||||
icon: 'mdi:account-plus',
|
||||
min: minCodeLength + 4, // minimum: "1:a:1234"
|
||||
max: maxCodeLength + 20, // reasonable max including slot and alias
|
||||
command_topic: addPinUserTopic,
|
||||
};
|
||||
|
||||
commandHandlers[addPinUserTopic] = async (payload: string) => {
|
||||
// Expected format: "slot:alias:code" e.g., "1:John:1234"
|
||||
const parts = payload.split(':');
|
||||
if (parts.length !== 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [slot, alias, code] = parts;
|
||||
if (!slot || !alias || !code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotNum = parseInt(slot, 10);
|
||||
if (Number.isNaN(slotNum) || slotNum < 1 || slotNum > supUsers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length < minCodeLength || code.length > maxCodeLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'user_code',
|
||||
cmd: 'cmd.usercode.set',
|
||||
val_t: 'str_map',
|
||||
val: {
|
||||
id_type: 'pin',
|
||||
slot: slot,
|
||||
alias: alias,
|
||||
code: code,
|
||||
user_status: supUserstatus.includes('enabled')
|
||||
? 'enabled'
|
||||
: supUserstatus[0] || 'enabled',
|
||||
user_type: supUsertypes.includes('normal')
|
||||
? 'normal'
|
||||
: supUsertypes[0] || 'normal',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Text input for adding RFID users
|
||||
if (supUsercodes.includes('rfid')) {
|
||||
const addRfidUserTopic = `${topicPrefix}${svc.addr}/add_rfid_user/command`;
|
||||
|
||||
components[`${svc.addr}_add_rfid_user`] = {
|
||||
unique_id: `${svc.addr}_add_rfid_user`,
|
||||
platform: 'text',
|
||||
entity_category: 'config',
|
||||
name: 'Add RFID User (slot:alias:code)',
|
||||
icon: 'mdi:account-plus',
|
||||
min: 6, // minimum: "1:a:xx"
|
||||
max: 50, // reasonable max for RFID codes
|
||||
command_topic: addRfidUserTopic,
|
||||
};
|
||||
|
||||
commandHandlers[addRfidUserTopic] = async (payload: string) => {
|
||||
// Expected format: "slot:alias:code" e.g., "1:John:ABCD1234"
|
||||
const parts = payload.split(':');
|
||||
if (parts.length !== 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [slot, alias, code] = parts;
|
||||
if (!slot || !alias || !code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotNum = parseInt(slot, 10);
|
||||
if (Number.isNaN(slotNum) || slotNum < 1 || slotNum > supUsers) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'user_code',
|
||||
cmd: 'cmd.usercode.set',
|
||||
val_t: 'str_map',
|
||||
val: {
|
||||
id_type: 'rfid',
|
||||
slot: slot,
|
||||
alias: alias,
|
||||
code: code,
|
||||
user_status: supUserstatus.includes('enabled')
|
||||
? 'enabled'
|
||||
: supUserstatus[0] || 'enabled',
|
||||
user_type: supUsertypes.includes('normal')
|
||||
? 'normal'
|
||||
: supUsertypes[0] || 'normal',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create text input for removing users (if supported)
|
||||
if (svc.intf?.includes('cmd.usercode.clear')) {
|
||||
const clearUserTopic = `${topicPrefix}${svc.addr}/clear_user/command`;
|
||||
|
||||
components[`${svc.addr}_clear_user`] = {
|
||||
unique_id: `${svc.addr}_clear_user`,
|
||||
platform: 'text',
|
||||
entity_category: 'config',
|
||||
name: 'Clear User (type:slot)',
|
||||
icon: 'mdi:account-minus',
|
||||
min: 5, // minimum: "pin:1"
|
||||
max: 10, // maximum: "rfid:999"
|
||||
command_topic: clearUserTopic,
|
||||
};
|
||||
|
||||
commandHandlers[clearUserTopic] = async (payload: string) => {
|
||||
// Expected format: "type:slot" e.g., "pin:1" or "rfid:2"
|
||||
const parts = payload.split(':');
|
||||
if (parts.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [idType, slot] = parts;
|
||||
if (!idType || !slot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!supUsercodes.includes(idType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotNum = parseInt(slot, 10);
|
||||
if (Number.isNaN(slotNum) || slotNum < 1 || slotNum > supUsers) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'user_code',
|
||||
cmd: 'cmd.usercode.clear',
|
||||
val_t: 'str_map',
|
||||
val: {
|
||||
id_type: idType,
|
||||
slot: slot,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Create sensor for configuration reports (if supported)
|
||||
if (svc.intf?.includes('evt.usercode.config_report')) {
|
||||
components[`${svc.addr}_config_status`] = {
|
||||
unique_id: `${svc.addr}_config_status`,
|
||||
platform: 'sensor',
|
||||
name: 'Configuration Status',
|
||||
icon: 'mdi:cog',
|
||||
value_template: `{% set config = value_json['${svc.addr}'].config_report %}{% if config %}{{ config.event | default('unknown') }} - Slot {{ config.slot | default('?') }} ({{ config.alias | default('unknown') }}){% else %}No recent changes{% endif %}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
components,
|
||||
commandHandlers,
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user