diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3d81fff..0ff22884 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,7 @@ repos: - id: check-merge-conflict - id: check-xml - id: check-yaml + exclude: ^blueprints/ - id: debug-statements - id: end-of-file-fixer - id: requirements-txt-fixer diff --git a/README.md b/README.md index 877b7d02..d9d19a0f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,18 @@ Features: - `input_boolean` - code is active when the input boolean is `on` - Optionally define a maximum number of uses for a code before the code is disabled +## Blueprints + +LCM provides blueprints for common automation patterns: + +| Blueprint | Description | Import | +| --------- | ----------- | ------ | +| Slot Usage Limiter | Limit PIN uses with a counter, auto-disable at 0 | [![Import](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Framan325%2Flock_code_manager%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Flock_code_manager%2Fslot_usage_limiter.yaml) | +| Calendar Condition | Binary sensor for advanced calendar-based access control | [![Import](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Framan325%2Flock_code_manager%2Fblob%2Fmain%2Fblueprints%2Ftemplate%2Flock_code_manager%2Fcalendar_condition.yaml) | +| Calendar PIN Setter | Set PINs from calendar event data | [![Import](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Framan325%2Flock_code_manager%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Flock_code_manager%2Fcalendar_pin_setter.yaml) | + +See the [Blueprints wiki page](https://github.com/raman325/lock_code_manager/wiki/Blueprints) for setup details. + Locks from the following integrations are currently supported: - Z-Wave diff --git a/blueprints/automation/lock_code_manager/calendar_pin_setter.yaml b/blueprints/automation/lock_code_manager/calendar_pin_setter.yaml new file mode 100644 index 00000000..09902c54 --- /dev/null +++ b/blueprints/automation/lock_code_manager/calendar_pin_setter.yaml @@ -0,0 +1,147 @@ +--- +blueprint: + name: "Lock Code Manager: Calendar PIN Setter" + description: > + Extracts a PIN from calendar event attributes using a user-provided template + and sets it on a Lock Code Manager code slot. Optionally clears the PIN when + the calendar event ends. + + + **Available template variables** + + Your PIN and slot number templates can use these variables: + + - `message` — calendar event summary/title + + - `description` — calendar event description + + - `location` — calendar event location + + - `start_time` — event start time + + - `end_time` — event end time + + - `all_day` — whether the event is an all-day event + + + **Example PIN templates** + + - Extract a 4-digit PIN from description: + `{{ description | regex_findall('\\b\\d{4}\\b') | first | default('') }}` + + - Use the first 4 characters of the event title: + `{{ message[:4] if message | length >= 4 else '' }}` + + + **Template debugging** + + To test your PIN template, go to Developer Tools → Template and paste: + + ``` + + {% set description = state_attr('calendar.YOUR_CALENDAR', 'description') | default('') %} + + {{ description | regex_findall('\\b\\d{4}\\b') | first | default('') }} + + ``` + domain: automation + # yamllint disable-line rule:line-length + source_url: https://github.com/raman325/lock_code_manager/blob/main/blueprints/automation/lock_code_manager/calendar_pin_setter.yaml + input: + config_entry: + name: Lock Code Manager config entry + description: The Lock Code Manager config entry that manages your lock(s). + selector: + config_entry: + integration: lock_code_manager + slot_number: + name: Slot number + description: > + The code slot number. Can be a static number (e.g. `3`) or a Jinja2 + template that resolves to a number using calendar event attributes + (e.g. `{{ description | regex_findall('slot (\d+)') | first }}`). + selector: + template: + calendar_entity: + name: Calendar entity + description: The calendar entity whose events provide PIN codes. + selector: + entity: + domain: calendar + pin_template: + name: PIN template + description: > + A Jinja2 template that extracts a PIN from calendar event attributes. + Must evaluate to a non-empty string to set the PIN, or empty string to + skip setting. + selector: + template: + clear_on_end: + name: Clear PIN when event ends + description: > + When enabled, the PIN is cleared (set to empty) when the calendar + event ends. + default: true + selector: + boolean: + +mode: single + +variables: + config_entry: !input config_entry + calendar_entity: !input calendar_entity + clear_on_end: !input clear_on_end + config_entry_title: > + {{ config_entry_attr(config_entry, 'title') }} + _all_entities: > + {{ integration_entities(config_entry_title) }} + +triggers: + - trigger: state + entity_id: !input calendar_entity + +actions: + - variables: + message: > + {{ state_attr(calendar_entity, 'message') | default('', true) }} + description: > + {{ state_attr(calendar_entity, 'description') | default('', true) }} + location: > + {{ state_attr(calendar_entity, 'location') | default('', true) }} + start_time: > + {{ state_attr(calendar_entity, 'start_time') | default('', true) }} + end_time: > + {{ state_attr(calendar_entity, 'end_time') | default('', true) }} + all_day: > + {{ state_attr(calendar_entity, 'all_day') | default(false, true) }} + slot_number: !input slot_number + pin_entity: > + {{ _all_entities + | select('match', 'text\\..*_code_slot_' ~ slot_number ~ '_pin') + | first | default('') }} + pin_value: !input pin_template + - choose: + - conditions: + - condition: template + value_template: > + {{ trigger.to_state.state == 'on' + and pin_entity != '' + and pin_value != '' }} + sequence: + - action: text.set_value + target: + entity_id: "{{ pin_entity }}" + data: + value: "{{ pin_value }}" + - conditions: + - condition: template + value_template: > + {{ trigger.to_state.state == 'off' + and clear_on_end + and pin_entity != '' }} + sequence: + - action: text.set_value + target: + entity_id: "{{ pin_entity }}" + data: + value: "" diff --git a/blueprints/automation/lock_code_manager/slot_usage_limiter.yaml b/blueprints/automation/lock_code_manager/slot_usage_limiter.yaml new file mode 100644 index 00000000..28a01621 --- /dev/null +++ b/blueprints/automation/lock_code_manager/slot_usage_limiter.yaml @@ -0,0 +1,145 @@ +--- +blueprint: + name: "Lock Code Manager: Slot Usage Limiter" + description: > + Decrements an `input_number` helper each time a code slot PIN is used. + When the counter reaches 0 the slot is automatically disabled. + Optionally resets the counter when the slot is re-enabled. + + + **Requirements** + + This blueprint requires at least one lock that supports code slot events + (i.e. produces `*_code_slot_*_pin_used` events). If none of your locks + support this, the counter will never decrement. + + + **Setup** + + 1. Create an `input_number` helper (Settings → Devices & Services → Helpers) + with min=0, max=your desired limit, and step=1. + + 2. Import this blueprint and configure it with your Lock Code Manager config + entry, slot number, and the helper you just created. + + + **Template debugging** + + To test entity resolution, go to Developer Tools → Template and paste: + + ``` + + {% set title = config_entry_attr('YOUR_CONFIG_ENTRY_ID', 'title') %} + + {% set all = integration_entities(title) %} + + {{ all | select('match', 'event\\..*_code_slot_1_pin_used') | list }} + + ``` + + Replace YOUR_CONFIG_ENTRY_ID with your LCM config entry ID and 1 with your + slot number. + domain: automation + # yamllint disable-line rule:line-length + source_url: https://github.com/raman325/lock_code_manager/blob/main/blueprints/automation/lock_code_manager/slot_usage_limiter.yaml + input: + config_entry: + name: Lock Code Manager config entry + description: The Lock Code Manager config entry that manages your lock(s). + selector: + config_entry: + integration: lock_code_manager + slot_number: + name: Code slot number + description: The code slot number to monitor. + selector: + number: + min: 1 + max: 999 + mode: box + uses_counter: + name: Uses counter + description: > + An `input_number` helper that tracks remaining uses. + Create one via Settings → Devices & Services → Helpers. + selector: + entity: + domain: input_number + initial_uses: + name: Initial uses on re-enable + description: > + When greater than 0, the counter is automatically reset to this value + each time the slot is re-enabled. Set to 0 to disable auto-reset. + default: 0 + selector: + number: + min: 0 + max: 999 + mode: box + +mode: queued +max: 10 + +trigger_variables: + _config_entry: !input config_entry + _slot: !input slot_number + _title: > + {{ config_entry_attr(_config_entry, 'title') }} + _all_entities: > + {{ integration_entities(_title) }} + pin_used_entity: > + {{ _all_entities + | select('match', 'event\\..*_code_slot_' ~ _slot ~ '_pin_used') + | first | default('') }} + enabled_switch: > + {{ _all_entities + | select('match', 'switch\\..*_code_slot_' ~ _slot ~ '_enabled') + | first | default('') }} + +triggers: + - trigger: state + entity_id: "{{ pin_used_entity }}" + id: pin_used + - trigger: state + entity_id: "{{ enabled_switch }}" + id: slot_enabled + to: "on" + +variables: + initial_uses: !input initial_uses + uses_counter: !input uses_counter + +actions: + - choose: + - conditions: + - condition: trigger + id: pin_used + sequence: + - variables: + current: > + {{ states(uses_counter) | int(0) }} + new_count: > + {{ [current - 1, 0] | max }} + - action: input_number.set_value + target: + entity_id: !input uses_counter + data: + value: "{{ new_count }}" + - if: + - condition: template + value_template: "{{ new_count == 0 }}" + then: + - action: switch.turn_off + target: + entity_id: "{{ enabled_switch }}" + - conditions: + - condition: trigger + id: slot_enabled + - condition: template + value_template: "{{ initial_uses | int(0) > 0 }}" + sequence: + - action: input_number.set_value + target: + entity_id: !input uses_counter + data: + value: !input initial_uses diff --git a/blueprints/template/lock_code_manager/calendar_condition.yaml b/blueprints/template/lock_code_manager/calendar_condition.yaml new file mode 100644 index 00000000..5b46106e --- /dev/null +++ b/blueprints/template/lock_code_manager/calendar_condition.yaml @@ -0,0 +1,136 @@ +--- +blueprint: + name: "Lock Code Manager: Calendar Condition" + description: > + Creates a binary sensor that turns ON when a calendar event is active and + an optional condition template is truthy. Assign the resulting binary sensor + as the condition entity for a Lock Code Manager code slot. + + + **Available template variables** + + Your condition template can use these variables: + + - `message` — calendar event summary/title + + - `description` — calendar event description + + - `location` — calendar event location + + - `start_time` — event start time + + - `end_time` — event end time + + - `all_day` — whether the event is an all-day event + + - `slot_number` — the resolved slot number + + - `config_entry_title` — the LCM config entry title + + - `name_state` — the current name of the code slot + + - `lock_entity_ids` — list of lock entity IDs that support code slot events for this slot + + + **Example condition templates** + + - Allow when event title contains "Guest": `{{ 'Guest' in message }}` + + - Allow only for a specific lock: `{{ 'lock.front_door' in lock_entity_ids }}` + + + **Note:** The condition template and calendar attributes are evaluated when the + sensor is first loaded and when Home Assistant reloads template entities. For + the default `{{ true }}`, the sensor dynamically tracks the calendar on/off + state. For conditions that reference event attributes, reload template entities + after calendar event changes to re-evaluate. + + + **Template debugging** + + To test your condition template, go to Developer Tools → Template and paste: + + ``` + + {% set message = state_attr('calendar.YOUR_CALENDAR', 'message') | default('', true) %} + + {% set description = state_attr('calendar.YOUR_CALENDAR', 'description') | default('', true) %} + + {% set location = state_attr('calendar.YOUR_CALENDAR', 'location') | default('', true) %} + + {{ YOUR_CONDITION_TEMPLATE_HERE }} + + ``` + domain: template + # yamllint disable-line rule:line-length + source_url: https://github.com/raman325/lock_code_manager/blob/main/blueprints/template/lock_code_manager/calendar_condition.yaml + input: + config_entry: + name: Lock Code Manager config entry + description: The Lock Code Manager config entry that manages your lock(s). + selector: + config_entry: + integration: lock_code_manager + slot_number: + name: Slot number + description: > + The code slot number. Can be a static number (e.g. `3`) or a Jinja2 + template that resolves to a number using calendar event attributes + (e.g. `{{ description | regex_findall('slot (\d+)') | first }}`). + selector: + template: + calendar_entity: + name: Calendar entity + description: The calendar entity whose events control this condition. + selector: + entity: + domain: calendar + condition_template: + name: Condition template + description: > + A Jinja2 template that must evaluate to truthy for the sensor to turn + ON (in addition to the calendar being active). Leave as `{{ true }}` + to activate whenever the calendar event is active. + default: "{{ true }}" + selector: + template: + +variables: + config_entry: !input config_entry + calendar_entity: !input calendar_entity + config_entry_title: > + {{ config_entry_attr(config_entry, 'title') }} + _all_entities: > + {{ integration_entities(config_entry_title) }} + message: > + {{ state_attr(calendar_entity, 'message') | default('', true) }} + description: > + {{ state_attr(calendar_entity, 'description') | default('', true) }} + location: > + {{ state_attr(calendar_entity, 'location') | default('', true) }} + start_time: > + {{ state_attr(calendar_entity, 'start_time') | default('', true) }} + end_time: > + {{ state_attr(calendar_entity, 'end_time') | default('', true) }} + all_day: > + {{ state_attr(calendar_entity, 'all_day') | default(false, true) }} + slot_number: !input slot_number + _name_entity: > + {{ _all_entities + | select('match', 'text\\..*_code_slot_' ~ slot_number ~ '_name') + | first | default('') }} + name_state: > + {{ states(_name_entity) if _name_entity else '' }} + _event_entity: > + {{ _all_entities + | select('match', 'event\\..*_code_slot_' ~ slot_number ~ '_pin_used') + | first | default('') }} + lock_entity_ids: > + {{ state_attr(_event_entity, 'event_types') | default([]) }} + _condition_result: !input condition_template + +binary_sensor: + state: > + {{ is_state(calendar_entity, 'on') and _condition_result }} + availability: > + {{ states(calendar_entity) not in ('unknown', 'unavailable') }}