diff --git a/.HA_VERSION b/.HA_VERSION new file mode 100644 index 0000000..617bf7c --- /dev/null +++ b/.HA_VERSION @@ -0,0 +1 @@ +2023.6.2 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6aeacdb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +quote_type = single + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60323d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.cloud/ +.storage/ +*.db +*.db-shm +*.db-wal +*.log +*.log.* +*.db +secrets.yaml +image/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4a954db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} diff --git a/automations.yaml b/automations.yaml new file mode 100644 index 0000000..3a1db06 --- /dev/null +++ b/automations.yaml @@ -0,0 +1,30 @@ +- id: '1686576758103' + alias: Notify when Jens arrive at Volleyball + description: '' + trigger: + - platform: geo_location + source: device_tracker.jens + zone: zone.volleyball + event: enter + condition: [] + action: + - service: notify.parents + data: + title: "\U0001F3D0 Volleyball" + message: Jens has arrived at Volleyball. + mode: single +- id: '1686576926556' + alias: New Automation + description: '' + trigger: + - platform: zone + entity_id: person.jens + zone: zone.volleyball + event: enter + condition: [] + action: + - service: notify.parents + data: + title: Volleyball + message: Jens has arrive at Volleyball. + mode: single diff --git a/automations/buttons/bedroom_ceiling_light_switch.yaml b/automations/buttons/bedroom_ceiling_light_switch.yaml new file mode 100644 index 0000000..e2b8682 --- /dev/null +++ b/automations/buttons/bedroom_ceiling_light_switch.yaml @@ -0,0 +1,20 @@ +id: 2da287d4-06a6-11ee-be56-0242ac120002 +alias: Bedroom Ceiling Light Switch +description: '' + +use_blueprint: + path: EPMatt/ikea_e1743.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.bedroom_ceiling_light_switch_action + helper_last_controller_event: input_text.bedroom_ceiling_light_switch + action_button_up_short: + - service: light.turn_on + data: {} + target: + device_id: 15ea5e855efdb27b362dd8725d47e961 + action_button_down_short: + - service: light.turn_off + data: {} + target: + device_id: 15ea5e855efdb27b362dd8725d47e961 diff --git a/automations/buttons/bedroom_left_bedside_switch.yaml b/automations/buttons/bedroom_left_bedside_switch.yaml new file mode 100644 index 0000000..eaf156c --- /dev/null +++ b/automations/buttons/bedroom_left_bedside_switch.yaml @@ -0,0 +1,20 @@ +id: 328dbe8a-06a6-11ee-be56-0242ac120002 +alias: Bedroom Left Bedside Switch +description: '' + +use_blueprint: + path: EPMatt/ikea_e1743.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.bedroom_left_bedside_switch_action + helper_last_controller_event: input_text.bedroom_left_bedside_switch + action_button_up_short: + - service: light.turn_on + data: {} + target: + device_id: 15ea5e855efdb27b362dd8725d47e961 + action_button_down_short: + - service: light.turn_off + data: {} + target: + device_id: 15ea5e855efdb27b362dd8725d47e961 diff --git a/automations/buttons/bedroom_right_bedside_switch.yaml b/automations/buttons/bedroom_right_bedside_switch.yaml new file mode 100644 index 0000000..61e3773 --- /dev/null +++ b/automations/buttons/bedroom_right_bedside_switch.yaml @@ -0,0 +1,20 @@ +id: 3699b038-06a6-11ee-be56-0242ac120002 +alias: Bedroom Right Bedside Switch +description: '' + +use_blueprint: + path: EPMatt/ikea_e1743.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.bedroom_right_bedside_switch_action + helper_last_controller_event: input_text.bedroom_right_bedside_switch + action_button_up_short: + - service: light.turn_on + data: {} + target: + device_id: e85627807df1e28343097ddab1d1c8f8 + action_button_down_short: + - service: light.turn_off + data: {} + target: + device_id: e85627807df1e28343097ddab1d1c8f8 diff --git a/automations/buttons/kids_room_shortcut_button.yaml b/automations/buttons/kids_room_shortcut_button.yaml new file mode 100644 index 0000000..b64c377 --- /dev/null +++ b/automations/buttons/kids_room_shortcut_button.yaml @@ -0,0 +1,20 @@ +id: 3b55fef6-06a6-11ee-be56-0242ac120002 +alias: Kids Room Shortcut Button +description: '' + +use_blueprint: + path: EPMatt/ikea_e1812.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.kids_room_shortcut_button_action + helper_last_controller_event: input_text.kids_room_shortcut_button + action_button_short: + - service: light.toggle + data: {} + target: + device_id: 0e2de7c93f4d91517ad5f0b24a66bd79 + action_button_long: + - service: light.toggle + data: {} + target: + device_id: efed7354b3c57de1fb089a0708f3375b diff --git a/automations/buttons/kitchen_lights_switch.yaml b/automations/buttons/kitchen_lights_switch.yaml new file mode 100644 index 0000000..7061e1c --- /dev/null +++ b/automations/buttons/kitchen_lights_switch.yaml @@ -0,0 +1,20 @@ +id: 3f386afe-06a6-11ee-be56-0242ac120002 +alias: Kitchen Lights Switch +description: '' + +use_blueprint: + path: EPMatt/ikea_e1743.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.kitchen_lights_switch_action + helper_last_controller_event: input_text.kitchen_lights_switch + action_button_up_short: + - service: light.turn_on + data: {} + target: + device_id: 105c00044612e5f684c0b394c13fe254 + action_button_down_short: + - service: light.turn_off + data: {} + target: + device_id: 105c00044612e5f684c0b394c13fe254 diff --git a/automations/buttons/living_room_shortcut_button.yaml b/automations/buttons/living_room_shortcut_button.yaml new file mode 100644 index 0000000..99100b9 --- /dev/null +++ b/automations/buttons/living_room_shortcut_button.yaml @@ -0,0 +1,17 @@ +id: 436128b4-06a6-11ee-be56-0242ac120002 +alias: Living Room Shortcut Button +description: '' + +use_blueprint: + path: EPMatt/ikea_e1812.yaml + input: + integration: Zigbee2MQTT + controller_entity: sensor.living_room_shortcut_button_action + helper_last_controller_event: input_text.living_room_shortcut_button + action_button_short: + - service: light.toggle + data: {} + target: + device_id: + - c9d4c69c32942c0267114791a03d4068 + - 5e138850a50d603210f76cf09ff41ed4 diff --git a/automations/hallway_front_door.yaml b/automations/hallway_front_door.yaml new file mode 100644 index 0000000..29799a0 --- /dev/null +++ b/automations/hallway_front_door.yaml @@ -0,0 +1,55 @@ +id: 1611087a-06a6-11ee-be56-0242ac120002 +alias: "Hallway Front Door Notifications" + +trigger: + - platform: state + entity_id: binary_sensor.hallway_front_door + from: "off" + to: "on" + id: opened + - platform: state + entity_id: binary_sensor.hallway_front_door + from: "on" + to: "off" + id: closed + - platform: state + entity_id: binary_sensor.hallway_front_door + from: "off" + to: "on" + for: 00:05:00 + id: left_opened + +condition: [] + +action: + - choose: + - conditions: + - condition: trigger + id: opened + sequence: + - service: notify.parents + data: + title: "Home" + message: "Front Door was opened." + - conditions: + - condition: trigger + id: closed + sequence: + - service: notify.parents + data: + title: "Home" + message: "Front Door was closed." + - conditions: + - condition: trigger + id: left_opened + sequence: + - service: notify.parents + data: + title: "Home" + data: + push: + sound: + name: default + critical: 1 + volume: 0.5 + message: "Front Door has been left open." diff --git a/automations/lights/living_room_tv_lights_off.yaml b/automations/lights/living_room_tv_lights_off.yaml new file mode 100644 index 0000000..d89bd04 --- /dev/null +++ b/automations/lights/living_room_tv_lights_off.yaml @@ -0,0 +1,25 @@ +id: 484a402c-06a6-11ee-be56-0242ac120002 +alias: Turn off TV lights when TV stops playing + +description: '' + +trigger: +- platform: state + entity_id: + - media_player.living_room_apple_tv + - media_player.living_room_tv + from: playing + for: + hours: 0 + minutes: 0 + seconds: 5 + +condition: [] + +action: +- service: light.turn_off + data: {} + target: + entity_id: light.living_room_tv_lights + +mode: single diff --git a/automations/lights/living_room_tv_lights_on.yaml b/automations/lights/living_room_tv_lights_on.yaml new file mode 100644 index 0000000..d91dcd7 --- /dev/null +++ b/automations/lights/living_room_tv_lights_on.yaml @@ -0,0 +1,24 @@ +id: 4b293604-06a6-11ee-be56-0242ac120002 +alias: Turn on TV lights when TV is playing +description: '' + +trigger: +- platform: state + entity_id: + - media_player.living_room_apple_tv + - media_player.living_room_tv + to: playing + for: + hours: 0 + minutes: 0 + seconds: 5 + +condition: [] + +action: +- service: light.turn_on + data: {} + target: + entity_id: light.living_room_tv_lights + +mode: single diff --git a/automations/zones/volleyball_arrive.yaml b/automations/zones/volleyball_arrive.yaml new file mode 100644 index 0000000..a34ce29 --- /dev/null +++ b/automations/zones/volleyball_arrive.yaml @@ -0,0 +1,15 @@ +id: '1686576926556' +alias: Notify when Jens arrive at Volleyball +description: '' +trigger: +- platform: zone + entity_id: person.jens + zone: zone.volleyball + event: enter +condition: [] +action: +- service: notify.parents + data: + title: "\U0001F3D0 Volleyball" + message: Jens has arrived at Volleyball. +mode: single diff --git a/automations/zones/volleyball_exit.yaml b/automations/zones/volleyball_exit.yaml new file mode 100644 index 0000000..96feb7c --- /dev/null +++ b/automations/zones/volleyball_exit.yaml @@ -0,0 +1,15 @@ +id: '1686576926123' +alias: Notify when Jens leaves Volleyball +description: '' +trigger: +- platform: zone + entity_id: person.jens + zone: zone.volleyball + event: leave +condition: [] +action: +- service: notify.parents + data: + title: "\U0001F3D0 Volleyball" + message: Jens has left the Volleyball. +mode: single diff --git a/blueprints/automation/EPMatt/ikea_e1743.yaml b/blueprints/automation/EPMatt/ikea_e1743.yaml new file mode 100644 index 0000000..4d305ed --- /dev/null +++ b/blueprints/automation/EPMatt/ikea_e1743.yaml @@ -0,0 +1,425 @@ +blueprint: + name: Controller - IKEA E1743 TRÅDFRI On/Off Switch & Dimmer + description: "# Controller - IKEA E1743 TRÅDFRI On/Off Switch & Dimmer\n\nController + automation for executing any kind of action triggered by the provided IKEA E1743 + TRÅDFRI On/Off Switch & Dimmer. Allows to optionally loop an action on a button + long press.\nSupports deCONZ, ZHA, Zigbee2MQTT.\n\nAutomations created with this + blueprint can be connected with one or more [Hooks](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/hooks) + supported by this controller.\nHooks allow to easily create controller-based automations + for interacting with media players, lights, covers and more.\nSee the list of + [Hooks available for this controller](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1743#available-hooks) + for additional details.\n\n\U0001F4D5 Full documentation regarding this blueprint + is available [here](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1743).\n\n\U0001F680 + This blueprint is part of the **[Awesome HA Blueprints](https://epmatt.github.io/awesome-ha-blueprints) + project**.\n\nℹ️ Version 2022.08.08\n" + source_url: https://github.com/EPMatt/awesome-ha-blueprints/blob/main/blueprints/controllers/ikea_e1743/ikea_e1743.yaml + domain: automation + input: + integration: + name: (Required) Integration + description: Integration used for connecting the remote with Home Assistant. + Select one of the available values. + selector: + select: + options: + - deCONZ + - ZHA + - Zigbee2MQTT + multiple: false + custom_value: false + controller_device: + name: (deCONZ, ZHA) Controller Device + description: The controller device to use for the automation. Choose a value + only if the remote is integrated with deCONZ, ZHA. + default: '' + selector: + device: {} + controller_entity: + name: (Zigbee2MQTT) Controller Entity + description: The action sensor of the controller to use for the automation. + Choose a value only if the remote is integrated with Zigbee2MQTT. + default: '' + selector: + entity: + domain: + - sensor + multiple: false + helper_last_controller_event: + name: (Required) Helper - Last Controller Event + description: Input Text used to store the last event fired by the controller. + You will need to manually create a text input entity for this, please read + the blueprint Additional Notes for more info. + default: '' + selector: + entity: + domain: + - input_text + multiple: false + action_button_up_short: + name: (Optional) Up button short press + description: Action to run on short up button press. + default: [] + selector: + action: {} + action_button_up_long: + name: (Optional) Up button long press + description: Action to run on long up button press. + default: [] + selector: + action: {} + action_button_up_release: + name: (Optional) Up button release + description: Action to run on up button release after long press. + default: [] + selector: + action: {} + action_button_up_double: + name: (Optional) Up button double press + description: Action to run on double up button press. + default: [] + selector: + action: {} + action_button_down_short: + name: (Optional) Down button short press + description: Action to run on short down button press. + default: [] + selector: + action: {} + action_button_down_long: + name: (Optional) Down button long press + description: Action to run on long down button press. + default: [] + selector: + action: {} + action_button_down_release: + name: (Optional) Down button release + description: Action to run on down button release after long press. + default: [] + selector: + action: {} + action_button_down_double: + name: (Optional) Down button double press + description: Action to run on double down button press. + default: [] + selector: + action: {} + button_up_long_loop: + name: (Optional) Up button long press - loop until release + description: Loop the up button action until the button is released. + default: false + selector: + boolean: {} + button_up_long_max_loop_repeats: + name: (Optional) Up button long press - Maximum loop repeats + description: Maximum number of repeats for the custom action, when looping is + enabled. Use it as a safety limit to prevent an endless loop in case the corresponding + stop event is not received. + default: 500 + selector: + number: + min: 1.0 + max: 5000.0 + mode: slider + step: 1.0 + button_down_long_loop: + name: (Optional) Down button long press - loop until release + description: Loop the down button action until the button is released. + default: false + selector: + boolean: {} + button_down_long_max_loop_repeats: + name: (Optional) Down button long press - Maximum loop repeats + description: Maximum number of repeats for the custom action, when looping is + enabled. Use it as a safety limit to prevent an endless loop in case the corresponding + stop event is not received. + default: 500 + selector: + number: + min: 1.0 + max: 5000.0 + mode: slider + step: 1.0 + button_up_double_press: + name: (Optional) Expose up button double press event + description: Choose whether or not to expose the virtual double press event + for the up button. Turn this on if you are providing an action for the up + button double press event. + default: false + selector: + boolean: {} + button_down_double_press: + name: (Optional) Expose down button double press event + description: Choose whether or not to expose the virtual double press event + for the down button. Turn this on if you are providing an action for the down + button double press event. + default: false + selector: + boolean: {} + helper_double_press_delay: + name: (Optional) Helper - Double Press delay + description: Max delay between the first and the second button press for the + double press event. Provide a value only if you are using a double press action. + Increase this value if you notice that the double press action is not triggered + properly. + default: 500 + selector: + number: + min: 100.0 + max: 5000.0 + unit_of_measurement: milliseconds + mode: box + step: 10.0 + helper_debounce_delay: + name: (Optional) Helper - Debounce delay + description: Delay used for debouncing RAW controller events, by default set + to 0. A value of 0 disables the debouncing feature. Increase this value if + you notice custom actions or linked Hooks running multiple times when interacting + with the device. When the controller needs to be debounced, usually a value + of 100 is enough to remove all duplicate events. + default: 0 + selector: + number: + min: 0.0 + max: 1000.0 + unit_of_measurement: milliseconds + mode: box + step: 10.0 +variables: + integration: !input integration + button_up_long_loop: !input button_up_long_loop + button_up_long_max_loop_repeats: !input button_up_long_max_loop_repeats + button_up_double_press: !input button_up_double_press + button_down_long_loop: !input button_down_long_loop + button_down_long_max_loop_repeats: !input button_down_long_max_loop_repeats + button_down_double_press: !input button_down_double_press + helper_last_controller_event: !input helper_last_controller_event + helper_double_press_delay: !input helper_double_press_delay + helper_debounce_delay: !input helper_debounce_delay + integration_id: '{{ integration | lower }}' + adjusted_double_press_delay: '{{ [helper_double_press_delay - helper_debounce_delay, + 100] | max }}' + actions_mapping: + deconz: + button_up_short: + - '1002' + button_up_long: + - '1001' + button_up_release: + - '1003' + button_down_short: + - '2002' + button_down_long: + - '2001' + button_down_release: + - '2003' + zha: + button_up_short: + - 'on' + button_up_long: + - move_with_on_off_0_83 + button_up_release: + - stop + button_down_short: + - 'off' + button_down_long: + - move_1_83 + button_down_release: + - stop + zigbee2mqtt: + button_up_short: + - 'on' + button_up_long: + - brightness_move_up + button_up_release: + - brightness_stop + button_down_short: + - 'off' + button_down_long: + - brightness_move_down + button_down_release: + - brightness_stop + button_up_short: '{{ actions_mapping[integration_id]["button_up_short"] }}' + button_up_long: '{{ actions_mapping[integration_id]["button_up_long"] }}' + button_up_release: '{{ actions_mapping[integration_id]["button_up_release"] }}' + button_down_short: '{{ actions_mapping[integration_id]["button_down_short"] }}' + button_down_long: '{{ actions_mapping[integration_id]["button_down_long"] }}' + button_down_release: '{{ actions_mapping[integration_id]["button_down_release"] + }}' + integrations_with_prev_event_storage: + - zha + - zigbee2mqtt + controller_entity: !input controller_entity + controller_device: !input controller_device + controller_id: '{% if integration_id=="zigbee2mqtt" %}{{controller_entity}}{% else + %}{{controller_device}}{% endif %}' +mode: restart +max_exceeded: silent +trigger: +- platform: event + event_type: state_changed + event_data: + entity_id: !input controller_entity +- platform: event + event_type: + - deconz_event + - zha_event + event_data: + device_id: !input controller_device +condition: +- condition: and + conditions: + - '{%- set trigger_action -%} {%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state + }} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%- + elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length + > 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%} {%- endset -%} {{ trigger_action + not in ["","None"] }}' + - '{{ integration_id != "zigbee2mqtt" or trigger.event.data.new_state.state != trigger.event.data.old_state.state + }}' +action: +- delay: + milliseconds: !input helper_debounce_delay +- variables: + trigger_action: '{%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state + }} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%- + elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length + > 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%}' + trigger_delta: '{{ (as_timestamp(now()) - ((states(helper_last_controller_event) + | from_json).t if helper_last_controller_event is not none and (states(helper_last_controller_event) + | regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else as_timestamp("1970-01-01 + 00:00:00"))) * 1000 }}' + last_controller_event: '{{ (states(helper_last_controller_event) | from_json).a + if helper_last_controller_event is not none and (states(helper_last_controller_event) + | regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else "" }}' +- service: input_text.set_value + data: + entity_id: !input helper_last_controller_event + value: '{{ {"a":trigger_action,"t":as_timestamp(now())} | to_json }}' +- choose: + - conditions: '{{ trigger_action | string in button_up_short }}' + sequence: + - choose: + - conditions: '{{ button_up_double_press }}' + sequence: + - choose: + - conditions: '{{ trigger_action | string in states(helper_last_controller_event) + and trigger_delta | int <= helper_double_press_delay | int }}' + sequence: + - service: input_text.set_value + data: + entity_id: !input helper_last_controller_event + value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json + }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_up_double + - choose: + - conditions: [] + sequence: !input action_button_up_double + default: + - delay: + milliseconds: '{{ adjusted_double_press_delay }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_up_short + - choose: + - conditions: [] + sequence: !input action_button_up_short + default: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_up_short + - choose: + - conditions: [] + sequence: !input action_button_up_short + - conditions: '{{ trigger_action | string in button_up_long }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_up_long + - choose: + - conditions: '{{ button_up_long_loop }}' + sequence: + - repeat: + while: '{{ repeat.index < button_up_long_max_loop_repeats | int }}' + sequence: !input action_button_up_long + default: !input action_button_up_long + - conditions: + - '{{ trigger_action | string in button_up_release }}' + - '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event + | string in button_up_long }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_up_release + - choose: + - conditions: [] + sequence: !input action_button_up_release + - conditions: '{{ trigger_action | string in button_down_short }}' + sequence: + - choose: + - conditions: '{{ button_down_double_press }}' + sequence: + - choose: + - conditions: '{{ trigger_action | string in states(helper_last_controller_event) + and trigger_delta | int <= helper_double_press_delay | int }}' + sequence: + - service: input_text.set_value + data: + entity_id: !input helper_last_controller_event + value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json + }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_down_double + - choose: + - conditions: [] + sequence: !input action_button_down_double + default: + - delay: + milliseconds: '{{ adjusted_double_press_delay }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_down_short + - choose: + - conditions: [] + sequence: !input action_button_down_short + default: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_down_short + - choose: + - conditions: [] + sequence: !input action_button_down_short + - conditions: '{{ trigger_action | string in button_down_long }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_down_long + - choose: + - conditions: '{{ button_down_long_loop }}' + sequence: + - repeat: + while: '{{ repeat.index < button_down_long_max_loop_repeats | int }}' + sequence: !input action_button_down_long + default: !input action_button_down_long + - conditions: + - '{{ trigger_action | string in button_down_release }}' + - '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event + | string in button_down_long }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_down_release + - choose: + - conditions: [] + sequence: !input action_button_down_release diff --git a/blueprints/automation/EPMatt/ikea_e1812.yaml b/blueprints/automation/EPMatt/ikea_e1812.yaml new file mode 100644 index 0000000..b787204 --- /dev/null +++ b/blueprints/automation/EPMatt/ikea_e1812.yaml @@ -0,0 +1,277 @@ +blueprint: + name: Controller - IKEA E1812 TRÅDFRI Shortcut button + description: "# Controller - IKEA E1812 TRÅDFRI Shortcut button\n\nController automation + for executing any kind of action triggered by the provided IKEA E1812 TRÅDFRI + Shortcut button. Allows to optionally loop an action on a button long press.\nSupports + deCONZ, ZHA, Zigbee2MQTT.\n\nAutomations created with this blueprint can be connected + with one or more [Hooks](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/hooks) + supported by this controller.\nHooks allow to easily create controller-based automations + for interacting with media players, lights, covers and more.\nSee the list of + [Hooks available for this controller](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1812#available-hooks) + for additional details.\n\n\U0001F4D5 Full documentation regarding this blueprint + is available [here](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1812).\n\n\U0001F680 + This blueprint is part of the **[Awesome HA Blueprints](https://epmatt.github.io/awesome-ha-blueprints) + project**.\n\nℹ️ Version 2022.08.08\n" + source_url: https://github.com/EPMatt/awesome-ha-blueprints/blob/main/blueprints/controllers/ikea_e1812/ikea_e1812.yaml + domain: automation + input: + integration: + name: (Required) Integration + description: Integration used for connecting the remote with Home Assistant. + Select one of the available values. + selector: + select: + options: + - deCONZ + - ZHA + - Zigbee2MQTT + custom_value: false + multiple: false + controller_device: + name: (deCONZ, ZHA) Controller Device + description: The controller device to use for the automation. Choose a value + only if the remote is integrated with deCONZ, ZHA. + default: '' + selector: + device: {} + controller_entity: + name: (Zigbee2MQTT) Controller Entity + description: The action sensor of the controller to use for the automation. + Choose a value only if the remote is integrated with Zigbee2MQTT. + default: '' + selector: + entity: + domain: + - sensor + multiple: false + helper_last_controller_event: + name: (Required) Helper - Last Controller Event + description: Input Text used to store the last event fired by the controller. + You will need to manually create a text input entity for this, please read + the blueprint Additional Notes for more info. + default: '' + selector: + entity: + domain: + - input_text + multiple: false + action_button_short: + name: (Optional) Button short press + description: Action to run on short button press. + default: [] + selector: + action: {} + action_button_long: + name: (Optional) Button long press + description: Action to run on long button press. + default: [] + selector: + action: {} + action_button_release: + name: (Optional) Button release + description: Action to run on button release after long press. + default: [] + selector: + action: {} + action_button_double: + name: (Optional) Button double press + description: Action to run on double button press. + default: [] + selector: + action: {} + button_long_loop: + name: (Optional) Button long press - loop until release + description: Loop the button action until the button is released. + default: false + selector: + boolean: {} + button_long_max_loop_repeats: + name: (Optional) Button long press - Maximum loop repeats + description: Maximum number of repeats for the custom action, when looping is + enabled. Use it as a safety limit to prevent an endless loop in case the corresponding + stop event is not received. + default: 500 + selector: + number: + min: 1.0 + max: 5000.0 + mode: slider + step: 1.0 + button_double_press: + name: (Optional) Expose button double press event + description: Choose whether or not to expose the virtual double press event + for the button. Turn this on if you are providing an action for the button + double press event. + default: false + selector: + boolean: {} + helper_double_press_delay: + name: (Optional) Helper - Double Press delay + description: Max delay between the first and the second button press for the + double press event. Provide a value only if you are using a double press action. + Increase this value if you notice that the double press action is not triggered + properly. + default: 500 + selector: + number: + min: 100.0 + max: 5000.0 + unit_of_measurement: milliseconds + mode: box + step: 10.0 + helper_debounce_delay: + name: (Optional) Helper - Debounce delay + description: Delay used for debouncing RAW controller events, by default set + to 0. A value of 0 disables the debouncing feature. Increase this value if + you notice custom actions or linked Hooks running multiple times when interacting + with the device. When the controller needs to be debounced, usually a value + of 100 is enough to remove all duplicate events. + default: 0 + selector: + number: + min: 0.0 + max: 1000.0 + unit_of_measurement: milliseconds + mode: box + step: 10.0 +variables: + integration: !input integration + button_long_loop: !input button_long_loop + button_long_max_loop_repeats: !input button_long_max_loop_repeats + button_double_press: !input button_double_press + helper_last_controller_event: !input helper_last_controller_event + helper_double_press_delay: !input helper_double_press_delay + helper_debounce_delay: !input helper_debounce_delay + integration_id: '{{ integration | lower }}' + adjusted_double_press_delay: '{{ [helper_double_press_delay - helper_debounce_delay, + 100] | max }}' + actions_mapping: + deconz: + button_short: + - '1002' + button_long: + - '1001' + button_release: + - '1003' + zha: + button_short: + - 'on' + button_long: + - move_with_on_off_0_83 + button_release: + - stop + zigbee2mqtt: + button_short: + - 'on' + button_long: + - brightness_move_up + button_release: + - brightness_stop + button_short: '{{ actions_mapping[integration_id]["button_short"] }}' + button_long: '{{ actions_mapping[integration_id]["button_long"] }}' + button_release: '{{ actions_mapping[integration_id]["button_release"] }}' + controller_entity: !input controller_entity + controller_device: !input controller_device + controller_id: '{% if integration_id=="zigbee2mqtt" %}{{controller_entity}}{% else + %}{{controller_device}}{% endif %}' +mode: restart +max_exceeded: silent +trigger: +- platform: event + event_type: state_changed + event_data: + entity_id: !input controller_entity +- platform: event + event_type: + - deconz_event + - zha_event + event_data: + device_id: !input controller_device +condition: +- condition: and + conditions: + - '{%- set trigger_action -%} {%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state + }} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%- + elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length + > 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%} {%- endset -%} {{ trigger_action + not in ["","None"] }}' + - '{{ integration_id != "zigbee2mqtt" or trigger.event.data.new_state.state != trigger.event.data.old_state.state + }}' +action: +- delay: + milliseconds: !input helper_debounce_delay +- variables: + trigger_action: '{%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state + }} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%- + elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length + > 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%}' + trigger_delta: '{{ (as_timestamp(now()) - ((states(helper_last_controller_event) + | from_json).t if helper_last_controller_event is not none and (states(helper_last_controller_event) + | regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else as_timestamp("1970-01-01 + 00:00:00"))) * 1000 }}' +- service: input_text.set_value + data: + entity_id: !input helper_last_controller_event + value: '{{ {"a":trigger_action,"t":as_timestamp(now())} | to_json }}' +- choose: + - conditions: '{{ trigger_action | string in button_short }}' + sequence: + - choose: + - conditions: '{{ button_double_press }}' + sequence: + - choose: + - conditions: '{{ trigger_action | string in states(helper_last_controller_event) + and trigger_delta | int <= helper_double_press_delay | int }}' + sequence: + - service: input_text.set_value + data: + entity_id: !input helper_last_controller_event + value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json + }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_double + - choose: + - conditions: [] + sequence: !input action_button_double + default: + - delay: + milliseconds: '{{ adjusted_double_press_delay }}' + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_short + - choose: + - conditions: [] + sequence: !input action_button_short + default: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_short + - choose: + - conditions: [] + sequence: !input action_button_short + - conditions: '{{ trigger_action | string in button_long }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_long + - choose: + - conditions: '{{ button_long_loop }}' + sequence: + - repeat: + while: '{{ repeat.index < button_long_max_loop_repeats | int }}' + sequence: !input action_button_long + default: !input action_button_long + - conditions: '{{ trigger_action | string in button_release }}' + sequence: + - event: ahb_controller_event + event_data: + controller: '{{ controller_id }}' + action: button_release + - choose: + - conditions: [] + sequence: !input action_button_release diff --git a/blueprints/automation/homeassistant/motion_light.yaml b/blueprints/automation/homeassistant/motion_light.yaml new file mode 100644 index 0000000..5b389a3 --- /dev/null +++ b/blueprints/automation/homeassistant/motion_light.yaml @@ -0,0 +1,55 @@ +blueprint: + name: Motion-activated Light + description: Turn on a light when motion is detected. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + author: Home Assistant + input: + motion_entity: + name: Motion Sensor + selector: + entity: + domain: binary_sensor + device_class: motion + light_target: + name: Light + selector: + target: + entity: + domain: light + no_motion_wait: + name: Wait time + description: Time to leave the light on after last motion is detected. + default: 120 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds + +# If motion is detected within the delay, +# we restart the script. +mode: restart +max_exceeded: silent + +trigger: + platform: state + entity_id: !input motion_entity + from: "off" + to: "on" + +action: + - alias: "Turn on the light" + service: light.turn_on + target: !input light_target + - alias: "Wait until there is no motion from device" + wait_for_trigger: + platform: state + entity_id: !input motion_entity + from: "on" + to: "off" + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + service: light.turn_off + target: !input light_target diff --git a/blueprints/automation/homeassistant/notify_leaving_zone.yaml b/blueprints/automation/homeassistant/notify_leaving_zone.yaml new file mode 100644 index 0000000..0798a05 --- /dev/null +++ b/blueprints/automation/homeassistant/notify_leaving_zone.yaml @@ -0,0 +1,47 @@ +blueprint: + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + author: Home Assistant + input: + person_entity: + name: Person + selector: + entity: + domain: person + zone_entity: + name: Zone + selector: + entity: + domain: zone + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + +trigger: + platform: state + entity_id: !input person_entity + +variables: + zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. + zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" + +condition: + condition: template + # The first case handles leaving the Home zone which has a special state when zoning called 'home'. + # The second case handles leaving all other zones. + value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +action: + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/blueprints/script/homeassistant/confirmable_notification.yaml b/blueprints/script/homeassistant/confirmable_notification.yaml new file mode 100644 index 0000000..37e0435 --- /dev/null +++ b/blueprints/script/homeassistant/confirmable_notification.yaml @@ -0,0 +1,85 @@ +blueprint: + name: Confirmable Notification + description: >- + A script that sends an actionable notification with a confirmation before + running the specified action. + domain: script + source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml + author: Home Assistant + input: + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + title: + name: "Title" + description: "The title of the button shown in the notification." + default: "" + selector: + text: + message: + name: "Message" + description: "The message body" + selector: + text: + confirm_text: + name: "Confirmation Text" + description: "Text to show on the confirmation button" + default: "Confirm" + selector: + text: + confirm_action: + name: "Confirmation Action" + description: "Action to run when notification is confirmed" + default: [] + selector: + action: + dismiss_text: + name: "Dismiss Text" + description: "Text to show on the dismiss button" + default: "Dismiss" + selector: + text: + dismiss_action: + name: "Dismiss Action" + description: "Action to run when notification is dismissed" + default: [] + selector: + action: + +mode: restart + +sequence: + - alias: "Set up variables" + variables: + action_confirm: "{{ 'CONFIRM_' ~ context.id }}" + action_dismiss: "{{ 'DISMISS_' ~ context.id }}" + - alias: "Send notification" + domain: mobile_app + type: notify + device_id: !input notify_device + title: !input title + message: !input message + data: + actions: + - action: "{{ action_confirm }}" + title: !input confirm_text + - action: "{{ action_dismiss }}" + title: !input dismiss_text + - alias: "Awaiting response" + wait_for_trigger: + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_confirm }}" + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_dismiss }}" + - choose: + - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" + sequence: !input confirm_action + - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" + sequence: !input dismiss_action diff --git a/button_card_templates/base.yaml b/button_card_templates/base.yaml new file mode 100644 index 0000000..680b69b --- /dev/null +++ b/button_card_templates/base.yaml @@ -0,0 +1,11 @@ +base: + variables: + state: > + [[[ return entity === undefined || entity.state; ]]] + entity_id: > + [[[ return entity === undefined || entity.entity_id ]]] + entity_picture: > + [[[ return entity === undefined || entity.attributes.entity_picture; ]]] + aspect-ratio: 1/1 + show_state: true + show_icon: false diff --git a/button_card_templates/card.yaml b/button_card_templates/card.yaml new file mode 100644 index 0000000..da825e7 --- /dev/null +++ b/button_card_templates/card.yaml @@ -0,0 +1,140 @@ +card: + show_state: true + state_display: > + [[[ + var state = entity ? entity.state : null; + + return state + ? `${state.charAt(0).toUpperCase()}${state.slice(1)}` + : null; + ]]] + variables: + color: "yellow" + tap_action: + action: toggle + double_tap_action: + action: more-info + hold_action: + action: more-info + styles: + grid: + - grid-template-columns: 38px fit-content(100%) + - grid-template-areas: | + "i n" + "i s" + - grid-template-rows: 19px 19px + - column-gap: 8px + - row-gap: 0px + card: + - background: var(--sidebar-background-color) + - box-sizing: border-box + - box-shadow: none + - border-radius: 8px + - padding: 12px + - margin-bottom: 8px + img_cell: + - border-radius: 4px + - background-color: > + [[[ + if (entity.state !== 'off') { + var color_set = entity.attributes.rgb_color ? entity.attributes.rgb_color : variables.color; + var color = `rgba(var(--color-${color_set}), 0.15)`; + + if (entity.attributes.rgb_color) { + color = `rgba(${color_set}, 0.15)`; + } + + return color; + } + + return `var(--sidebar-border-color)`; + ]]] + - display: flex + - align-items: center + - justify-content: center + - flex-shrink: 0 + - height: 38px + - width: 38px + - transition: background-color 350ms ease-in-out + icon: + - display: flex + - align-items: center + - justify-content: center + - height: 24px + - width: 24px + - color: > + [[[ + if (entity.state !== 'off') { + var color_set = entity.attributes.rgb_color ? entity.attributes.rgb_color : variables.color; + var color = `rgba(var(--color-${color_set}), 1)`; + + if (entity.attributes.rgb_color) { + color = `rgba(${color_set}, 1)`; + } + + return color; + } + + return `var(--secondary-text-color)`; + ]]] + - transition: color 350ms ease-in-out + name: + - font-weight: 600 + - font-size: 14px + - text-align: left + state: + - font-weight: 600 + - font-size: 12px + - color: var(--secondary-text-color) + - text-align: left + - width: 100% + +card_contact_sensor: + template: + - card + state_display: > + [[[ + return entity && entity.state === 'on' + ? "Open" + : "Closed" + ]]] + +card_power_outlet: + variables: + power_outlet_icon: '[[[ return entity.attributes.icon ]]]' + power_outlet_name: '[[[ return entity.attributes.friendly_name ]]]' + power_outlet_consumption_sensor: + state: + - operator: "template" + value: "test" + custom_fields: + item1: + card: + type: "custom:button-card" + custom_fields: + item1: + card: + type: "custom:button-card" + entity: '[[[ return entity.entity_id ]]]' + icon: '[[[ return variables.power_outlet_icon ]]]' + item2: + card: + type: "custom:button-card" + entity: '[[[ return entity.entity_id ]]]' + name: '[[[ return variables.power_outlet_name ]]]' + label: |- + [[[ + if (entity.state === 'on' && variables.power_outlet_consumption_sensor !== null) { + return `${entity.state} • ${variables.power_outlet_consumption_sensor}`; + } else { + return entity.state; + } + ]]] + # state_display: > + # [[[ + # var state = entity ? entity.state : null; + + # return state + # ? `${state.charAt(0).toUpperCase()}${state.slice(1)}` + # : null; + # ]]] diff --git a/button_card_templates/clock.yaml b/button_card_templates/clock.yaml new file mode 100644 index 0000000..bebb3fc --- /dev/null +++ b/button_card_templates/clock.yaml @@ -0,0 +1,15 @@ +clock: + show_state: true + show_icon: false + show_name: false + tap_action: + action: none + styles: + card: + - background: "transparent" + - box-shadow: "none" + - font-family: ui-rounded, system-ui, "SF Pro Text", Robot, sans-serif + - font-size: 64px + - font-weight: 500 + - padding: 0 + - line-height: 1 diff --git a/button_card_templates/date.yaml b/button_card_templates/date.yaml new file mode 100644 index 0000000..b2b850a --- /dev/null +++ b/button_card_templates/date.yaml @@ -0,0 +1,20 @@ +date: + show_icon: false + tap_action: + action: none + styles: + card: + - background: "transparent" + - box-shadow: "none" + - font-family: ui-rounded, system-ui, "SF Pro Text", Robot, sans-serif + - font-size: 20px + - font-weight: 500 + - padding: 0 + triggers_update: "all" + name: > + [[[ + const event = new Date(); + const options = {weekday: 'long', day: 'numeric', month: 'long', }; + let formatted_date = event.toLocaleDateString("en", options); + return formatted_date; + ]]] diff --git a/button_card_templates/footer.yaml b/button_card_templates/footer.yaml new file mode 100644 index 0000000..a19a0b5 --- /dev/null +++ b/button_card_templates/footer.yaml @@ -0,0 +1,26 @@ +footer: + show_state: false + show_icon: false + tap_action: + action: none + name: > + [[[ + if (!entity) { + let attr = []; + + for (let [k, value] of Object.entries(entity.attributes)) { + value !== false && (attr += `
${value}
`); + } + + return attr; + } + ]]] + styles: + card: + - border-radius: 0 + - height: 64px + - min-height: 64px + - margin: 0 + - flex-direction: row + - padding: 0 + - border-right: none diff --git a/button_card_templates/icons.yaml b/button_card_templates/icons.yaml new file mode 100644 index 0000000..da28730 --- /dev/null +++ b/button_card_templates/icons.yaml @@ -0,0 +1,44 @@ +icon_lightning: + custom_fields: + icon: > + + +icon_trash_can: + custom_fields: + icon: > + + +icon_homepod_mini: + custom_fields: + icon: > + + +icon_media_play_pause: + custom_fields: + icon: > + + + # icon: > + # [[[ + # if (variables.state && variables.state == 'playing') { + # return ` + # + # `; + # } else { + # return ` + # + # `; + # } + # ]]] diff --git a/button_card_templates/light.yaml b/button_card_templates/light.yaml new file mode 100644 index 0000000..83ed68d --- /dev/null +++ b/button_card_templates/light.yaml @@ -0,0 +1,75 @@ +card_light: + show_icon: false + show_name: false + show_state: false + variables: + name: "[[[ return entity && entity.attributes.friendly_name; ]]]" + icon: "[[[ return entity && entity.attributes.icon; ]]]" + color: "yellow" + tap_action: + action: toggle + hold_action: + action: more-info + state: + - operator: "template" + value: "[[[ return variables.active_state ]]]" + styles: + card: + - background-color: > + [[[ + var color_set = entity.attributes.rgb_color ? entity.attributes.rgb_color : variables.color; + var color = `rgba(var(--color-${color_set}), 0.25)`; + + if (entity.attributes.rgb_color) { + color = `rgba(${color_set}, 0.25)`; + } + + return color; + ]]] + styles: + grid: + - grid-template-areas: > + "item1" + - grid-template-columns: 1fr + - grid-template-rows: min-content + - row-gap: 12px + card: + - border-radius: 8px + - box-sizing: border-box + - box-shadow: none + - padding: 12px + custom_fields: + item1: + card: + type: "custom:button-card" + custom_fields: + item1: + card: + type: "custom:button-card" + entity: "[[[ return entity.entity_id ]]]" + icon: "[[[ return variables.icon ]]]" + styles: + icon: + - color: > + [[[ + var color_set = entity.attributes.rgb_color ? entity.attributes.rgb_color : variables.color; + var color = `rgba(var(--color-${color_set}), 1)`; + + if (entity.attributes.rgb_color) { + color = `rgba(${color_set}, 1)`; + } + + return color; + ]]] + img_cell: + - background-color: > + [[[ + var color_set = entity.attributes.rgb_color ? entity.attributes.rgb_color : variables.color; + var color = `rgba(var(--color-${color_set}), 0.25)`; + + if (entity.attributes.rgb_color) { + color = `rgba(${color_set}, 0.25)`; + } + + return color; + ]]] diff --git a/button_card_templates/media.yaml b/button_card_templates/media.yaml new file mode 100644 index 0000000..ac0b962 --- /dev/null +++ b/button_card_templates/media.yaml @@ -0,0 +1,470 @@ +base_media: + show_icon: false + show_name: false + show_state: false + variables: + media_on: > + [[[ return !entity || ['on', 'playing'].indexOf(entity.state) !== -1; ]]] + media_off: > + [[[ return !entity || ['off', 'standby', 'unknown', 'unavailable', 'paused', 'idle'].indexOf(entity.state) !== -1; ]]] + media_icon: > + [[[ return entity && entity.attributes.media_icon; ]]] + media_source: > + [[[ return entity && entity.attributes.media_source; ]]] + tap_action: + action: call-service + service: > + [[[ + return variables.media_on + ? 'media_player.media_play_pause' + : 'media_player.toggle' + ]]] + service_data: + entity_id: '[[[ return variables.entity_id; ]]]' + double_tap_action: + action: more-info + hold_action: + action: call-service + service: media_player.turn_off + service_data: + entity_id: '[[[ return variables.entity_id; ]]]' + styles: + grid: + - box-sizing: border-box + - grid-template-areas: | + "metadata controls extracontrols" + - grid-template-columns: repeat(3, minmax(0, 1fr)) + - grid-template-rows: auto + - gap: 0px + - height: 120px + - max-height: 120px + - padding: 0px + card: + - background: var(--sidebar-background-color) + - border-top: 1px solid var(--sidebar-border-color) + - border-radius: 0px + - box-shadow: none + - box-sizing: border-box + - height: 120px + - max-height: 120px + - padding: 0px + custom_fields: + metadata: + card: + type: "custom:button-card" + entity: '[[[ return variables.entity_picture; ]]]' + variables: + media_icon: '[[[ return variables.media_icon; ]]]' + media_source: '[[[ return variables.media_source; ]]]' + entity_picture: '[[[ return variables.entity_picture; ]]]' + template: + - section_media + - metadata_media + styles: + grid: + - padding: 0px + controls: + card: + type: "custom:button-card" + variables: + entity_id: '[[[ return variables.entity_id ]]]' + template: + - section_media + - controls_media + extracontrols: + card: + type: "custom:button-card" + template: + - section_media + +section_media: + show_name: false + show_state: false + show_icon: false + styles: + grid: + - box-sizing: border-box + - padding: 12px + - overflow: hidden + card: + - box-sizing: border-box + - border-radius: 0px + - box-shadow: none + - height: 120px + - padding: 0px + - overflow: hidden + +metadata_media: + tap_action: + action: none + double_tap_action: + action: none + hold_action: + action: none + styles: + grid: + - box-sizing: border-box + - grid-template-areas: | + "artwork information" + - grid-template-columns: 96px auto + - gap: 16px + - padding: 12px + - overflow: hidden + - width: 100% + card: + - box-sizing: border-box + - overflow: hidden + - padding: 12px + custom_fields: + artwork: + - box-sizing: border-box + - overflow: hidden + information: + - box-sizing: border-box + - overflow: hidden + - width: 100% + custom_fields: + artwork: + card: + type: "custom:button-card" + entity: '[[[ return entity.entity_id; ]]]' + variables: + media_icon: '[[[ return variables.media_icon; ]]]' + media_source: '[[[ return variables.media_source; ]]]' + entity_picture: '[[[ return variables.entity_picture; ]]]' + template: + - artwork_media + information: + card: + type: "custom:button-card" + entity: '[[[ return entity.entity_id; ]]]' + variables: + media_icon: '[[[ return variables.media_icon; ]]]' + media_source: '[[[ return variables.media_source; ]]]' + entity_picture: '[[[ return variables.entity_picture; ]]]' + template: + - information_media + +artwork_media: + show_name: false + show_state: false + icon: '[[[ return variables.media_icon ]]]' + styles: + card: + - aspect-ratio: 1 + - background-image: > + [[[ + if ( + entity + && states[variables.entity_picture].attributes.entity_picture + && states[variables.entity_picture].attributes.entity_picture !== '' + ) { + let artwork = states[variables.entity_picture].attributes.entity_picture; + + if (artwork.includes('cache=https://')) { + artwork = artwork.split('cache=').pop(); + } + + artwork = artwork.replace('{w}x{h}{c}.{f}', '300x300bb.jpg'); + artwork = artwork.replace('{w}x{h}bb.{f}', '300x300bb.jpg'); + + return `url('${artwork}')`; + } + + return 'none'; + ]]] + - background-size: cover + - background-position: center + - background-repeat: no-repeat + - background-color: var(--primary-background-color) + - box-sizing: border-box + - border: 1px solid rgba(255, 255, 255, 0.5) + - border-radius: 8px + - box-shadow: none + - height: 96px + - width: 96px + icon: + - color: var(--secondary-text-color) + - display: > + [[[ + if ( + entity + && variables.entity_picture + && states[variables.entity_picture].attributes.entity_picture + && states[variables.entity_picture].attributes.entity_picture !== '' + && states[variables.entity_picture].attributes.entity_picture !== undefined + ) { + return 'none'; + } + + return 'initial'; + ]]] + +information_media: + show_name: false + show_state: false + show_icon: false + styles: + grid: + - grid-template-areas: | + "mediatitle" + "mediaartist" + "mediasource" + - grid-template-columns: 100% + - grid-template-rows: auto + - gap: 2px + - overflow: hidden + - width: 100% + card: + - background: transparent + - box-sizing: border-box + - border-radius: 0px + - box-shadow: none + - padding: 0px + - overflow: hidden + custom_fields: + mediatitle: + card: + type: "custom:button-card" + variables: + field_text: '[[[ return entity.attributes.media_title; ]]]' + template: + - marquee_field + styles: + grid: + - height: 20px + state: + - color: var(--primary-text-color) + - font-size: 16px + - font-weight: 600 + mediaartist: + card: + type: "custom:button-card" + variables: + field_text: > + [[[ + if (entity.attributes.media_album) { + return `${entity.attributes.media_artist} – ${entity.attributes.media_album}`; + } + + return entity.attributes.media_artist; + ]]] + template: + - marquee_field + mediasource: + card: + type: "custom:button-card" + icon: '[[[ return states[variables.media_icon].attributes.media_icon; ]]]' + name: '[[[ return states[variables.media_source].attributes.media_source; ]]]' + styles: + card: + - margin-top: 2px + name: + - color: var(--secondary-text-color) + - font-size: 10px + - font-weight: 600 + - text-transform: uppercase + template: + - information_item_media + +information_item_media: + styles: + grid: + - box-sizing: border-box + - display: flex + - justify-content: flex-start + - text-align: left + - gap: 2px + - width: 100% + icon: + - color: var(--secondary-text-color) + - height: 24px + - width: 24px + img_cell: + - display: flex + - align-items: center + - justify-content: center + - flex-shrink: 0 + - width: 32px + state: + - width: auto + - white-space: nowrap + card: + - box-sizing: border-box + - border-radius: 0px + - box-shadow: none + - padding: 0px + - overflow: hidden + - width: 100% + +marquee_field: + show_name: false + show_icon: false + show_state: true + variables: + field_text: + state_display: > + [[[ + if (variables.field_text && variables.field_text !== null) { + const parentElem = this.shadowRoot; + const fieldText = variables.field_text; + + let output = fieldText; + + function marquee() { + const stateEl = parentElem.getElementById("state"); + const containerEl = parentElem.getElementById("container"); + + if (stateEl && containerEl) { + stateEl.innerHTML = output; + + stateEl.classList.remove("ellipsis"); + + const resizeObserver = new ResizeObserver(entries => { + const spacer = " ".repeat(3); + const state = entries[0]; + const container = entries[1]; + const resize = state && state.contentRect && + container && container.contentRect && + state.contentRect.width !== 0 && + container.contentRect.width !== 0; + + if (resize && state.contentRect.width < container.contentRect.width) { + stateEl.classList.remove("marquee"); + } else if (resize && state.contentRect.width >= container.contentRect.width) { + stateEl.innerHTML = `${output} ${spacer} ${output} ${spacer} `; + stateEl.classList.add("marquee"); + } + }); + + resizeObserver.observe(stateEl); + resizeObserver.observe(containerEl); + } + } + + setTimeout(marquee, 100); + + return output; + } + + return null; + ]]] + styles: + grid: + - box-sizing: border-box + - grid-template-areas: | + "s" + - grid-template-columns: 1fr + - position: relative + - overflow: hidden + - height: 16px + card: + - box-sizing: border-box + - border-radius: 0px + - box-shadow: none + - display: block + - padding: 0px + state: + - box-sizing: border-box + - overflow: visible + - text-overflow: clip + - white-space: nowrap + - display: flex + - align-self: center + - justify-self: flex-start + - position: absolute + - text-align: left + - left: "0" + - width: auto + - min-width: auto + - max-width: 1000000px + - color: var(--secondary-text-color) + - font-size: 14px + - font-weight: 500 + - padding-left: 4px + extra_styles: | + .marquee { + animation: marquee 20s linear infinite; + text-overflow: clip !important; + overflow: inital !important; + } + + @keyframes marquee { + from { + transform: translateX(0%); + } + + to { + transform: translateX(-50%); + } + } + + +controls_media: + styles: + grid: + - display: flex + - align-items: center + - justify-content: center + - gap: 16px + custom_fields: + previous: + card: + type: "custom:button-card" + icon: mdi:skip-previous + template: + - controls_button_media + tap_action: + action: call-service + service: media_player.media_previous_track + service_data: + entity_id: '[[[ return variables.entity_id ]]]' + play_pause: + card: + type: "custom:button-card" + template: + - controls_button_media + icon: '[[[ return states[variables.entity_id].state === "playing" ? "mdi:pause" : "mdi:play" ]]]' + tap_action: + action: call-service + service: media_player.media_play_pause + service_data: + entity_id: '[[[ return variables.entity_id ]]]' + next: + card: + type: "custom:button-card" + icon: mdi:skip-next + template: + - controls_button_media + tap_action: + action: call-service + service: media_player.media_next_track + service_data: + entity_id: '[[[ return variables.entity_id ]]]' + +controls_button_media: + styles: + grid: + - grid-template-areas: "icon" + - grid-template-columns: 48px + - grid-template-rows: 48px + card: + - border-radius: 6px + - box-shadow: none + - box-sizing: border-box + - padding: 0px + icon: + - display: flex + - align-items: center + - justify-content: center + - height: 48px + - width: 48px + +conditional_media: + template: + - base + - base_media + variables: + media_source: '[[[ return variables.media_source ]]]' + entity_picture: '[[[ return entity.entity_id ]]]' + entity_id: '[[[ return entity.entity_id ]]]' diff --git a/button_card_templates/media_player.yaml b/button_card_templates/media_player.yaml new file mode 100644 index 0000000..58297c7 --- /dev/null +++ b/button_card_templates/media_player.yaml @@ -0,0 +1,416 @@ +base_media_player: + variables: + media_on: > + [[[ return !entity || ['playing'].indexOf(entity.state) !== -1; ]]] + media_off: > + [[[ return !entity || ['off', 'idle', 'standby', 'unknown', 'unavailable', 'paused'].indexOf(entity.state) !== -1; ]]] + +reset_media_player_style: + styles: + card: + - background: transparent + - padding: 0 + - box-shadow: none + - border-radius: 0 + +artwork_media_player: + show_name: false + show_state: false + show_icon: > + [[[ + if (entity && variables.entity_picture) { + return false; + } else { + return true; + } + ]]] + template: + - base + - reset_media_player_style + styles: + grid: + - display: flex + - align-items: center + - justify-content: center + card: + # - background-color: var(--primary-background-color) + - background-image: > + [[[ + console.log(variables.entity_picture) + console.log(entity) + let entity_picture = entity === undefined || entity.attributes.entity_picture; + + console.log(entity_picture) + if (entity_picture) { + let picture = entity_picture.replace('{w}x{h}{c}.{f}', '300x300bb.jpg'); + + if (entity_picture.includes('cache=https://')) { + picture = picture.split('cache=').pop(); + } + + console.log(picture); + return `url('${picture}')`; + } else { + return 'none'; + } + ]]] + - background-color: var(--sidebar-border-color) + - background-size: cover + - background-position: center + - background-repeat: none + - height: 96px + - width: 96px + - border-radius: 8px + - box-shadow: none + - border: 1px solid rgba(0, 0, 0, .25) + - box-sizing: border-box + icon: + - color: var(--secondary-text-color) + - opacity: > + [[[ + if (variables.entity_picture) { + return '0' + } + + return '0.5'; + ]]] + +info_media_player: + show_name: false + show_state: false + show_icon: false + triggers_update: "all" + styles: + grid: + - display: flex + - flex-direction: column + - align-items: flex-start + - gap: 2px + card: + - background: transparent + - padding: 0 + - box-shadow: none + - border-radius: 0 + custom_fields: + media_title: + card: + type: "custom:button-card" + template: reset_media_player_style + show_icon: false + show_state: false + styles: + name: + - font-family: var(--primary-font-family) + - font-size: 16px + - font-weight: 600 + - color: var(--primary-text-color) + - padding-left: 2px + entity: > + [[[ return entity.entity_id ]]] + name: > + [[[ return entity && entity.attributes.media_title ]]] + media_artist: + card: + type: "custom:button-card" + template: reset_media_player_style + show_icon: false + show_state: false + styles: + name: + - font-family: var(--primary-font-family) + - font-size: 14px + - font-weight: 500 + - color: var(--secondary-text-color) + - padding-left: 2px + entity: > + [[[ return entity.entity_id ]]] + name: > + [[[ return entity && entity.attributes.media_artist ]]] + media_source: + card: + type: "custom:button-card" + template: reset_media_player_style + show_icon: true + show_state: false + styles: + grid: + - display: flex + - gap: 6px + - justify-content: center + - align-items: center + img_cell: + - display: flex + - align-items: center + - justify-content: center + - height: 24px + - width: 24px + icon: + - display: flex + - align-items: center + - justify-content: center + - color: var(--secondary-text-color) + - height: 20px + - width: 20px + name: + - font-family: var(--primary-font-family) + - font-size: 10px + - font-weight: 600 + - letter-spacing: .5px + - line-height: 24px + - align-self: center + - color: var(--secondary-text-color) + - text-transform: uppercase + card: + - margin-top: 4px + entity: > + [[[ return entity.entity_id ]]] + +metadata_media_player: + show_name: false + show_state: false + show_icon: false + triggers_update: "all" + tap_action: + action: none + styles: + grid: + - display: flex + - gap: 16px + card: + - background: transparent + - padding: 0 + - box-shadow: none + - border-radius: 0 + custom_fields: + artwork: + card: + type: "custom:button-card" + template: artwork_media_player + # variables: + # entity_picture: > + # [[[ return states[entity.entity_id].attributes.entity_picture ]]] + info: + card: + type: "custom:button-card" + template: info_media_player + entity: > + [[[ return entity.entity_id ]]] + +controls_button_media_player: + triggers_update: "all" + show_name: false + show_state: false + show_icon: true + tap_action: + action: toggle + styles: + grid: + - display: flex + - align-items: center + - justify-content: center + card: + - background: transparent + - box-shadow: none + - padding: 0px + - height: 48px + - width: 48px + - pointer-events: initial !important + icon: + - height: 32px + - width: 32px + +controls_media_player: + template: + - base + - base_media_player + - reset_media_player_style + show_icon: false + show_name: false + show_state: false + triggers_update: "all" + tap_action: + action: call-service + service: media_player.media_play_pause + service_data: + entity_id: > + [[[ return variables.entity_id; ]]] + styles: + grid: + - display: flex + - align-items: center + - justify-content: center + - gap: 10px + - padding: 10px + card: + - background: transparent + - box-shadow: none + - padding: 0px + custom_fields: + previous: + card: + type: "custom:button-card" + template: + - controls_button_media_player + icon: mdi:skip-previous + show_name: false + show_state: false + tap_action: + action: call-service + service: media_player.media_previous_track + service_data: + entity_id: > + [[[ return entity.entity_id ]]] + play_pause: + card: + type: "custom:button-card" + template: + - base + - base_media_player + - controls_button_media_player + icon: > + [[[ + return entity && variables.media_on + ? 'mdi:pause' + : 'mdi:play-outline' + ]]] + show_name: false + show_state: false + tap_action: + action: call-service + service: media_player.media_play_pause + service_data: + entity_id: > + [[[ return entity.entity_id ]]] + next: + card: + type: "custom:button-card" + template: + - controls_button_media_player + icon: mdi:skip-next + show_name: false + show_state: false + tap_action: + action: call-service + service: media_player.media_next_track + service_data: + entity_id: > + [[[ return entity.entity_id ]]] + +extra_controls_media_player: + tap_action: + action: toggle + template: + - base + - base_media_player + - reset_media_player_style + show_icon: false + show_name: false + show_state: false + triggers_update: "all" + styles: + grid: + - display: flex + - align-items: center + - justify-content: flex-end + - gap: 10px + - padding: 10px + card: + - background: transparent + - box-shadow: none + - padding: 0px + custom_fields: + shuffle: + card: + type: "custom:button-card" + template: + - controls_button_media_player + icon: mdi:repeat + show_name: false + show_state: false + tap_action: + action: call-service + service: media_player.repeat_set + service_data: + entity_id: > + [[[ return entity.entity_id ]]] + repeat: + card: + type: "custom:button-card" + template: + - base + - base_media_player + - controls_button_media_player + icon: mdi:shuffle-variant + show_name: false + show_state: false + tap_action: + action: call-service + service: media_player.shuffle_set + service_data: + entity_id: > + [[[ return entity.entity_id ]]] + +conditional_media_player: + template: + - base + - base_media_player + show_name: false + show_state: false + show_icon: false + triggers_update: "all" + tap_action: + action: none + styles: + grid: + - box-sizing: border-box + - grid-template-areas: | + "metadata controls extra_controls" + - grid-template-columns: 1fr 1fr 1fr + - gap: 10px + - align-items: center + - justify-content: center + - max-height: 120px + - padding: 12px + card: + - background: var(--sidebar-background-color) + - border-top: 1px solid var(--sidebar-border-color) + - border-radius: 0px + - box-sizing: border-box + - box-shadow: none + - display: flex + - align-items: center + - justify-content: center + - max-height: 120px + - padding: 0px + img_cell: + - display: none + icon: + - display: none + name: + - display: none + state: + - display: none + custom_fields: + metadata: + card: + type: "custom:button-card" + entity: > + [[[ return entity.entity_id ]]] + variables: + entity_picture: sensor.active_media_players + template: + - metadata_media_player + controls: + card: + type: "custom:button-card" + entity: > + [[[ return entity.entity_id ]]] + template: + - controls_media_player + extra_controls: + card: + type: "custom:button-card" + entity: > + [[[ return entity.entity_id ]]] + template: + - extra_controls_media_player diff --git a/button_card_templates/meteocard.yaml b/button_card_templates/meteocard.yaml new file mode 100644 index 0000000..9fd6517 --- /dev/null +++ b/button_card_templates/meteocard.yaml @@ -0,0 +1,122 @@ +meteocard: + show_name: true + show_state: true + show_icon: true + name: > + [[[ + if (entity) { + return entity.attributes.event; + } + + return 'No warnings'; + ]]] + state_display: > + [[[ + if (entity) { + return entity.attributes.description; + } + + return null; + ]]] + icon: > + [[[ + if (entity) { + const awarenessType = Number(entity.attributes.awareness_type.split(';')[0]); + + if (awarenessType === 1) { + return 'mdi:weather-windy'; + } else if (awarenessType === 2) { + return 'mdi:weather-snowy-heavy'; + } else if (awarenessType === 3) { + return 'mdi:weather-lightning'; + } else if (awarenessType === 4) { + return 'mdi:weather-fog'; + } else if (awarenessType === 5) { + return 'mdi:thermometer'; + } else if (awarenessType === 6) { + return 'mdi:snowflake'; + } else if (awarenessType === 7) { + return 'mdi:tsunami'; + } else if (awarenessType === 8) { + return 'mdi:pine-tree-fire'; + } else if (awarenessType === 9) { + return 'mdi:image-filter-hdr'; + } else if (awarenessType === 10) { + return 'mdi:weather-pouring'; + } else if (awarenessType === 11 || awarenessType === 12) { + return 'mdi:home-flood'; + } + + return "mdi:alert-circle-outline"; + } + + return null; + ]]] + styles: + grid: + - grid-template-areas: | + "i n" + "i s" + - grid-template-columns: 48px auto + - grid-template-rows: auto auto + - grid-column-gap: 0px + - grid-row-gap: 0px + card: + - background-color: > + [[[ + if (entity) { + const severity = entity.attributes.severity.toLowerCase(); + + if (severity === 'unknown' || severity === 'minor' || severity === 'moderate') { + return 'rgba(var(--color-yellow), 1)'; + } else if (severity === 'severe') { + return 'rgba(var(--color-orange), 1)'; + } else if (severity === 'high' || severity === 'extreme') { + return 'rgba(var(--color-red), 1)'; + } + + return 'var(--primary-background-color)'; + } + + return 'var(--primary-background-color)'; + ]]] + - box-sizing: border-box + # - box-shadow: none + - border-radius: 50px + - color: > + [[[ + if (entity) { + const severity = entity.attributes.severity.toLowerCase(); + + if (severity === 'unknown' || severity === 'minor' || severity === 'moderate' || severity === 'severe') { + return 'rgba(var(--color-black), 1)'; + } else if (severity === 'high' || severity === 'extreme') { + return 'rgba(var(--color-white), 1)'; + } + + return 'var(--primary-text-color)'; + } + + return 'var(--primary-text-color)'; + ]]] + - margin-top: 12px + - padding: 8px + img_cell: + - display: flex + - align-items: center + - justify-content: center + - height: 32px + - width: 32px + icon: + - height: 32px + - width: 32px + name: + - font-size: 12px + - font-weight: 600 + - text-align: left + - width: 100% + state: + - font-size: 10px + - font-weight: 400 + - text-align: left + - width: 100% diff --git a/button_card_templates/persons.yaml b/button_card_templates/persons.yaml new file mode 100644 index 0000000..8778f67 --- /dev/null +++ b/button_card_templates/persons.yaml @@ -0,0 +1,48 @@ +persons: + styles: + grid: + - display: flex + - justify-content: center + - gap: 8px + card: + - background: "transparent" + - border-radius: 0 + - box-shadow: none + +person: + show_name: false + show_state: false + template: + - base + triggers_update: sensor.time + styles: + custom_fields: + icon: + - clip-path: circle() + - pointer-events: none + - height: 100% + - aspect-ratio: 1 + - width: 100% + grid: + - display: flex + - align-items: center + - justify-content: center + card: + - max-width: 48px + - background: "transparent" + - box-shadow: "none" + - border-radius: 50% + - padding: 0px + - opacity: > + [[[ + return variables.state === "home" + ? 1 + : .5; + ]]] + custom_fields: + icon: > + [[[ + return entity && variables.entity_picture + ? `${subtitle}
+ ${output} + `; + } + ]]] diff --git a/button_card_templates/utilities.yaml b/button_card_templates/utilities.yaml new file mode 100644 index 0000000..1a7745d --- /dev/null +++ b/button_card_templates/utilities.yaml @@ -0,0 +1,121 @@ +utilities: + show_name: false + show_icon: false + show_state: false + + styles: + grid: + - grid-template-areas: | + "electricity waste" + - grid-template-columns: 1fr 1fr + - grid-gap: 1px + card: + - border-radius: 12px + - box-shadow: none + - font-family: ui-rounded, system-ui, "SF Pro Text", Roboto, sans-serif + - gap: 1px + - display: flex + - padding: 0 + - overflow: hidden + + custom_fields: + electricity: + card: + type: "custom:button-card" + show_icon: false + entity: sensor.nordpool_kwh_price + template: + - utility + - icon_lightning + waste: + card: + type: "custom:button-card" + show_icon: false + template: + - utility + - icon_trash_can + entity: sensor.template_waste_collection + +utility: + show_state: true + state_display: | + [[[ + if (entity) { + var value = states[entity.entity_id]; + + if ( + value + && value.attributes + && value.attributes.unit_of_measurement + && value.attributes.unit_of_measurement === "EUR/kWh") + { + var formattedState = Number(value.state); + return `${formattedState.toFixed(2)} €/kWh`; + } + + return `${value.state}`; + } + + return null; + ]]] + styles: + grid: + - box-sizing: border-box + - grid-template-columns: 32px auto + - grid-template-areas: | + "icon value" + "icon name" + - grid-template-rows: 19px 13px + - column-gap: 4px + - row-gap: 0px + - padding-right: 8px + - padding-left: 8px + - padding-top: 8px + - padding-bottom: 8px + + + img_cell: + - display: none + - box-sizing: border-box + + custom_fields: + icon: + - box-sizing: border-box + - color: "var(--secondary-text-color)" + - height: 32px + + state: + - box-sizing: border-box + - display: flex + - align-items: end; + - font-family: ui-rounded, system-ui, "SF Pro Text", Roboto, sans-serif + - color: "var(--primary-text-color)" + - grid-area: value + - font-size: 12px + - line-height: 15px + - font-weight: 600 + - text-align: left + - width: 100% + - align-self: end + + name: + - box-sizing: border-box + - font-family: ui-rounded, system-ui, "SF Pro Text", Roboto, sans-serif + - grid-area: name + - color: "var(--secondary-text-color)" + - font-size: 8px + - font-weight: 700 + - line-height: 11px + - text-align: left + - width: 100% + - align-self: start + + card: + - background: "var(--primary-background-color)" + - box-sizing: border-box + - display: flex + - border-radius: 0px + - box-shadow: none + - padding: 0 0 0 0 + - align-items: center + - justify-content: center diff --git a/configuration.yaml b/configuration.yaml new file mode 100644 index 0000000..1b2277a --- /dev/null +++ b/configuration.yaml @@ -0,0 +1,23 @@ +homeassistant: + name: Home + latitude: !secret zone_home_latitude + longitude: !secret zone_home_longitude + elevation: !secret zone_home_elevation + unit_system: metric + temperature_unit: C + time_zone: Europe/Berlin + language: en + country: DE + currency: EUR + customize: !include customize.yaml + customize_domain: + automation: + initial_state: true + packages: !include_dir_named integrations + allowlist_external_dirs: + - /config + + +# automation: !include automations.yaml +# script: !include scripts.yaml +# scene: !include scenes.yaml diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py new file mode 100644 index 0000000..3f6e874 --- /dev/null +++ b/custom_components/browser_mod/__init__.py @@ -0,0 +1,37 @@ +import logging + +from .store import BrowserModStore +from .mod_view import async_setup_view +from .connection import async_setup_connection +from .const import DOMAIN, DATA_BROWSERS, DATA_ADDERS, DATA_STORE +from .service import async_setup_services + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + + store = BrowserModStore(hass) + await store.load() + + hass.data[DOMAIN] = { + DATA_BROWSERS: {}, + DATA_ADDERS: {}, + DATA_STORE: store, + } + + return True + + +async def async_setup_entry(hass, config_entry): + + for domain in ["sensor", "binary_sensor", "light", "media_player", "camera"]: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + + await async_setup_connection(hass) + await async_setup_view(hass) + await async_setup_services(hass) + + return True diff --git a/custom_components/browser_mod/__pycache__/__init__.cpython-310.pyc b/custom_components/browser_mod/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..281c6f5 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/__init__.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/__init__.cpython-311.pyc b/custom_components/browser_mod/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1d7e332 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/__init__.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/binary_sensor.cpython-310.pyc b/custom_components/browser_mod/__pycache__/binary_sensor.cpython-310.pyc new file mode 100644 index 0000000..d4733fc Binary files /dev/null and b/custom_components/browser_mod/__pycache__/binary_sensor.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/binary_sensor.cpython-311.pyc b/custom_components/browser_mod/__pycache__/binary_sensor.cpython-311.pyc new file mode 100644 index 0000000..5ee7e18 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/binary_sensor.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/browser.cpython-310.pyc b/custom_components/browser_mod/__pycache__/browser.cpython-310.pyc new file mode 100644 index 0000000..f894485 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/browser.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/browser.cpython-311.pyc b/custom_components/browser_mod/__pycache__/browser.cpython-311.pyc new file mode 100644 index 0000000..5c9fcb9 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/browser.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/camera.cpython-310.pyc b/custom_components/browser_mod/__pycache__/camera.cpython-310.pyc new file mode 100644 index 0000000..7304af3 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/camera.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/camera.cpython-311.pyc b/custom_components/browser_mod/__pycache__/camera.cpython-311.pyc new file mode 100644 index 0000000..2ad2b28 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/camera.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/config_flow.cpython-310.pyc b/custom_components/browser_mod/__pycache__/config_flow.cpython-310.pyc new file mode 100644 index 0000000..49aa750 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/config_flow.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/config_flow.cpython-311.pyc b/custom_components/browser_mod/__pycache__/config_flow.cpython-311.pyc new file mode 100644 index 0000000..14fa0b4 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/config_flow.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/connection.cpython-310.pyc b/custom_components/browser_mod/__pycache__/connection.cpython-310.pyc new file mode 100644 index 0000000..87c8dee Binary files /dev/null and b/custom_components/browser_mod/__pycache__/connection.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/connection.cpython-311.pyc b/custom_components/browser_mod/__pycache__/connection.cpython-311.pyc new file mode 100644 index 0000000..a6ba851 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/connection.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/const.cpython-310.pyc b/custom_components/browser_mod/__pycache__/const.cpython-310.pyc new file mode 100644 index 0000000..eb4b9ff Binary files /dev/null and b/custom_components/browser_mod/__pycache__/const.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/const.cpython-311.pyc b/custom_components/browser_mod/__pycache__/const.cpython-311.pyc new file mode 100644 index 0000000..497a461 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/const.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/entities.cpython-310.pyc b/custom_components/browser_mod/__pycache__/entities.cpython-310.pyc new file mode 100644 index 0000000..f6020f0 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/entities.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/entities.cpython-311.pyc b/custom_components/browser_mod/__pycache__/entities.cpython-311.pyc new file mode 100644 index 0000000..cdb6588 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/entities.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/light.cpython-310.pyc b/custom_components/browser_mod/__pycache__/light.cpython-310.pyc new file mode 100644 index 0000000..a701a65 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/light.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/light.cpython-311.pyc b/custom_components/browser_mod/__pycache__/light.cpython-311.pyc new file mode 100644 index 0000000..fee3fb4 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/light.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/media_player.cpython-310.pyc b/custom_components/browser_mod/__pycache__/media_player.cpython-310.pyc new file mode 100644 index 0000000..0d5ea8c Binary files /dev/null and b/custom_components/browser_mod/__pycache__/media_player.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/media_player.cpython-311.pyc b/custom_components/browser_mod/__pycache__/media_player.cpython-311.pyc new file mode 100644 index 0000000..d48379f Binary files /dev/null and b/custom_components/browser_mod/__pycache__/media_player.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/mod_view.cpython-310.pyc b/custom_components/browser_mod/__pycache__/mod_view.cpython-310.pyc new file mode 100644 index 0000000..87cbfd1 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/mod_view.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/mod_view.cpython-311.pyc b/custom_components/browser_mod/__pycache__/mod_view.cpython-311.pyc new file mode 100644 index 0000000..34c0641 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/mod_view.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/sensor.cpython-310.pyc b/custom_components/browser_mod/__pycache__/sensor.cpython-310.pyc new file mode 100644 index 0000000..15c6f54 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/sensor.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/sensor.cpython-311.pyc b/custom_components/browser_mod/__pycache__/sensor.cpython-311.pyc new file mode 100644 index 0000000..c7edf2a Binary files /dev/null and b/custom_components/browser_mod/__pycache__/sensor.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/service.cpython-310.pyc b/custom_components/browser_mod/__pycache__/service.cpython-310.pyc new file mode 100644 index 0000000..d4fdbe6 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/service.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/service.cpython-311.pyc b/custom_components/browser_mod/__pycache__/service.cpython-311.pyc new file mode 100644 index 0000000..35e1ea1 Binary files /dev/null and b/custom_components/browser_mod/__pycache__/service.cpython-311.pyc differ diff --git a/custom_components/browser_mod/__pycache__/store.cpython-310.pyc b/custom_components/browser_mod/__pycache__/store.cpython-310.pyc new file mode 100644 index 0000000..48cebea Binary files /dev/null and b/custom_components/browser_mod/__pycache__/store.cpython-310.pyc differ diff --git a/custom_components/browser_mod/__pycache__/store.cpython-311.pyc b/custom_components/browser_mod/__pycache__/store.cpython-311.pyc new file mode 100644 index 0000000..511945a Binary files /dev/null and b/custom_components/browser_mod/__pycache__/store.cpython-311.pyc differ diff --git a/custom_components/browser_mod/binary_sensor.py b/custom_components/browser_mod/binary_sensor.py new file mode 100644 index 0000000..1ad13f6 --- /dev/null +++ b/custom_components/browser_mod/binary_sensor.py @@ -0,0 +1,59 @@ +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.entity import EntityCategory + +from .const import DOMAIN, DATA_ADDERS +from .entities import BrowserModEntity + + +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): + hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + await async_setup_platform(hass, {}, async_add_entities) + + +class BrowserBinarySensor(BrowserModEntity, BinarySensorEntity): + def __init__(self, coordinator, browserID, parameter, name, icon=None): + BrowserModEntity.__init__(self, coordinator, browserID, name, icon) + BinarySensorEntity.__init__(self) + self.parameter = parameter + + @property + def is_on(self): + return self._data.get("browser", {}).get(self.parameter, None) + + @property + def entity_category(self): + return EntityCategory.DIAGNOSTIC + + @property + def extra_state_attributes(self): + retval = super().extra_state_attributes + if self.parameter == "fullyKiosk": + retval["data"] = self._data.get("browser", {}).get("fully_data") + return retval + + +class ActivityBinarySensor(BrowserModEntity, BinarySensorEntity): + def __init__(self, coordinator, browserID): + BrowserModEntity.__init__(self, coordinator, browserID, None) + BinarySensorEntity.__init__(self) + + @property + def unique_id(self): + return f"{self.browserID}-activity" + + @property + def entity_registry_visible_default(self): + return True + + @property + def device_class(self): + return "motion" + + @property + def is_on(self): + return self._data.get("activity", False) diff --git a/custom_components/browser_mod/browser.py b/custom_components/browser_mod/browser.py new file mode 100644 index 0000000..0fdd846 --- /dev/null +++ b/custom_components/browser_mod/browser.py @@ -0,0 +1,216 @@ +import logging + +from homeassistant.components.websocket_api import event_message +from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.core import callback + +from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS +from .sensor import BrowserSensor +from .light import BrowserModLight +from .binary_sensor import BrowserBinarySensor, ActivityBinarySensor +from .media_player import BrowserModPlayer +from .camera import BrowserModCamera + +_LOGGER = logging.getLogger(__name__) + + +class Coordinator(DataUpdateCoordinator): + def __init__(self, hass, browserID): + super().__init__( + hass, + _LOGGER, + name="Browser Mod Coordinator", + ) + self.browserID = browserID + + +class BrowserModBrowser: + """A Browser_mod browser. + Handles the Home Assistant device corresponding to a registered Browser. + Creates and updates entities based on available data. + """ + + def __init__(self, hass, browserID): + self.browserID = browserID + self.coordinator = Coordinator(hass, browserID) + self.entities = {} + self.data = {} + self.settings = {} + self._connections = [] + + self.update_entities(hass) + + def update(self, hass, newData): + """Update state of all related entities.""" + self.data.update(newData) + self.update_entities(hass) + self.coordinator.async_set_updated_data(self.data) + + def update_settings(self, hass, settings): + """Update Browser settings and entities if needed.""" + self.settings = settings + self.update_entities(hass) + + def update_entities(self, hass): + """Create all entities associated with the browser.""" + + coordinator = self.coordinator + browserID = self.browserID + + def _assert_browser_sensor(type, name, *properties, **kwarg): + """Create a browser state sensor if it does not already exist""" + if name in self.entities: + return + adder = hass.data[DOMAIN][DATA_ADDERS][type] + cls = {"sensor": BrowserSensor, "binary_sensor": BrowserBinarySensor}[type] + new = cls(coordinator, browserID, name, *properties, **kwarg) + adder([new]) + self.entities[name] = new + + _assert_browser_sensor("sensor", "path", "Browser path", icon="mdi:web") + _assert_browser_sensor("sensor", "visibility", "Browser visibility") + _assert_browser_sensor( + "sensor", "userAgent", "Browser userAgent", icon="mdi:account-details" + ) + _assert_browser_sensor( + "sensor", "currentUser", "Browser user", icon="mdi:account" + ) + _assert_browser_sensor( + "binary_sensor", "fullyKiosk", "Browser FullyKiosk", icon="mdi:alpha-f" + ) + _assert_browser_sensor( + "sensor", "width", "Browser width", "px", icon="mdi:arrow-left-right" + ) + _assert_browser_sensor( + "sensor", "height", "Browser height", "px", icon="mdi:arrow-up-down" + ) + + # Don't create battery sensor unless battery level is reported + if self.data.get("browser", {}).get("battery_level", None) is not None: + _assert_browser_sensor( + "sensor", "battery_level", "Browser battery", "%", "battery" + ) + # Don't create a charging sensor unless charging state is reported + if self.data.get("browser", {}).get("charging", None) is not None: + _assert_browser_sensor( + "binary_sensor", "charging", "Browser charging", icon="mdi:power-plug" + ) + + _assert_browser_sensor( + "binary_sensor", + "darkMode", + "Browser dark mode", + icon="mdi:theme-light-dark", + ) + + if "activity" not in self.entities: + adder = hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] + new = ActivityBinarySensor(coordinator, browserID) + adder([new]) + self.entities["activity"] = new + + if "screen" not in self.entities: + adder = hass.data[DOMAIN][DATA_ADDERS]["light"] + new = BrowserModLight(coordinator, browserID, self) + adder([new]) + self.entities["screen"] = new + + if "player" not in self.entities: + adder = hass.data[DOMAIN][DATA_ADDERS]["media_player"] + new = BrowserModPlayer(coordinator, browserID, self) + adder([new]) + self.entities["player"] = new + + if "camera" not in self.entities and self.settings.get("camera"): + adder = hass.data[DOMAIN][DATA_ADDERS]["camera"] + new = BrowserModCamera(coordinator, browserID) + adder([new]) + self.entities["camera"] = new + if "camera" in self.entities and not self.settings.get("camera"): + er = entity_registry.async_get(hass) + er.async_remove(self.entities["camera"].entity_id) + del self.entities["camera"] + + hass.create_task( + self.send( + None, browserEntities={k: v.entity_id for k, v in self.entities.items()} + ) + ) + + @callback + async def send(self, command, **kwargs): + """Send a command to this browser.""" + if self.connection is None: + return + + for (connection, cid) in self.connection: + connection.send_message( + event_message( + cid, + { + "command": command, + **kwargs, + }, + ) + ) + + def delete(self, hass): + """Delete the device and associated entities.""" + dr = device_registry.async_get(hass) + er = entity_registry.async_get(hass) + + for e in self.entities.values(): + er.async_remove(e.entity_id) + + self.entities = {} + + device = dr.async_get_device({(DOMAIN, self.browserID)}) + dr.async_remove_device(device.id) + + @property + def connection(self): + """The current websocket connections for this Browser.""" + return self._connections + + def open_connection(self, hass, connection, cid): + """Add a websocket connection.""" + self._connections.append((connection, cid)) + self.update(hass, {"connected": True}) + + def close_connection(self, hass, connection): + """Close a websocket connection.""" + self._connections = list( + filter(lambda v: v[0] != connection, self._connections) + ) + self.update(hass, {"connected": False}) + + +def getBrowser(hass, browserID, *, create=True): + """Get or create browser by browserID.""" + browsers = hass.data[DOMAIN][DATA_BROWSERS] + if browserID in browsers: + return browsers[browserID] + + if not create: + return None + + browsers[browserID] = BrowserModBrowser(hass, browserID) + return browsers[browserID] + + +def deleteBrowser(hass, browserID): + """Delete a browser by BrowserID.""" + browsers = hass.data[DOMAIN][DATA_BROWSERS] + if browserID in browsers: + browsers[browserID].delete(hass) + del browsers[browserID] + + +def getBrowserByConnection(hass, connection): + """Get the browser that has a given connection open.""" + browsers = hass.data[DOMAIN][DATA_BROWSERS] + + for k, v in browsers.items(): + if any([c[0] == connection for c in v.connection]): + return v diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js new file mode 100644 index 0000000..76f3c33 --- /dev/null +++ b/custom_components/browser_mod/browser_mod.js @@ -0,0 +1,407 @@ +const e="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||"undefined"!=typeof global&&global;function t(e,t,i,o){var s,n=arguments.length,r=n<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,o);else for(var a=e.length-1;a>=0;a--)(s=e[a])&&(r=(n<3?s(r):n>3?s(t,i,r):s(t,i))||r);return n>3&&r&&Object.defineProperty(t,i,r),r}void 0!==e.EventTarget&&function(e){try{new e}catch(e){return!1}return!0}(e.EventTarget)||(e.EventTarget=function(){function e(){this.__listeners=new Map}return e.prototype=Object.create(Object.prototype),e.prototype.addEventListener=function(e,t,i){if(arguments.length<2)throw new TypeError(`TypeError: Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`);const o=this.__listeners,s=e.toString();o.has(s)||o.set(s,new Map);const n=o.get(s);n.has(t)||n.set(t,i)},e.prototype.removeEventListener=function(e,t,i){if(arguments.length<2)throw new TypeError(`TypeError: Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`);const o=this.__listeners,s=e.toString();if(o.has(s)){const e=o.get(s);e.has(t)&&e.delete(t)}},e.prototype.dispatchEvent=function(e){if(!(e instanceof Event))throw new TypeError("Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.");const t=e.type,i=this.__listeners.get(t);if(i)for(const[t,o]of i.entries()){try{"function"==typeof t?t.call(this,e):t&&"function"==typeof t.handleEvent&&t.handleEvent(e)}catch(e){setTimeout((()=>{throw e}))}o&&o.once&&i.delete(t)}return!0},e}());const i=window,o=i.ShadowRoot&&(void 0===i.ShadyCSS||i.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),n=new WeakMap;class r{constructor(e,t,i){if(this._$cssResult$=!0,i!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(o&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=n.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&n.set(t,e))}return e}toString(){return this.cssText}}const a=(e,...t)=>{const i=1===e.length?e[0]:t.reduce(((t,i,o)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[o+1]),e[0]);return new r(i,e,s)},d=o?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new r("string"==typeof e?e:e+"",void 0,s))(t)})(e):e;var l;const c=window,h=c.trustedTypes,u=h?h.emptyScript:"",p=c.reactiveElementPolyfillSupport,v={toAttribute(e,t){switch(t){case Boolean:e=e?u:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},m=(e,t)=>t!==e&&(t==t||e==e),_={attribute:!0,type:String,converter:v,reflect:!1,hasChanged:m};class g extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(e){var t;this.finalize(),(null!==(t=this.h)&&void 0!==t?t:this.h=[]).push(e)}static get observedAttributes(){this.finalize();const e=[];return this.elementProperties.forEach(((t,i)=>{const o=this._$Ep(i,t);void 0!==o&&(this._$Ev.set(o,i),e.push(o))})),e}static createProperty(e,t=_){if(t.state&&(t.attribute=!1),this.finalize(),this.elementProperties.set(e,t),!t.noAccessor&&!this.prototype.hasOwnProperty(e)){const i="symbol"==typeof e?Symbol():"__"+e,o=this.getPropertyDescriptor(e,i,t);void 0!==o&&Object.defineProperty(this.prototype,e,o)}}static getPropertyDescriptor(e,t,i){return{get(){return this[t]},set(o){const s=this[e];this[t]=o,this.requestUpdate(e,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)||_}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const e=Object.getPrototypeOf(this);if(e.finalize(),void 0!==e.h&&(this.h=[...e.h]),this.elementProperties=new Map(e.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const e=this.properties,t=[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)];for(const i of t)this.createProperty(i,e[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(d(e))}else void 0!==e&&t.push(d(e));return t}static _$Ep(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}u(){var e;this._$E_=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(e=this.constructor.h)||void 0===e||e.forEach((e=>e(this)))}addController(e){var t,i;(null!==(t=this._$ES)&&void 0!==t?t:this._$ES=[]).push(e),void 0!==this.renderRoot&&this.isConnected&&(null===(i=e.hostConnected)||void 0===i||i.call(e))}removeController(e){var t;null===(t=this._$ES)||void 0===t||t.splice(this._$ES.indexOf(e)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((e,t)=>{this.hasOwnProperty(t)&&(this._$Ei.set(t,this[t]),delete this[t])}))}createRenderRoot(){var e;const t=null!==(e=this.shadowRoot)&&void 0!==e?e:this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{o?e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):t.forEach((t=>{const o=document.createElement("style"),s=i.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=t.cssText,e.appendChild(o)}))})(t,this.constructor.elementStyles),t}connectedCallback(){var e;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostConnected)||void 0===t?void 0:t.call(e)}))}enableUpdating(e){}disconnectedCallback(){var e;null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostDisconnected)||void 0===t?void 0:t.call(e)}))}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$EO(e,t,i=_){var o;const s=this.constructor._$Ep(e,i);if(void 0!==s&&!0===i.reflect){const n=(void 0!==(null===(o=i.converter)||void 0===o?void 0:o.toAttribute)?i.converter:v).toAttribute(t,i.type);this._$El=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$El=null}}_$AK(e,t){var i;const o=this.constructor,s=o._$Ev.get(e);if(void 0!==s&&this._$El!==s){const e=o.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==(null===(i=e.converter)||void 0===i?void 0:i.fromAttribute)?e.converter:v;this._$El=s,this[s]=n.fromAttribute(t,e.type),this._$El=null}}requestUpdate(e,t,i){let o=!0;void 0!==e&&(((i=i||this.constructor.getPropertyOptions(e)).hasChanged||m)(this[e],t)?(this._$AL.has(e)||this._$AL.set(e,t),!0===i.reflect&&this._$El!==e&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(e,i))):o=!1),!this.isUpdatePending&&o&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var e;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((e,t)=>this[t]=e)),this._$Ei=void 0);let t=!1;const i=this._$AL;try{t=this.shouldUpdate(i),t?(this.willUpdate(i),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostUpdate)||void 0===t?void 0:t.call(e)})),this.update(i)):this._$Ek()}catch(e){throw t=!1,this._$Ek(),e}t&&this._$AE(i)}willUpdate(e){}_$AE(e){var t;null===(t=this._$ES)||void 0===t||t.forEach((e=>{var t;return null===(t=e.hostUpdated)||void 0===t?void 0:t.call(e)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(e){return!0}update(e){void 0!==this._$EC&&(this._$EC.forEach(((e,t)=>this._$EO(t,this[t],e))),this._$EC=void 0),this._$Ek()}updated(e){}firstUpdated(e){}}var w;g.finalized=!0,g.elementProperties=new Map,g.elementStyles=[],g.shadowRootOptions={mode:"open"},null==p||p({ReactiveElement:g}),(null!==(l=c.reactiveElementVersions)&&void 0!==l?l:c.reactiveElementVersions=[]).push("1.6.1");const b=window,y=b.trustedTypes,f=y?y.createPolicy("lit-html",{createHTML:e=>e}):void 0,$=`lit$${(Math.random()+"").slice(9)}$`,E="?"+$,S=`<${E}>`,x=document,A=()=>x.createComment(""),C=e=>null===e||"object"!=typeof e&&"function"!=typeof e,T=Array.isArray,k="[ \t\n\f\r]",P=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,M=/-->/g,I=/>/g,O=RegExp(`>|${k}(?:([^\\s"'>=/]+)(${k}*=${k}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),L=/'/g,D=/"/g,U=/^(?:script|style|textarea|title)$/i,R=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),j=Symbol.for("lit-noChange"),H=Symbol.for("lit-nothing"),N=new WeakMap,B=x.createTreeWalker(x,129,null,!1),z=(e,t)=>{const i=e.length-1,o=[];let s,n=2===t?"":"");if(!Array.isArray(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==f?f.createHTML(a):a,o]};class W{constructor({strings:e,_$litType$:t},i){let o;this.parts=[];let s=0,n=0;const r=e.length-1,a=this.parts,[d,l]=z(e,t);if(this.el=W.createElement(d,i),B.currentNode=this.el.content,2===t){const e=this.el.content,t=e.firstChild;t.remove(),e.append(...t.childNodes)}for(;null!==(o=B.nextNode())&&a.lengthmedia_player
and camera
components of Browser
+ Mod. + Do not report any issues to Home Assistant before clearing + EVERY setting here and thouroghly clearing all your browser + caches. Failure to do so means you risk wasting a lot of peoples + time, and you will be severly and rightfully ridiculed. +
++ Settings below are applied by first match. I.e. if a matching User + setting exists, it will be applied. Otherwise any matching Browser + setting and otherwise the GLOBAL setting if that differs from + DEFAULT. +
+ +