From fb3af71fe359ccf7c03c0bfd7c24ef3ca52d088c Mon Sep 17 00:00:00 2001 From: Florian Brinker Date: Sun, 21 Mar 2021 18:41:41 +0100 Subject: [PATCH] Update HA, add vacuum, grid cards, fixes --- .gitignore | 3 + .vscode/home-assistant.code-workspace | 12 + .vscode/settings.json | 5 + README.md | 18 + config/alerts/windows.yaml | 4 +- config/alexa.yaml | 13 +- config/automations/livingroom.yaml | 66 + config/automations/wallboard.yaml | 48 +- config/cameras.yaml | 21 +- config/customizations/misc.yaml | 9 +- config/google_assistant.yaml | 63 + config/lights.yaml | 21 +- config/scripts/robots.yaml | 54 + config/sensors/devices.yaml | 5 - config/sensors_binary/cctv.yaml | 9 - config/sensors_binary/misc.yaml | 6 + config/sensors_binary/wallboard.yaml | 12 +- config/switches/cctv.yaml | 53 - config/switches/misc.yaml | 13 + config/switches/wallboard.yaml | 19 +- config/views/devices.yaml | 32 +- config/views/floorplan.yaml | 1 + config/views/humidity.yaml | 244 ++-- config/views/lights.yaml | 66 +- config/views/{livingroom.yaml => media.yaml} | 24 +- config/views/network.yaml | 4 + config/views/overview.yaml | 18 +- config/views/{landroid.yaml => robots.yaml} | 44 +- config/views/spotify.yaml | 5 - configuration.yaml | 71 +- custom_components/deebot/__init__.py | 119 ++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 3381 bytes .../__pycache__/binary_sensor.cpython-38.pyc | Bin 0 -> 1886 bytes .../deebot/__pycache__/sensor.cpython-38.pyc | Bin 0 -> 5422 bytes .../deebot/__pycache__/vacuum.cpython-38.pyc | Bin 0 -> 7591 bytes custom_components/deebot/binary_sensor.py | 48 + custom_components/deebot/manifest.json | 11 + custom_components/deebot/sensor.py | 179 +++ custom_components/deebot/vacuum.py | 247 ++++ custom_components/fontawesome/__init__.py | 50 + .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 1409 bytes custom_components/fontawesome/config_flow.py | 49 + custom_components/fontawesome/data/fab.js | 1 + custom_components/fontawesome/data/far.js | 1 + custom_components/fontawesome/data/fas.js | 1 + custom_components/fontawesome/manifest.json | 9 + .../fontawesome/translations/en.json | 21 + custom_components/landroid_cloud/__init__.py | 39 +- .../landroid_cloud/manifest.json | 6 +- custom_components/landroid_cloud/sensor.py | 259 ++-- .../landroid_cloud/services.yaml | 6 + .../reolink_dev/ReolinkPyPi/__init__.py | 1 - .../__pycache__/__init__.cpython-37.pyc | Bin 203 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 207 -> 0 bytes .../__pycache__/camera.cpython-37.pyc | Bin 7282 -> 0 bytes .../__pycache__/camera.cpython-38.pyc | Bin 7238 -> 0 bytes .../reolink_dev/ReolinkPyPi/camera.py | 278 ---- custom_components/reolink_dev/__init__.py | 161 ++- .../__pycache__/__init__.cpython-37.pyc | Bin 191 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 195 -> 0 bytes .../__pycache__/camera.cpython-37.pyc | Bin 10496 -> 0 bytes .../__pycache__/camera.cpython-38.pyc | Bin 10446 -> 0 bytes custom_components/reolink_dev/base.py | 366 +++++ .../reolink_dev/binary_sensor.py | 106 ++ custom_components/reolink_dev/camera.py | 411 +++--- custom_components/reolink_dev/config_flow.py | 217 +++ custom_components/reolink_dev/const.py | 31 + custom_components/reolink_dev/entity.py | 41 + custom_components/reolink_dev/manifest.json | 31 +- custom_components/reolink_dev/media_source.py | 412 ++++++ custom_components/reolink_dev/services.yaml | 101 +- custom_components/reolink_dev/strings.json | 42 + custom_components/reolink_dev/switch.py | 269 ++++ .../reolink_dev/translations/de.json | 39 + .../reolink_dev/translations/en.json | 42 + .../reolink_dev/translations/es.json | 39 + .../reolink_dev/translations/fr.json | 39 + .../reolink_dev/translations/il.json | 39 + .../reolink_dev/translations/nl.json | 39 + .../reolink_dev/translations/pl.json | 39 + .../reolink_dev/translations/se.json | 39 + custom_components/reolink_dev/typings.py | 31 + ui-lovelace.yaml | 20 +- www/images/playstation.jpg | Bin 34380 -> 0 bytes .../custom/auto-entities/auto-entities.js | 194 +++ .../custom/color-lite-card/color-lite-card.js | 69 + .../now-playing-card/now-playing-card.js | 102 ++ .../custom/vacuum-card/vacuum-card.js | 674 +++++++++ .../weather-card/icons/cloudy-day-1.svg | 175 +++ .../weather-card/icons/cloudy-day-2.svg | 176 +++ .../weather-card/icons/cloudy-day-3.svg | 175 +++ .../weather-card/icons/cloudy-night-1.svg | 198 +++ .../weather-card/icons/cloudy-night-2.svg | 198 +++ .../weather-card/icons/cloudy-night-3.svg | 198 +++ .../custom/weather-card/icons/cloudy.svg | 500 +++++++ .../custom/weather-card/icons/day.svg | 521 +++++++ .../custom/weather-card/icons/night.svg | 503 +++++++ .../custom/weather-card/icons/rainy-1.svg | 157 +++ .../custom/weather-card/icons/rainy-2.svg | 133 ++ .../custom/weather-card/icons/rainy-3.svg | 157 +++ .../custom/weather-card/icons/rainy-4.svg | 66 + .../custom/weather-card/icons/rainy-5.svg | 90 ++ .../custom/weather-card/icons/rainy-6.svg | 91 ++ .../custom/weather-card/icons/rainy-7.svg | 91 ++ .../custom/weather-card/icons/snowy-1.svg | 230 +++ .../custom/weather-card/icons/snowy-2.svg | 237 ++++ .../custom/weather-card/icons/snowy-3.svg | 268 ++++ .../custom/weather-card/icons/snowy-4.svg | 94 ++ .../custom/weather-card/icons/snowy-5.svg | 166 +++ .../custom/weather-card/icons/snowy-6.svg | 225 +++ .../custom/weather-card/icons/thunder.svg | 268 ++++ .../weather-card/icons/weather-sprite.svg | 1245 +++++++++++++++++ .../custom/weather-card/icons/weather.svg | 1245 +++++++++++++++++ .../icons/weather_sagittarius.svg | 9 + .../weather-card/icons/weather_sunset.svg | 14 + .../weather-card/weather-card-editor.js | 216 +++ .../custom/weather-card/weather-card.js | 530 +++++++ www/lovelace/resources/auto-entities.js | 2 - www/lovelace/resources/color-lite-card.js | 64 - www/lovelace/resources/now-playing-card.js | 102 -- www/lovelace/resources/weather-card.js | 247 ---- www/vacuums/Dobby_liveMap.png | Bin 0 -> 14808 bytes 122 files changed, 12940 insertions(+), 1532 deletions(-) create mode 100644 .vscode/home-assistant.code-workspace create mode 100644 .vscode/settings.json create mode 100644 config/google_assistant.yaml create mode 100644 config/scripts/robots.yaml delete mode 100644 config/sensors/devices.yaml delete mode 100644 config/sensors_binary/cctv.yaml delete mode 100644 config/switches/cctv.yaml rename config/views/{livingroom.yaml => media.yaml} (69%) rename config/views/{landroid.yaml => robots.yaml} (89%) delete mode 100644 config/views/spotify.yaml create mode 100644 custom_components/deebot/__init__.py create mode 100644 custom_components/deebot/__pycache__/__init__.cpython-38.pyc create mode 100644 custom_components/deebot/__pycache__/binary_sensor.cpython-38.pyc create mode 100644 custom_components/deebot/__pycache__/sensor.cpython-38.pyc create mode 100644 custom_components/deebot/__pycache__/vacuum.cpython-38.pyc create mode 100644 custom_components/deebot/binary_sensor.py create mode 100644 custom_components/deebot/manifest.json create mode 100644 custom_components/deebot/sensor.py create mode 100644 custom_components/deebot/vacuum.py create mode 100644 custom_components/fontawesome/__init__.py create mode 100644 custom_components/fontawesome/__pycache__/__init__.cpython-38.pyc create mode 100644 custom_components/fontawesome/config_flow.py create mode 100644 custom_components/fontawesome/data/fab.js create mode 100644 custom_components/fontawesome/data/far.js create mode 100644 custom_components/fontawesome/data/fas.js create mode 100644 custom_components/fontawesome/manifest.json create mode 100644 custom_components/fontawesome/translations/en.json delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/__init__.py delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-38.pyc delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-37.pyc delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-38.pyc delete mode 100644 custom_components/reolink_dev/ReolinkPyPi/camera.py delete mode 100644 custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc delete mode 100644 custom_components/reolink_dev/__pycache__/__init__.cpython-38.pyc delete mode 100644 custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc delete mode 100644 custom_components/reolink_dev/__pycache__/camera.cpython-38.pyc create mode 100644 custom_components/reolink_dev/base.py create mode 100644 custom_components/reolink_dev/binary_sensor.py create mode 100644 custom_components/reolink_dev/config_flow.py create mode 100644 custom_components/reolink_dev/const.py create mode 100644 custom_components/reolink_dev/entity.py create mode 100644 custom_components/reolink_dev/media_source.py create mode 100644 custom_components/reolink_dev/strings.json create mode 100644 custom_components/reolink_dev/switch.py create mode 100644 custom_components/reolink_dev/translations/de.json create mode 100644 custom_components/reolink_dev/translations/en.json create mode 100644 custom_components/reolink_dev/translations/es.json create mode 100644 custom_components/reolink_dev/translations/fr.json create mode 100644 custom_components/reolink_dev/translations/il.json create mode 100644 custom_components/reolink_dev/translations/nl.json create mode 100644 custom_components/reolink_dev/translations/pl.json create mode 100644 custom_components/reolink_dev/translations/se.json create mode 100644 custom_components/reolink_dev/typings.py delete mode 100644 www/images/playstation.jpg create mode 100644 www/lovelace/custom/auto-entities/auto-entities.js create mode 100644 www/lovelace/custom/color-lite-card/color-lite-card.js create mode 100644 www/lovelace/custom/now-playing-card/now-playing-card.js create mode 100644 www/lovelace/custom/vacuum-card/vacuum-card.js create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-day-1.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-day-2.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-day-3.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-night-1.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-night-2.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy-night-3.svg create mode 100644 www/lovelace/custom/weather-card/icons/cloudy.svg create mode 100644 www/lovelace/custom/weather-card/icons/day.svg create mode 100644 www/lovelace/custom/weather-card/icons/night.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-1.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-2.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-3.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-4.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-5.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-6.svg create mode 100644 www/lovelace/custom/weather-card/icons/rainy-7.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-1.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-2.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-3.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-4.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-5.svg create mode 100644 www/lovelace/custom/weather-card/icons/snowy-6.svg create mode 100644 www/lovelace/custom/weather-card/icons/thunder.svg create mode 100644 www/lovelace/custom/weather-card/icons/weather-sprite.svg create mode 100644 www/lovelace/custom/weather-card/icons/weather.svg create mode 100644 www/lovelace/custom/weather-card/icons/weather_sagittarius.svg create mode 100644 www/lovelace/custom/weather-card/icons/weather_sunset.svg create mode 100644 www/lovelace/custom/weather-card/weather-card-editor.js create mode 100644 www/lovelace/custom/weather-card/weather-card.js delete mode 100644 www/lovelace/resources/auto-entities.js delete mode 100644 www/lovelace/resources/color-lite-card.js delete mode 100644 www/lovelace/resources/now-playing-card.js delete mode 100644 www/lovelace/resources/weather-card.js create mode 100644 www/vacuums/Dobby_liveMap.png diff --git a/.gitignore b/.gitignore index 270f44b..be588c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /* # Allow +!/.vscode/ + !*.yaml !*.jpg !*.png @@ -23,6 +25,7 @@ android/ ssh-key/ ip_bans.yaml +google_assistant_service_keys.json secrets.yaml secrets.js known_devices.yaml diff --git a/.vscode/home-assistant.code-workspace b/.vscode/home-assistant.code-workspace new file mode 100644 index 0000000..6acf14b --- /dev/null +++ b/.vscode/home-assistant.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": { + "files.associations": { + "*.yaml": "home-assistant" + } + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a04b218 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 78f50d1..0b50210 100644 --- a/README.md +++ b/README.md @@ -56,4 +56,22 @@ I use the following software, running in docker containers, on my Raspberry Pi: ### Security * [Yi Home Camera 1080p](https://amzn.to/2SYhoW6) - with [custom firmware and mqtt add-on](https://github.com/fbrinker/yi-hack-mqtt) +# Custom stuff + +## Custom Components +* https://github.com/And3rsL/Deebot-for-Home-Assistant +* https://github.com/MTrab/landroid_cloud +* https://github.com/fwestenberg/reolink_dev +* https://github.com/thomasloven/hass-fontawesome + +## Lovelace Ressources +* https://github.com/bramkragten/weather-card/ +* https://github.com/denysdovhan/vacuum-card +* https://github.com/thomasloven/lovelace-auto-entities +* https://github.com/bradcrc/Now-Playing-Card +* https://github.com/bradcrc/color-lite-card + +## Packages +* https://github.com/Barma-lej/halandroid + Work in progress. Not all of them are integrated yet. \ No newline at end of file diff --git a/config/alerts/windows.yaml b/config/alerts/windows.yaml index 4d495d3..77b81d6 100644 --- a/config/alerts/windows.yaml +++ b/config/alerts/windows.yaml @@ -30,13 +30,13 @@ garage_door_open: name: Garagentor geöffnet message: "Zur Info - Die *Garage* steht noch *offen*!" done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*." - entity_id: binary_sensor.lumi_garage_door + entity_id: binary_sensor.garage_door state: "on" repeat: - 10 - 30 can_acknowledge: true - skip_first: true + skip_first: false notifiers: - telegram_group - alexa_all \ No newline at end of file diff --git a/config/alexa.yaml b/config/alexa.yaml index ed85c62..8203b50 100644 --- a/config/alexa.yaml +++ b/config/alexa.yaml @@ -1,6 +1,7 @@ # Display Categories: https://developer.amazon.com/de/docs/device-apis/alexa-discovery.html#display-categories smart_home: - endpoint: https://api.amazonalexa.com/v3/events + endpoint: https://api.eu.amazonalexa.com/v3/events + locale: de-DE client_id: !secret alexa_client_id client_secret: !secret alexa_client_secret filter: @@ -13,6 +14,7 @@ - light.onair - light.stimmungslicht - light.lichterkette + - light.philips_iris - switch.livingroom_music - switch.livingroom_netflix - switch.harmony_firetv @@ -20,6 +22,7 @@ - switch.harmony_playstation - switch.harmony_denon_power - switch.desktop_wol + - switch.desktop_jenny_wol - switch.wallboard_display - switch.tplink1 - switch.osram_plug_01_57b6060a_on_off @@ -43,7 +46,7 @@ name: Esstisch description: Esstisch-Lichter light.office_rgb: - name: Büro + name: Bürolicht description: Büro - Deckenlampe light.lichtleiste: name: Schreibtischlicht @@ -57,6 +60,9 @@ light.onair: name: Studio-Treppe description: Studio-Treppenlicht + light.philips_iris: + name: Iris + description: Studio-Stimmungslicht switch.livingroom_music: name: Musik description: Wohnzimmer - Musik @@ -78,6 +84,9 @@ switch.desktop_wol: name: Computer description: Computer im Büro + switch.desktop_jenny_wol: + name: Ronny + description: Jennys Computer switch.wallboard_display: name: Display description: Wallboard Display diff --git a/config/automations/livingroom.yaml b/config/automations/livingroom.yaml index 1919df7..e48e0a7 100644 --- a/config/automations/livingroom.yaml +++ b/config/automations/livingroom.yaml @@ -1,3 +1,69 @@ +- alias: Christmas-Tree on + trigger: + - platform: time + at: + - "05:30:00" + - "16:00:00" + - platform: zone + entity_id: + - person.jenny + zone: zone.home + event: enter + action: + service: light.turn_on + data: + entity_id: + - light.weihnachtsbaum + +- alias: Christmas-Tree off morning + trigger: + - platform: time + at: + - "08:00:00" + condition: + condition: not + conditions: + - condition: zone + entity_id: person.jenny + zone: zone.home + action: + service: light.turn_off + data: + entity_id: + - light.weihnachtsbaum + +- alias: Christmas-Tree off evening + trigger: + - platform: time + at: + - "23:00:00" + action: + service: light.turn_off + data: + entity_id: + - light.weihnachtsbaum + +- alias: Christmas-Tree off Presence + trigger: + - platform: zone + entity_id: + - person.jenny + - person.florian + zone: zone.home + event: leave + condition: + - condition: zone + entity_id: + - person.jenny + - person.florian + zone: zone.not_home + action: + service: light.turn_off + data: + entity_id: + - light.weihnachtsbaum + + - alias: Ambilight HDMI trigger: platform: state diff --git a/config/automations/wallboard.yaml b/config/automations/wallboard.yaml index ed3f5c2..74a41ac 100644 --- a/config/automations/wallboard.yaml +++ b/config/automations/wallboard.yaml @@ -1,25 +1,25 @@ -- alias: Wallboard On - trigger: - - platform: state - entity_id: binary_sensor.anyone_home - to: 'on' - - platform: state - entity_id: sensor.harmony_activity - from: 'Fire TV sehen' - action: - service: switch.turn_on - data: - entity_id: switch.wallboard_display +#- alias: Wallboard On +# trigger: +# - platform: state +# entity_id: binary_sensor.anyone_home +# to: 'on' +# - platform: state +# entity_id: sensor.harmony_activity +# from: 'Fire TV sehen' +# action: +# service: switch.turn_on +# data: +# entity_id: switch.wallboard_display -- alias: Wallboard Off - trigger: - - platform: state - entity_id: binary_sensor.anyone_home - to: 'off' - - platform: state - entity_id: sensor.harmony_activity - to: 'Fire TV sehen' - action: - service: switch.turn_off - data: - entity_id: switch.wallboard_display \ No newline at end of file +#- alias: Wallboard Off +# trigger: +# - platform: state +# entity_id: binary_sensor.anyone_home +# to: 'off' +# - platform: state +# entity_id: sensor.harmony_activity +# to: 'Fire TV sehen' +# action: +# service: switch.turn_off +# data: +# entity_id: switch.wallboard_display \ No newline at end of file diff --git a/config/cameras.yaml b/config/cameras.yaml index 8e88788..969c90a 100644 --- a/config/cameras.yaml +++ b/config/cameras.yaml @@ -1,9 +1,12 @@ -- platform: reolink_dev - host: !secret cam_livingroom_ip - username: !secret cam_livingroom_user - password: !secret cam_livingroom_password - name: livingroom - stream: main - protocol: rtmp - channel: 0 - scan_interval: 30 \ No newline at end of file +- platform: generic + name: deebot_dobby_live_map + still_image_url: "https://hass.f-brinker.de/local/vacuums/Dobby_liveMap.png" +#- platform: reolink_dev +# host: !secret cam_livingroom_ip +# username: !secret cam_livingroom_user +# password: !secret cam_livingroom_password +# name: livingroom +# stream: main +# protocol: rtmp +# channel: 0 +# scan_interval: 30 \ No newline at end of file diff --git a/config/customizations/misc.yaml b/config/customizations/misc.yaml index 7286aee..d5bb0d4 100644 --- a/config/customizations/misc.yaml +++ b/config/customizations/misc.yaml @@ -1,4 +1,11 @@ person.jenny: entity_picture: "/local/avatars/jenny-bty.jpg" person.florian: - entity_picture: "/local/avatars/flo-mtb.jpg" \ No newline at end of file + entity_picture: "/local/avatars/flo-mtb.jpg" + +device_tracker.sm_g985f: + entity_picture: "/local/avatars/jenny-bty.jpg" + icon: mdi:cellphone +device_tracker.pixely: + entity_picture: "/local/avatars/flo-mtb.jpg" + icon: mdi:cellphone \ No newline at end of file diff --git a/config/google_assistant.yaml b/config/google_assistant.yaml new file mode 100644 index 0000000..dfb89af --- /dev/null +++ b/config/google_assistant.yaml @@ -0,0 +1,63 @@ +# https://console.actions.google.com/u/0/project/home-assistant-e12c3/overview +project_id: !secret google_project_id +service_account: !include google_assistant_service_keys.json +report_state: true +expose_by_default: false +exposed_domains: + - switch + - light +entity_config: + light.ambilight: + name: Ambilight + room: Wohnzimmer + light.kuchen_theke: + name: Küche + room: Küche + light.esstisch: + name: Esstisch + room: Wohnzimmer + light.office_rgb: + name: Büro + room: Büro + light.lichtleiste: + name: Schreibtischlicht + room: Büro + light.stimmungslicht: + name: Stimmungslicht + room: Wohnzimmer + light.lichterkette: + name: Lichterkette + room: Garten + light.weihnachtsbaum: + name: Weihnachtsbaum + room: Wohnzimmer + light.onair: + name: Studio-Treppe + room: Studio + switch.livingroom_music: + name: Musik + room: Wohnzimmer + switch.harmony_firetv: + name: Fernseher + room: Wohnzimmer + switch.harmony_steamlink: + name: Konsole + room: Wohnzimmer + switch.harmony_playstation: + name: Playstation + room: Wohnzimmer + switch.harmony_denon_power: + name: Receiver + room: Wohnzimmer + switch.desktop_wol: + name: Computer + room: Büro + switch.wallboard_display: + name: Display + room: Wohnzimmer + switch.osram_plug_01_57b6060a_on_off: + name: Ring + room: Studio + switch.onair_lamp_recording: + name: Aufnahme + room: Studio \ No newline at end of file diff --git a/config/lights.yaml b/config/lights.yaml index f00f6d7..3da0f9b 100644 --- a/config/lights.yaml +++ b/config/lights.yaml @@ -1,7 +1,16 @@ - - platform: hyperion - name: Ambilight - host: !secret ambilight_ip - hdmi_priority: 900 + - platform: group + name: Küchen-Theke + entities: + - light.kitchen1 + - light.kitchen2 + - platform: group + name: Esstisch + entities: + - light.dining1 + - light.dining2 +# - platform: hyperion +# name: Ambilight +# host: !secret ambilight_ip - platform: group name: onAir entities: @@ -9,7 +18,7 @@ - light.innr_gu10_rgb_2 - platform: switch name: Lichterkette - entity_id: switch.innr_steckdose + entity_id: switch.garden_chain_of_lights - platform: switch name: Stimmungslicht - entity_id: switch.livingroom_stimmungslicht \ No newline at end of file + entity_id: switch.moodlight \ No newline at end of file diff --git a/config/scripts/robots.yaml b/config/scripts/robots.yaml new file mode 100644 index 0000000..6022852 --- /dev/null +++ b/config/scripts/robots.yaml @@ -0,0 +1,54 @@ +vacuum_living_room: + alias: "Wohnzimmer saugen" + sequence: + - service: vacuum.send_command + data: + entity_id: vacuum.dobby + command: spot_area + params: + rooms: 0 + cleanings: 1 + +vacuum_kitchen: + alias: "Küche saugen" + sequence: + - service: vacuum.send_command + data: + entity_id: vacuum.dobby + command: spot_area + params: + rooms: 1 + cleanings: 1 + +vacuum_corridor: + alias: "Flur saugen" + sequence: + - service: vacuum.send_command + data: + entity_id: vacuum.dobby + command: spot_area + params: + rooms: 2 + cleanings: 1 + +vacuum_laundry: + alias: "HWR saugen" + sequence: + - service: vacuum.send_command + data: + entity_id: vacuum.dobby + command: spot_area + params: + rooms: 3 + cleanings: 1 + +vacuum_dining_room: + alias: "Esszimmer saugen" + sequence: + - service: vacuum.send_command + data: + entity_id: vacuum.dobby + command: spot_area + params: + rooms: 4 + cleanings: 1 \ No newline at end of file diff --git a/config/sensors/devices.yaml b/config/sensors/devices.yaml deleted file mode 100644 index dcd8d31..0000000 --- a/config/sensors/devices.yaml +++ /dev/null @@ -1,5 +0,0 @@ - - platform: template - sensors: - device_mobile_fb_battery: - value_template: '{{ states.device_tracker.mobile_fb.attributes.battery }}' - unit_of_measurement: '%' \ No newline at end of file diff --git a/config/sensors_binary/cctv.yaml b/config/sensors_binary/cctv.yaml deleted file mode 100644 index 6f479fa..0000000 --- a/config/sensors_binary/cctv.yaml +++ /dev/null @@ -1,9 +0,0 @@ - - platform: template - sensors: - motion_livingroom: - friendly_name: Motion Livingroom - device_class: motion - entity_id: camera.livingroom - value_template: "{{ is_state('camera.livingroom', 'motion') }}" - delay_off: - seconds: 30 \ No newline at end of file diff --git a/config/sensors_binary/misc.yaml b/config/sensors_binary/misc.yaml index 9a1dc2f..0f1387f 100644 --- a/config/sensors_binary/misc.yaml +++ b/config/sensors_binary/misc.yaml @@ -14,6 +14,12 @@ count: 2 scan_interval: 15 + - platform: ping + name: desktop_jenny_ping + host: !secret desktop_jenny_ip + count: 2 + scan_interval: 15 + - platform: template sensors: anyone_home: diff --git a/config/sensors_binary/wallboard.yaml b/config/sensors_binary/wallboard.yaml index f074638..0d7e683 100644 --- a/config/sensors_binary/wallboard.yaml +++ b/config/sensors_binary/wallboard.yaml @@ -1,6 +1,6 @@ - - platform: command_line - name: Wallboard HDMI Status - command: !secret wallboard_hdmi_status_cmd - payload_on: display_power=1 - payload_off: display_power=0 - scan_interval: 60 \ No newline at end of file +# - platform: command_line +# name: Wallboard HDMI Status +# command: !secret wallboard_hdmi_status_cmd +# payload_on: display_power=1 +# payload_off: display_power=0 +# scan_interval: 60 \ No newline at end of file diff --git a/config/switches/cctv.yaml b/config/switches/cctv.yaml deleted file mode 100644 index cd7ada9..0000000 --- a/config/switches/cctv.yaml +++ /dev/null @@ -1,53 +0,0 @@ -- platform: template - switches: - - camera_livingroom_email: - value_template: "{{ is_state_attr('camera.livingroom', 'email_enabled', true) }}" - turn_on: - service: camera.enable_email - data: - entity_id: camera.livingroom - turn_off: - service: camera.disable_email - data: - entity_id: camera.livingroom - icon_template: >- - {% if is_state_attr('camera.livingroom', 'email_enabled', true) %} - mdi:email - {% else %} - mdi:email-outline - {% endif %} - - camera_livingroom_ftp: - value_template: "{{ is_state_attr('camera.livingroom', 'ftp_enabled', true) }}" - turn_on: - service: camera.enable_ftp - data: - entity_id: camera.livingroom - turn_off: - service: camera.disable_ftp - data: - entity_id: camera.livingroom - icon_template: >- - {% if is_state_attr('camera.livingroom', 'ftp_enabled', true) %} - mdi:filmstrip - {% else %} - mdi:filmstrip-off - {% endif %} - - camera_livingroom_ir_lights: - value_template: "{{ is_state_attr('camera.livingroom', 'ir_lights_enabled', true) }}" - turn_on: - service: camera.enable_ir_lights - data: - entity_id: camera.livingroom - turn_off: - service: camera.disable_ir_lights - data: - entity_id: camera.livingroom - icon_template: >- - {% if is_state_attr('camera.livingroom', 'ir_lights_enabled', true) %} - mdi:flashlight - {% else %} - mdi:flashlight-off - {% endif %} \ No newline at end of file diff --git a/config/switches/misc.yaml b/config/switches/misc.yaml index a3598b3..c7fea29 100644 --- a/config/switches/misc.yaml +++ b/config/switches/misc.yaml @@ -14,6 +14,19 @@ turn_off: service: script.dummy + desktop_jenny_wol: + value_template: "{{ is_state('binary_sensor.desktop_jenny_ping', 'on') }}" + turn_on: + service: shell_command.ssh + data: + sshkey: !secret sshkey_wakeonlan + host: !secret nas_ip + user: !secret nas_ssh_user + port: !secret nas_ssh_port + command_or_param: !secret desktop_jenny_mac + turn_off: + service: script.dummy + onair_lamp_recording: value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}" turn_on: diff --git a/config/switches/wallboard.yaml b/config/switches/wallboard.yaml index db06884..72a7bd0 100644 --- a/config/switches/wallboard.yaml +++ b/config/switches/wallboard.yaml @@ -1,10 +1,9 @@ -- platform: template - switches: - - wallboard_display: - friendly_name: Wallboard Display Toggle - value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}" - turn_on: - service: script.wallboard_hdmi_on - turn_off: - service: script.wallboard_hdmi_off \ No newline at end of file +#- platform: template +# switches: +# wallboard_display: +# friendly_name: Wallboard Display Toggle +# value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}" +# turn_on: +# service: script.wallboard_hdmi_on +# turn_off: +# service: script.wallboard_hdmi_off \ No newline at end of file diff --git a/config/views/devices.yaml b/config/views/devices.yaml index 982f837..411c1b1 100644 --- a/config/views/devices.yaml +++ b/config/views/devices.yaml @@ -1,20 +1,15 @@ icon: mdi:cellphone-link path: devices cards: - - type: entities - entities: - - entity: sensor.myip - name: Öffentliche IP - icon: mdi:earth - type: horizontal-stack cards: - type: entities title: Florian show_header_toggle: false entities: - - entity: device_tracker.pixel_4 + - entity: device_tracker.pixely name: Standort - - entity: sensor.battery_level + - entity: sensor.pixely_akkufullstand name: Handy-Akku icon: mdi:battery - type: entities @@ -29,28 +24,23 @@ cards: - type: horizontal-stack cards: - type: entities - title: Wake On Lan + title: WOL Florian show_header_toggle: false entities: - entity: switch.desktop_wol name: Desktop-PC icon: mdi:desktop-classic - type: entities - title: Wallboard + title: WOL Jenny show_header_toggle: false entities: - - entity: switch.wallboard_display - name: Display - icon: mdi:tablet + - entity: switch.desktop_jenny_wol + name: Desktop-PC Ronny + icon: mdi:desktop-classic - type: entities - title: Home Assistant Slaves + title: Wallboard show_header_toggle: false entities: - - type: weblink - name: Livingroom - url: !secret url_haslave_livingroom - icon: mdi:home-assistant - - type: weblink - name: Office - url: !secret url_haslave_office - icon: mdi:home-assistant \ No newline at end of file + - entity: switch.wallboard_display + name: Display + icon: mdi:tablet \ No newline at end of file diff --git a/config/views/floorplan.yaml b/config/views/floorplan.yaml index 185c275..9ef20c1 100644 --- a/config/views/floorplan.yaml +++ b/config/views/floorplan.yaml @@ -1,6 +1,7 @@ #icon: mdi:home-variant-outline title: Haus path: floor +icon: 'mdi:floor-plan' panel: true cards: - type: picture-elements diff --git a/config/views/humidity.yaml b/config/views/humidity.yaml index 4f75315..bab2cfd 100644 --- a/config/views/humidity.yaml +++ b/config/views/humidity.yaml @@ -1,151 +1,103 @@ icon: mdi:water-percent path: humidity cards: - - type: vertical-stack + + - type: grid + title: Wohnzimmer + columns: 2 + square: true cards: - - type: markdown - content: "### Elternbad" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.hygro_bathroom_parents_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.hygro_bathroom_parents_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### \"Kinderbad\"" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.hygro_bathroom_kids_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.hygro_bathroom_kids_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### Schlafzimmer" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.lumi_bedroom_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.lumi_bedroom_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### Gästezimmer" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.lumi_guestroom_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.lumi_guestroom_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: vertical-stack + - type: sensor + entity: sensor.lumi_livingroom_humidity + name: Luftfeuchtigkeit + graph: line + unit: "%" + detail: 2 + hours_to_show: 24 + - type: sensor + entity: sensor.lumi_livingroom_temperature + name: Temperatur + graph: line + unit: °C + detail: 2 + hours_to_show: 24 + + - type: grid + title: Schlafzimmer + columns: 2 + square: true cards: - - type: markdown - content: "### Wohnzimmer" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.lumi_livingroom_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.lumi_livingroom_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### Hauswirtschaftsraum" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.hygro_hwr_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.hygro_hwr_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### Büro" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.lumi_office_humidity - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.lumi_office_temperature - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 - - type: markdown - content: "### Dachboden" - - type: horizontal-stack - cards: - - type: sensor - entity: sensor.attic_humidity_2 - name: Luftfeuchtigkeit - graph: line - unit: "%" - detail: 2 - hours_to_show: 12 - - type: sensor - entity: sensor.attic_temperature_2 - name: Temperatur - graph: line - unit: °C - detail: 2 - hours_to_show: 12 + - type: sensor + entity: sensor.lumi_bedroom_humidity + name: Luftfeuchtigkeit + graph: line + unit: "%" + detail: 2 + hours_to_show: 24 + - type: sensor + entity: sensor.lumi_bedroom_temperature + name: Temperatur + graph: line + unit: °C + detail: 2 + hours_to_show: 24 + + - type: grid + title: Gästezimmer + columns: 2 + square: true + cards: + - type: sensor + entity: sensor.lumi_guestroom_humidity + name: Luftfeuchtigkeit + graph: line + unit: "%" + detail: 2 + hours_to_show: 24 + - type: sensor + entity: sensor.lumi_guestroom_temperature + name: Temperatur + graph: line + unit: °C + detail: 2 + hours_to_show: 24 + + - type: grid + title: Büro + columns: 2 + square: true + cards: + - type: sensor + entity: sensor.lumi_office_humidity + name: Luftfeuchtigkeit + graph: line + unit: "%" + detail: 2 + hours_to_show: 24 + - type: sensor + entity: sensor.lumi_office_temperature + name: Temperatur + graph: line + unit: °C + detail: 2 + hours_to_show: 24 + + - type: grid + title: Dachboden + columns: 2 + square: true + cards: + - type: sensor + entity: sensor.attic_humidity_2 + name: Luftfeuchtigkeit + graph: line + unit: "%" + detail: 2 + hours_to_show: 24 + - type: sensor + entity: sensor.attic_temperature_2 + name: Temperatur + graph: line + unit: °C + detail: 2 + hours_to_show: 24 diff --git a/config/views/lights.yaml b/config/views/lights.yaml index 9227993..af69093 100644 --- a/config/views/lights.yaml +++ b/config/views/lights.yaml @@ -1,24 +1,42 @@ title: Lichter path: lights +icon: 'mdi:lightbulb' cards: - - type: light - entity: light.kuchen_theke - name: Küchen-Theke - - type: light - entity: light.stimmungslicht - name: Stimmungslicht - - type: light - entity: light.ambilight - name: Ambilight - - type: light - entity: light.lichterkette - name: Lichterkette (Garten) - - type: light - entity: light.esstisch - name: Esstisch - - type: light - entity: light.tint_rgb_gu10_1 - name: Kinderbad + - type: grid + title: Erdgeschoss + columns: 3 + suqare: true + cards: + - type: light + entity: light.kuchen_theke + name: Küchen-Theke + - type: light + entity: light.esstisch + name: Esstisch + - type: light + entity: light.stimmungslicht + name: Stimmungslicht + - type: light + entity: light.ambilight + name: Ambilight + - type: light + entity: light.lichterkette + name: Garten + + - type: grid + title: 1. Stock + columns: 3 + suqare: true + cards: + - type: light + entity: light.office_rgb + name: Bürolicht + - type: light + entity: light.lichtleiste + name: Schreibtischlicht + - type: light + entity: light.tint_rgb_gu10_1 + name: Kinderbad - type: vertical-stack title: Studio @@ -29,19 +47,11 @@ cards: - type: entities show_header_toggle: false entities: + - entity: light.philips_iris + name: Iris - entity: switch.onair_lamp_recording name: Studio Aufnahme icon: mdi:camera-rear - entity: switch.osram_plug_01_57b6060a_on_off name: Studio Ringlicht icon: mdi:checkbox-blank-circle-outline - - - type: vertical-stack - title: Büro - cards: - - type: light - entity: light.office_rgb - name: Deckenlicht - - type: light - entity: light.lichtleiste - name: Schreibtischlicht diff --git a/config/views/livingroom.yaml b/config/views/media.yaml similarity index 69% rename from config/views/livingroom.yaml rename to config/views/media.yaml index 996eef3..edfa4b6 100644 --- a/config/views/livingroom.yaml +++ b/config/views/media.yaml @@ -1,20 +1,9 @@ -title: Wohnzimmer -path: livingroom +title: Media +path: media +icon: 'mdi:theater' cards: - # Lights - - type: horizontal-stack - cards: - - type: entities - entities: - - entity: light.kuchen_theke - name: Küchen-Theke - - entity: light.esstisch - name: Esstisch - - entity: light.stimmungslicht - name: Stimmungslicht - icon: mdi:lightbulb - - entity: light.ambilight - name: Ambilight + - type: media-control + entity: media_player.spotify # Denon + Harmony - type: vertical-stack @@ -45,14 +34,11 @@ cards: icon: mdi:bluetooth-audio - entity: switch.harmony_steamlink icon: mdi:steam - - entity: switch.harmony_playstation - icon: mdi:playstation state_image: "PowerOff": /local/images/power.jpg "Fire TV sehen": /local/images/firetv.jpg "Musik Bluetooth": /local/images/music.jpg "SteamLink": /local/images/steamlink.jpg - "PlayStation": /local/images/playstation.jpg entity: sensor.harmony_activity # Spotify diff --git a/config/views/network.yaml b/config/views/network.yaml index 2829541..c880a2d 100644 --- a/config/views/network.yaml +++ b/config/views/network.yaml @@ -3,6 +3,10 @@ path: network cards: - type: vertical-stack cards: + - type: entity + entity: sensor.myip + name: Öffentliche IP + icon: mdi:earth - type: gauge entity: sensor.adguard_average_processing_speed max: 100 diff --git a/config/views/overview.yaml b/config/views/overview.yaml index 8b2929d..db36901 100644 --- a/config/views/overview.yaml +++ b/config/views/overview.yaml @@ -1,5 +1,6 @@ title: Übersicht path: overview +icon: "mdi:tablet-dashboard" cards: - type: conditional conditions: @@ -16,11 +17,12 @@ cards: - type: vertical-stack cards: - type: custom:weather-card - entity: weather.openweathermap name: Wetter - - type: iframe - url: !secret iframe_windy - aspect_ratio: 75% + entity: weather.openweathermap + icons: "/local/lovelace/custom/weather-card/icons/" +# - type: iframe +# url: !secret iframe_windy +# aspect_ratio: 75% # - type: iframe # url: !secret iframe_earth # aspect_ratio: 75% @@ -45,7 +47,7 @@ cards: - person.florian - person.jenny - type: map - aspect_ratio: 75% - default_zoom: 15 - entities: - - zone.home \ No newline at end of file + aspect_ratio: 60% + entities: + - device_tracker.sm_g985f + - device_tracker.pixel_4 \ No newline at end of file diff --git a/config/views/landroid.yaml b/config/views/robots.yaml similarity index 89% rename from config/views/landroid.yaml rename to config/views/robots.yaml index e152ed2..acdb81f 100644 --- a/config/views/landroid.yaml +++ b/config/views/robots.yaml @@ -1,8 +1,48 @@ -title: Mäheroboter -path: mower +title: Roboter +path: robots badges: [] icon: 'mdi:robot-vacuum-variant' cards: + # Dobby + - type: 'custom:vacuum-card' + entity: vacuum.dobby + map: camera.deebot_dobby_live_map + stats: + default: + - entity_id: sensor.dobby_heap + unit: Stunden + subtitle: Filter + - entity_id: sensor.dobby_sidebrush + unit: Stunden + subtitle: Seitenbürste + - entity_id: sensor.dobby_brush + unit: Stunden + subtitle: Hauptbürste + cleaning: + - entity_id: sensor.dobby_stats_area + unit: m2 + subtitle: Gereinigter Bereich + - entity_id: sensor.dobby_stats_time + unit: Minuten + subtitle: Dauer + actions: + - name: Wohnzimmer + service: script.vacuum_living_room + icon: 'mdi:sofa' + - name: Küche + service: script.vacuum_kitchen + icon: 'mdi:pot-steam' + - name: Esszimmer + service: script.vacuum_dining_room + icon: 'mdi:table-furniture' + - name: HWR + service: script.vacuum_laundry + icon: 'mdi:washing-machine' + - name: Flur + service: script.vacuum_corridor + icon: 'mdi:door' + + # Hans-Dieter - type: vertical-stack cards: - elements: diff --git a/config/views/spotify.yaml b/config/views/spotify.yaml deleted file mode 100644 index 57fba67..0000000 --- a/config/views/spotify.yaml +++ /dev/null @@ -1,5 +0,0 @@ -icon: mdi:spotify -path: spotify -cards: - - type: media-control - entity: media_player.spotify \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml index 72b1a7a..435b8b1 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -1,5 +1,7 @@ homeassistant: name: Home + internal_url: !secret url_internal + external_url: !secret url_external latitude: !secret home_lat longitude: !secret home_long elevation: !secret home_elevation @@ -15,12 +17,11 @@ homeassistant: - 192.168.0.0/16 - fd00::/8 # landroid - packages: !include_dir_named packages + #packages: !include_dir_named packages config: conversation: -device_tracker: -#discovery: +dhcp: history: logbook: map: @@ -35,31 +36,36 @@ http: ip_ban_enabled: true login_attempts_threshold: 5 use_x_forwarded_for: true - base_url: !secret url_base trusted_proxies: - 127.0.0.1 - ::1 + # traefik.web + - 172.19.0.0/24 frontend: themes: !include_dir_merge_named themes/ lovelace: mode: yaml + resources: + - url: /local/lovelace/custom/weather-card/weather-card.js + type: module + - url: /local/lovelace/custom/vacuum-card/vacuum-card.js + type: module + - url: /local/lovelace/custom/auto-entities/auto-entities.js + type: module + - url: /local/lovelace/custom/color-lite-card/color-lite-card.js + type: module + - url: /local/lovelace/custom/now-playing-card/now-playing-card.js + type: module tts: - platform: google_translate service_name: google_say zha: - usb_path: /dev/ttyACM0 - radio_type: deconz database_path: /config/zigbee.db -emulated_roku: - servers: - - name: Home Assistant - listen_port: !secret roku_port - mqtt: broker: !secret mqtt_broker_ip username: !secret mqtt_username @@ -90,32 +96,18 @@ notify: - platform: command_line name: alexa_all command: "/config/alexa_wrapper.sh -d 'ALL'" -# - name: android -# platform: fcm-android - -media_player: - - platform: androidtv - device_class: firetv - name: Fire TV - host: !secret firetv_ip - adbkey: !secret adbkey - get_sources: true - -remote: - - platform: harmony - name: Livingroom Harmony - host: !secret harmonyhub_ip - delay_secs: 0.3 person: - name: Jenny id: jenny + user_id: !secret userId_jenny device_trackers: - device_tracker.sm_g985f - name: Florian id: florian + user_id: !secret userId_florian device_trackers: - - device_tracker.pixel_4 + - device_tracker.pixely zone: - name: !secret work_name_f @@ -129,10 +121,6 @@ zone: radius: 500 icon: mdi:briefcase -weather: - - platform: openweathermap - api_key: !secret openweathermap - ffmpeg: ffmpeg_bin: /usr/bin/ffmpeg @@ -198,20 +186,27 @@ spotify: client_id: !secret spotify_client_id client_secret: !secret spotify_client_secret -ecovacs: +deebot: username: !secret ecovacs_user password: !secret ecovacs_password country: de continent: eu + deviceid: + - !secret ecovacs_serial_dobby + live_map: true + show_color_rooms: true + livemappath: 'www/vacuums/' -#tplink: -# discovery: false -# switch: -# - host: !secret tplink_ip +fontawesome: + regular: + solid: + brands: # External config files -alert: !include_dir_merge_named config/alerts/ alexa: !include config/alexa.yaml +google_assistant: !include config/google_assistant.yaml + +alert: !include_dir_merge_named config/alerts/ automation: !include_dir_merge_list config/automations/ binary_sensor: !include_dir_merge_list config/sensors_binary/ camera: !include config/cameras.yaml diff --git a/custom_components/deebot/__init__.py b/custom_components/deebot/__init__.py new file mode 100644 index 0000000..b86d8ee --- /dev/null +++ b/custom_components/deebot/__init__.py @@ -0,0 +1,119 @@ +"""Support for Deebot Vaccums.""" +import asyncio +import logging +import async_timeout +import time +import random +import string +import base64 +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from datetime import timedelta +from deebotozmo import * +from homeassistant.util import Throttle +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['deebotozmo==1.7.8'] + +CONF_COUNTRY = "country" +CONF_CONTINENT = "continent" +CONF_DEVICEID = "deviceid" +CONF_LIVEMAPPATH = "livemappath" +CONF_LIVEMAP = "live_map" +CONF_SHOWCOLORROOMS = "show_color_rooms" +DEEBOT_DEVICES = "deebot_devices" + +# Generate a random device ID on each bootup +DEEBOT_API_DEVICEID = "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(8) +) + +_LOGGER = logging.getLogger(__name__) + +HUB = None +DOMAIN = 'deebot' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), + vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), + vol.Required(CONF_DEVICEID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_LIVEMAP, default=True): cv.boolean, + vol.Optional(CONF_SHOWCOLORROOMS, default=False): cv.boolean, + vol.Optional(CONF_LIVEMAPPATH, default='www/'): cv.string + }), +}, extra=vol.ALLOW_EXTRA) + +def setup(hass, config): + """Set up the Deebot.""" + global HUB + + HUB = DeebotHub(config[DOMAIN]) + + for component in ('sensor', 'binary_sensor', 'vacuum'): + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + +class DeebotHub(Entity): + """Deebot Hub""" + + def __init__(self, domain_config): + """Initialize the Deebot Vacuum.""" + + self.config = domain_config + self._lock = threading.Lock() + + self.ecovacs_api = EcoVacsAPI( + DEEBOT_API_DEVICEID, + domain_config.get(CONF_USERNAME), + EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)), + domain_config.get(CONF_COUNTRY), + domain_config.get(CONF_CONTINENT) + ) + + devices = self.ecovacs_api.devices() + liveMapEnabled = domain_config.get(CONF_LIVEMAP) + liveMapRooms = domain_config.get(CONF_SHOWCOLORROOMS) + country = domain_config.get(CONF_COUNTRY).lower() + continent = domain_config.get(CONF_CONTINENT).lower() + self.vacbots = [] + + # CREATE VACBOT FOR EACH DEVICE + for device in devices: + if device['name'] in domain_config.get(CONF_DEVICEID): + vacbot = VacBot( + self.ecovacs_api.uid, + self.ecovacs_api.resource, + self.ecovacs_api.user_access_token, + device, + country, + continent, + liveMapEnabled, + liveMapRooms + ) + + _LOGGER.debug("New vacbot found: " + device['name']) + + self.vacbots.append(vacbot) + + _LOGGER.debug("Hub initialized") + + @Throttle(timedelta(seconds=10)) + def update(self): + """ Update all statuses. """ + try: + for vacbot in self.vacbots: + vacbot.request_all_statuses() + except Exception as ex: + _LOGGER.error('Update failed: %s', ex) + raise + + @property + def name(self): + """ Return the name of the hub.""" + return "Deebot Hub" \ No newline at end of file diff --git a/custom_components/deebot/__pycache__/__init__.cpython-38.pyc b/custom_components/deebot/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2947539407a85c429ec25015944d0401c450d620 GIT binary patch literal 3381 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 Optional[str]: + """Return the icon to use in the frontend, if any.""" + return "mdi:water" if self.is_on else "mdi:water-off" diff --git a/custom_components/deebot/manifest.json b/custom_components/deebot/manifest.json new file mode 100644 index 0000000..69ad740 --- /dev/null +++ b/custom_components/deebot/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "deebot", + "name": "Deebot for Hassio", + "documentation": "https://github.com/And3rsL/Deebot-for-hassio", + "requirements": [ + "deebotozmo==1.7.8" + ], + "dependencies": [], + "codeowners": ["@And3rsL"], + "homeassistant": "0.110.0" +} \ No newline at end of file diff --git a/custom_components/deebot/sensor.py b/custom_components/deebot/sensor.py new file mode 100644 index 0000000..4bb6b8f --- /dev/null +++ b/custom_components/deebot/sensor.py @@ -0,0 +1,179 @@ +"""Support for Deebot Sensor.""" +from typing import Optional + +from deebotozmo import * +from homeassistant.const import (STATE_UNKNOWN) +from homeassistant.helpers.entity import Entity + +from . import HUB as hub + +_LOGGER = logging.getLogger(__name__) + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, +) + +STATE_CODE_TO_STATE = { + 'STATE_IDLE': STATE_IDLE, + 'STATE_CLEANING': STATE_CLEANING, + 'STATE_RETURNING': STATE_RETURNING, + 'STATE_DOCKED': STATE_DOCKED, + 'STATE_ERROR': STATE_ERROR, + 'STATE_PAUSED': STATE_PAUSED, +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Deebot sensor.""" + hub.update() + + for vacbot in hub.vacbots: + # General + add_devices([DeebotLastCleanImageSensor(vacbot, "last_clean_image")], True) + add_devices([DeebotWaterLevelSensor(vacbot, "water_level")], True) + + # Components + add_devices([DeebotComponentSensor(vacbot, COMPONENT_MAIN_BRUSH)], True) + add_devices([DeebotComponentSensor(vacbot, COMPONENT_SIDE_BRUSH)], True) + add_devices([DeebotComponentSensor(vacbot, COMPONENT_FILTER)], True) + + # Stats + add_devices([DeebotStatsSensor(vacbot, "stats_area")], True) + add_devices([DeebotStatsSensor(vacbot, "stats_time")], True) + add_devices([DeebotStatsSensor(vacbot, "stats_type")], True) + + +class DeebotBaseSensor(Entity): + """Deebot base sensor""" + + def __init__(self, vacbot, device_id): + """Initialize the Sensor.""" + + self._state = STATE_UNKNOWN + self._vacbot = vacbot + self._id = device_id + + if self._vacbot.vacuum.get("nick", None) is not None: + self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"]) + else: + # In case there is no nickname defined, use the device id + self._vacbot_name = "{}".format(self._vacbot.vacuum["did"]) + + self._name = self._vacbot_name + "_" + device_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + +class DeebotLastCleanImageSensor(DeebotBaseSensor): + """Deebot Sensor""" + + def __init__(self, vacbot, device_id): + """Initialize the Sensor.""" + super(DeebotLastCleanImageSensor, self).__init__(vacbot, device_id) + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + if self._vacbot.last_clean_image is not None: + return self._vacbot.last_clean_image + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + return "mdi:image-search" + + +class DeebotWaterLevelSensor(DeebotBaseSensor): + """Deebot Sensor""" + + def __init__(self, vacbot, device_id): + """Initialize the Sensor.""" + super(DeebotWaterLevelSensor, self).__init__(vacbot, device_id) + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + + if self._vacbot.water_level is not None: + return self._vacbot.water_level + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + return "mdi:water" + + +class DeebotComponentSensor(DeebotBaseSensor): + """Deebot Sensor""" + + def __init__(self, vacbot, device_id): + """Initialize the Sensor.""" + super(DeebotComponentSensor, self).__init__(vacbot, device_id) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return '%' + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + + for key, val in self._vacbot.components.items(): + if key == self._id: + return int(val) + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + if self._id == COMPONENT_MAIN_BRUSH or self._id == COMPONENT_SIDE_BRUSH: + return "mdi:broom" + elif self._id == COMPONENT_FILTER: + return "mdi:air-filter" + + +class DeebotStatsSensor(DeebotBaseSensor): + """Deebot Sensor""" + + def __init__(self, vacbot, device_id): + """Initialize the Sensor.""" + super(DeebotStatsSensor, self).__init__(vacbot, device_id) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self._id == 'stats_area': + return "mq" + elif self._id == 'stats_time': + return "min" + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + + if self._id == 'stats_area' and self._vacbot.stats_area is not None: + return int(self._vacbot.stats_area) + elif self._id == 'stats_time' and self._vacbot.stats_time is not None: + return int(self._vacbot.stats_time/60) + elif self._id == 'stats_type': + return self._vacbot.stats_type + else: + return STATE_UNKNOWN + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + if self._id == 'stats_area': + return "mdi:floor-plan" + elif self._id == 'stats_time': + return "mdi:timer-outline" + elif self._id == 'stats_type': + return "mdi:cog" diff --git a/custom_components/deebot/vacuum.py b/custom_components/deebot/vacuum.py new file mode 100644 index 0000000..0987e94 --- /dev/null +++ b/custom_components/deebot/vacuum.py @@ -0,0 +1,247 @@ +"""Support for Deebot Vaccums.""" +import base64 +from typing import Optional, Dict, Any, Union, List + +from deebotozmo import * +from homeassistant.util import slugify + +from . import HUB as hub + +CONF_COUNTRY = "country" +CONF_CONTINENT = "continent" +CONF_DEVICEID = "deviceid" +CONF_LIVEMAPPATH = "livemappath" +CONF_LIVEMAP = "live_map" +CONF_SHOWCOLORROOMS = "show_color_rooms" +DEEBOT_DEVICES = "deebot_devices" + +from homeassistant.components.vacuum import ( + PLATFORM_SCHEMA, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_SEND_COMMAND, + SUPPORT_START, + SUPPORT_STATE, + VacuumEntity, +) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_DEEBOT = ( + SUPPORT_BATTERY + | SUPPORT_FAN_SPEED + | SUPPORT_LOCATE + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_SEND_COMMAND + | SUPPORT_START + | SUPPORT_STATE +) + +STATE_CODE_TO_STATE = { + 'STATE_IDLE': STATE_IDLE, + 'STATE_CLEANING': STATE_CLEANING, + 'STATE_RETURNING': STATE_RETURNING, + 'STATE_DOCKED': STATE_DOCKED, + 'STATE_ERROR': STATE_ERROR, + 'STATE_PAUSED': STATE_PAUSED, +} + +ATTR_COMPONENT_PREFIX = "component_" + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Deebot vacuums.""" + if DEEBOT_DEVICES not in hass.data: + hass.data[DEEBOT_DEVICES] = [] + + for vacbot in hub.vacbots: + vacuum = DeebotVacuum(hass, vacbot) + add_devices([vacuum]) + +class DeebotVacuum(VacuumEntity): + """Deebot Vacuums""" + + def __init__(self, hass, vacbot): + """Initialize the Deebot Vacuum.""" + self._hass = hass + + self.device = vacbot + + if self.device.vacuum.get("nick", None) is not None: + self._name = "{}".format(self.device.vacuum["nick"]) + else: + # In case there is no nickname defined, use the device id + self._name = "{}".format(self.device.vacuum["did"]) + + self._fan_speed = None + self._live_map = None + self._live_map_path = hub.config.get(CONF_LIVEMAPPATH) + self._name + '_liveMap.png' + + self.device.refresh_statuses() + + _LOGGER.debug("Vacuum initialized: %s", self.name) + + def on_fan_change(self, fan_speed): + self._fan_speed = fan_speed + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return True + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return self.device.vacuum.get("did", None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_DEEBOT + + @property + def state(self): + """Return the state of the vacuum cleaner.""" + if self.device.vacuum_status is not None and self.device.is_available == True: + return STATE_CODE_TO_STATE[self.device.vacuum_status] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.device.is_available + + async def async_return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + await self.hass.async_add_executor_job(self.device.Charge) + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + if self.device.battery_status is not None: + return self.device.battery_status + + return super().battery_level + + @property + def fan_speed(self): + """Return the fan speed of the vacuum cleaner.""" + return self.device.fan_speed + + async def async_set_fan_speed(self, fan_speed, **kwargs): + await self.hass.async_add_executor_job(self.device.SetFanSpeed, fan_speed) + + @property + def fan_speed_list(self): + """Get the list of available fan speed steps of the vacuum cleaner.""" + return [FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_MAX, FAN_SPEED_MAXPLUS] + + async def async_pause(self): + """Pause the vacuum cleaner.""" + await self.hass.async_add_executor_job(self.device.CleanPause) + + async def async_start(self): + """Start the vacuum cleaner.""" + await self.hass.async_add_executor_job(self.device.CleanResume) + + async def async_locate(self, **kwargs): + """Locate the vacuum cleaner.""" + await self.hass.async_add_executor_job(self.device.PlaySound) + + async def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + _LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs) + + if command == 'spot_area': + await self.hass.async_add_executor_job(self.device.SpotArea, params['rooms'], params['cleanings']) + return + + if command == 'custom_area': + await self.hass.async_add_executor_job(self.device.CustomArea, params['coordinates'], params['cleanings']) + return + + if command == 'set_water': + await self.hass.async_add_executor_job(self.device.SetWaterLevel, params['amount']) + return + + if command == 'relocate': + await self.hass.async_add_executor_job(self.device.Relocate) + return + + if command == 'auto_clean': + self.hass.async_add_executor_job(self.device.Clean, params['type']) + return + + if command == 'refresh_components': + await self.hass.async_add_executor_job(self.device.refresh_components) + return + + if command == 'refresh_statuses': + await self.hass.async_add_executor_job(self.device.refresh_statuses) + return + + if command == 'refresh_live_map': + await self.hass.async_add_executor_job(self.device.refresh_liveMap) + return + + if command == 'save_live_map': + if(self._live_map != self.device.live_map): + self._live_map = self.device.live_map + with open(params['path'], "wb") as fh: + fh.write(base64.decodebytes(self.device.live_map)) + + await self.hass.async_add_executor_job(self.device.exc_command, command, params) + + async def async_update(self): + """Fetch state from the device.""" + await self.hass.async_add_executor_job(self.device.request_all_statuses) + + try: + if(self._live_map != self.device.live_map): + self._live_map = self.device.live_map + with open(self._live_map_path, "wb") as fh: + fh.write(base64.decodebytes(self.device.live_map)) + except KeyError: + _LOGGER.warning("Can't access local folder: %s", self._live_map_path) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return device specific state attributes. + + Implemented by platform classes. Convention for attribute names + is lowercase snake_case. + """ + + data: Dict[str, Union[int, List[int]]] = {} + + # Needed for custom vacuum-card (https://github.com/denysdovhan/vacuum-card) + # Should find a better way without breaking everyone rooms script + data['status'] = STATE_CODE_TO_STATE[self.device.vacuum_status] + + if self.device.getSavedRooms() is not None: + for r in self.device.getSavedRooms(): + # convert room name to snake_case to meet the convention + room_name = "room_" + slugify(r["subtype"]) + room_values = data.get(room_name) + if room_values is None: + data[room_name] = r["id"] + elif isinstance(room_values, list): + room_values.append(r["id"]) + else: + # Convert from int to list + data[room_name] = [room_values, r["id"]] + + return data \ No newline at end of file diff --git a/custom_components/fontawesome/__init__.py b/custom_components/fontawesome/__init__.py new file mode 100644 index 0000000..0eb7576 --- /dev/null +++ b/custom_components/fontawesome/__init__.py @@ -0,0 +1,50 @@ +DOMAIN = "fontawesome" + +DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url' +ICONS_URL = f'/{DOMAIN}/' +ICON_FILES = { + 'regular': 'far.js', + 'solid': 'fas.js', + 'brands': 'fab.js', +} + + +async def async_setup(hass, config): + for f in ICON_FILES.values(): + hass.http.register_static_path( + f"/{DOMAIN}/{f}", + hass.config.path(f"custom_components/{DOMAIN}/data/{f}"), + True + ) + conf = config.get(DOMAIN) + if not conf: + return True + register_modules(hass, conf) + return True + + +async def async_setup_entry(hass, config_entry): + config_entry.add_update_listener(_update_listener) + register_modules(hass, config_entry.options) + return True + + +async def async_remove_entry(hass, config_entry): + register_modules(hass, []) + return True + + +async def _update_listener(hass, config_entry): + register_modules(hass, config_entry.options) + return True + + +def register_modules(hass, modules): + if DATA_EXTRA_MODULE_URL not in hass.data: + hass.data[DATA_EXTRA_MODULE_URL] = set() + url_set = hass.data[DATA_EXTRA_MODULE_URL] + + for k, v in ICON_FILES.items(): + url_set.discard(ICONS_URL+v) + if k in modules and modules[k] is not False: + url_set.add(ICONS_URL+v) diff --git a/custom_components/fontawesome/__pycache__/__init__.cpython-38.pyc b/custom_components/fontawesome/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a1350c2d62dc48d8698373ce22ec6cbae924a65 GIT binary patch literal 1409 zcmZ`(&2Aev5ay8FAIY+#IF3{I2Pg^@$o3GSKo30xMU2=13^;NT*=QlOWj7+_tS#*- zB;~}x>JX&w&_fzPI{G>K7QFT?0;K40*9~m9*#&3FA(!9Gd^3Bnyxb)C{QmNXU;pNW z{N)!HAGDEsILs~zN%}-WA0$=eHIeiU>r=@jmw^obV0|Vd8UIH5TsCAAbs(2y3w0=$ z9`CjrKv2jUC_KE`Wt0d@Z zhA1yq2UBBV+~4bp2RpmlJr|y&qp32EKe2Y=)-b5d zSfz!rsm%srlG-OO+}eA%xzlywpvZ^Wq2ra69V+YE??0&XW)hB9rN-zbbi~2XasSa7 z(W|(A`*o+jqVrz|FL2TkBFi%?#GT2hTT0DoJ`hIP>7<2sm~|8cS`fkttOI=)22J4j zHbJl9&Lsjo89pNw0UzT8Q%Ec<0Ek5bN;s;M2R(8|yZh+;jZ{i8onWq%82MD>O1t(| zi(bLITvSYKR^;Z4>T7=8s)G}laD7?@T#Fli^ZJE2>J#1{6QUsKdkgd@IQy{o9?_DL z9FmG|^jP0SQv#JQdIRTIAy;cit;WTPsxd#uW5$=I$JC#q{xD)2cReCz2`*#O8uKGO z@PEMi7H)ir;~WWN^Oj3U0UG?c?~e^pc%&mZj`X^vud2BvchB_(nNiE;ylS0qf1)!s z<0YL3HZ1uq;@K=&3CE$nT|&t=u$_(@IH%Yy?@v6vEeW;=?LJ6icXMlVe^YG#xc_KV zJlxwl*xeQfk9HmRE#er~n)bZ7AhT+0TqH9yNVRkg-#ESMD8o8Qf|~c}y!Ks$e+^%$ z4vrn1T%n`N*;hVXPi4({leV#n*J=*CNkjX;h5pQoFTC(xR8?ISN5xl_$~ynPrLKUu d_-^gYcYvE!uW-ttMJeF>E{x)pIE;fh_y?yjL8Jfx literal 0 HcmV?d00001 diff --git a/custom_components/fontawesome/config_flow.py b/custom_components/fontawesome/config_flow.py new file mode 100644 index 0000000..0e5380b --- /dev/null +++ b/custom_components/fontawesome/config_flow.py @@ -0,0 +1,49 @@ +import logging +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register("fontawesome") +class FontawesomeConfigFlow(config_entries.ConfigFlow): + async def async_step_user(self, user_input=None): + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return self.async_create_entry(title="", data={}) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + return FontawesomeEditFlow(config_entry) + + +class FontawesomeEditFlow(config_entries.OptionsFlow): + def __init__(self, config_entry): + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + "regular", + default=self.config_entry.options.get("regular", False), + ): bool, + vol.Optional( + "solid", + default=self.config_entry.options.get("solid", False), + ): bool, + vol.Optional( + "brands", + default=self.config_entry.options.get("brands", False), + ): bool, + } + ) + ) diff --git a/custom_components/fontawesome/data/fab.js b/custom_components/fontawesome/data/fab.js new file mode 100644 index 0000000..d71a560 --- /dev/null +++ b/custom_components/fontawesome/data/fab.js @@ -0,0 +1 @@ +!function(c){var l={};function h(m){if(l[m])return l[m].exports;var z=l[m]={i:m,l:!1,exports:{}};return c[m].call(z.exports,z,z.exports,h),z.l=!0,z.exports}h.m=c,h.c=l,h.d=function(c,l,m){h.o(c,l)||Object.defineProperty(c,l,{enumerable:!0,get:m})},h.r=function(c){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(c,"__esModule",{value:!0})},h.t=function(c,l){if(1&l&&(c=h(c)),8&l)return c;if(4&l&&"object"==typeof c&&c&&c.__esModule)return c;var m=Object.create(null);if(h.r(m),Object.defineProperty(m,"default",{enumerable:!0,value:c}),2&l&&"string"!=typeof c)for(var z in c)h.d(m,z,function(l){return c[l]}.bind(null,z));return m},h.n=function(c){var l=c&&c.__esModule?function(){return c.default}:function(){return c};return h.d(l,"a",l),l},h.o=function(c,l){return Object.prototype.hasOwnProperty.call(c,l)},h.p="",h(h.s=0)}([function(c,l,h){"use strict";h.r(l);const m=document.createElement("ha-iconset-svg");if(m.name="fab",m.size="1024",m.innerHTML='\n\x3c!--\nFont Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com\nLicense - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)\n--\x3e\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n'.replace(/\/g,""),!customElements.get("ha-iconset-svg")){const c=document.createElement("iron-meta");c.type="iconset",c.key=m.name,c.value=m,m.appendChild(c),m.applyIcon=function(c,l){this.removeIcon(c);const h=document.createElementNS("http://www.w3.org/2000/svg","svg"),m=this.querySelector(`[id='${l}']`).cloneNode(!0);return h.appendChild(m),h.setAttribute("viewBox",m.getAttribute("viewBox")||"0 0 24 24"),h.style.cssText="pointer-events: none; display: block; width: 100%; height: 100%;",c.shadowRoot.insertBefore(h,c.shadowRoot.childNodes[0]),c._svgIcon=h}.bind(m),m.removeIcon=function(c){c._svgIcon&&(c.shadowRoot.removeChild(c._svgIcon),c._svgIcon=null)}.bind(m)}document.body.appendChild(m)}]); \ No newline at end of file diff --git a/custom_components/fontawesome/data/far.js b/custom_components/fontawesome/data/far.js new file mode 100644 index 0000000..042c155 --- /dev/null +++ b/custom_components/fontawesome/data/far.js @@ -0,0 +1 @@ +!function(c){var h={};function l(m){if(h[m])return h[m].exports;var s=h[m]={i:m,l:!1,exports:{}};return c[m].call(s.exports,s,s.exports,l),s.l=!0,s.exports}l.m=c,l.c=h,l.d=function(c,h,m){l.o(c,h)||Object.defineProperty(c,h,{enumerable:!0,get:m})},l.r=function(c){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(c,"__esModule",{value:!0})},l.t=function(c,h){if(1&h&&(c=l(c)),8&h)return c;if(4&h&&"object"==typeof c&&c&&c.__esModule)return c;var m=Object.create(null);if(l.r(m),Object.defineProperty(m,"default",{enumerable:!0,value:c}),2&h&&"string"!=typeof c)for(var s in c)l.d(m,s,function(h){return c[h]}.bind(null,s));return m},l.n=function(c){var h=c&&c.__esModule?function(){return c.default}:function(){return c};return l.d(h,"a",h),h},l.o=function(c,h){return Object.prototype.hasOwnProperty.call(c,h)},l.p="",l(l.s=0)}([function(c,h,l){"use strict";l.r(h);const m=document.createElement("ha-iconset-svg");if(m.name="far",m.size="1024",m.innerHTML='\n\x3c!--\nFont Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com\nLicense - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)\n--\x3e\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n'.replace(/\/g,""),!customElements.get("ha-iconset-svg")){const c=document.createElement("iron-meta");c.type="iconset",c.key=m.name,c.value=m,m.appendChild(c),m.applyIcon=function(c,h){this.removeIcon(c);const l=document.createElementNS("http://www.w3.org/2000/svg","svg"),m=this.querySelector(`[id='${h}']`).cloneNode(!0);return l.appendChild(m),l.setAttribute("viewBox",m.getAttribute("viewBox")||"0 0 24 24"),l.style.cssText="pointer-events: none; display: block; width: 100%; height: 100%;",c.shadowRoot.insertBefore(l,c.shadowRoot.childNodes[0]),c._svgIcon=l}.bind(m),m.removeIcon=function(c){c._svgIcon&&(c.shadowRoot.removeChild(c._svgIcon),c._svgIcon=null)}.bind(m)}document.body.appendChild(m)}]); \ No newline at end of file diff --git a/custom_components/fontawesome/data/fas.js b/custom_components/fontawesome/data/fas.js new file mode 100644 index 0000000..a918d40 --- /dev/null +++ b/custom_components/fontawesome/data/fas.js @@ -0,0 +1 @@ +!function(c){var h={};function l(a){if(h[a])return h[a].exports;var v=h[a]={i:a,l:!1,exports:{}};return c[a].call(v.exports,v,v.exports,l),v.l=!0,v.exports}l.m=c,l.c=h,l.d=function(c,h,a){l.o(c,h)||Object.defineProperty(c,h,{enumerable:!0,get:a})},l.r=function(c){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(c,"__esModule",{value:!0})},l.t=function(c,h){if(1&h&&(c=l(c)),8&h)return c;if(4&h&&"object"==typeof c&&c&&c.__esModule)return c;var a=Object.create(null);if(l.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:c}),2&h&&"string"!=typeof c)for(var v in c)l.d(a,v,function(h){return c[h]}.bind(null,v));return a},l.n=function(c){var h=c&&c.__esModule?function(){return c.default}:function(){return c};return l.d(h,"a",h),h},l.o=function(c,h){return Object.prototype.hasOwnProperty.call(c,h)},l.p="",l(l.s=0)}([function(c,h,l){"use strict";l.r(h);const a=document.createElement("ha-iconset-svg");if(a.name="fas",a.size="1024",a.innerHTML='\n\x3c!--\nFont Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com\nLicense - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)\n--\x3e\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n'.replace(/\/g,""),!customElements.get("ha-iconset-svg")){const c=document.createElement("iron-meta");c.type="iconset",c.key=a.name,c.value=a,a.appendChild(c),a.applyIcon=function(c,h){this.removeIcon(c);const l=document.createElementNS("http://www.w3.org/2000/svg","svg"),a=this.querySelector(`[id='${h}']`).cloneNode(!0);return l.appendChild(a),l.setAttribute("viewBox",a.getAttribute("viewBox")||"0 0 24 24"),l.style.cssText="pointer-events: none; display: block; width: 100%; height: 100%;",c.shadowRoot.insertBefore(l,c.shadowRoot.childNodes[0]),c._svgIcon=l}.bind(a),a.removeIcon=function(c){c._svgIcon&&(c.shadowRoot.removeChild(c._svgIcon),c._svgIcon=null)}.bind(a)}document.body.appendChild(a)}]); \ No newline at end of file diff --git a/custom_components/fontawesome/manifest.json b/custom_components/fontawesome/manifest.json new file mode 100644 index 0000000..f90f54f --- /dev/null +++ b/custom_components/fontawesome/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "fontawesome", + "name": "Fontawesome icons", + "documentation": "", + "dependencies": ["frontend"], + "codeowners": [], + "requirements": [], + "config_flow": true +} \ No newline at end of file diff --git a/custom_components/fontawesome/translations/en.json b/custom_components/fontawesome/translations/en.json new file mode 100644 index 0000000..84b9633 --- /dev/null +++ b/custom_components/fontawesome/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "FontAwesome", + "abort": { + "single_instance_allowed": "Only a single configuration of FontAwesome is allowed." + } + }, + "options": { + "step": { + "init": { + "title": "Icon sets", + "description": "Which icon sets to include", + "data": { + "regular": "Include Regular icons (far:)", + "solid": "Include Solid icons (fas:)", + "brands": "Include Brand icons (fab:)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/landroid_cloud/__init__.py b/custom_components/landroid_cloud/__init__.py index 1e0bac9..71685b6 100644 --- a/custom_components/landroid_cloud/__init__.py +++ b/custom_components/landroid_cloud/__init__.py @@ -57,11 +57,11 @@ API_WORX_SENSORS = { }, "icon": "mdi:battery", "unit": "%", - "device_class": None, + "device_class": "battery", }, "error": { "state": {"error_description": "state", "error": "error_id"}, - "icon": None, + "icon": "mdi:alert", "unit": None, "device_class": None, }, @@ -83,6 +83,8 @@ API_WORX_SENSORS = { "rain_delay": "raindelay", "schedule_variation": "timeextension", "firmware": "firmware_version", + "serial": "serial", + "mac": "mac", }, "icon": None, "unit": None, @@ -130,7 +132,7 @@ async def async_setup(hass, config): async def handle_start(call): """Handle start service call.""" if "id" in call.data: - ID = call.data["id"] + ID = int(call.data["id"]) for cli in client: attrs = vars(cli) @@ -144,7 +146,7 @@ async def async_setup(hass, config): async def handle_pause(call): """Handle pause service call.""" if "id" in call.data: - ID = call.data["id"] + ID = int(call.data["id"]) for cli in client: attrs = vars(cli) @@ -158,7 +160,7 @@ async def async_setup(hass, config): async def handle_home(call): """Handle pause service call.""" if "id" in call.data: - ID = call.data["id"] + ID = int(call.data["id"]) for cli in client: attrs = vars(cli) @@ -177,31 +179,46 @@ async def async_setup(hass, config): if "id" in call.data: _LOGGER.debug("Data from Home Assistant: %s", call.data["id"]) - + for cli in client: attrs = vars(cli) - if (attrs["id"] == call.data["id"]): + if (attrs["id"] == int(call.data["id"])): break else: id += 1 if "raindelay" in call.data: - tmpdata["rd"] = call.data["raindelay"] + tmpdata["rd"] = int(call.data["raindelay"]) _LOGGER.debug("Setting rain_delay for %s to %s", client[id].name, call.data["raindelay"]) sendData = True if "timeextension" in call.data: - tmpdata["sc"] = {} - tmpdata["sc"]["p"] = call.data["timeextension"] + tmpdata["sc"] = {} + tmpdata["sc"]["p"] = int(call.data["timeextension"]) data = json.dumps(tmpdata) _LOGGER.debug("Setting time_extension for %s to %s", client[id].name, call.data["timeextension"]) sendData = True + if "multizone_distances" in call.data: + tmpdata["mz"] = [int(x) for x in call.data["multizone_distances"]] + data = json.dumps(tmpdata) + _LOGGER.debug("Setting multizone distances for %s to %s", client[id].name, call.data["multizone_distances"]) + sendData = True + + if "multizone_probabilities" in call.data: + tmpdata["mzv"] = [] + for idx, val in enumerate(call.data["multizone_probabilities"]): + for _ in range(val): + tmpdata["mzv"].append(idx) + data = json.dumps(tmpdata) + _LOGGER.debug("Setting multizone probabilities for %s to %s", client[id].name, call.data["multizone_probabilities"]) + sendData = True + if sendData: data = json.dumps(tmpdata) _LOGGER.debug("Sending: %s", data) client[id].sendData(data) - + hass.services.async_register(DOMAIN, SERVICE_CONFIG, handle_config) return True diff --git a/custom_components/landroid_cloud/manifest.json b/custom_components/landroid_cloud/manifest.json index 779ed0b..9adf556 100644 --- a/custom_components/landroid_cloud/manifest.json +++ b/custom_components/landroid_cloud/manifest.json @@ -1,7 +1,9 @@ { "domain": "landroid_cloud", "name": "Worx Landroid Cloud", - "documentation": "https://www.home-assistant.io/integrations/landroid_cloud/", - "requirements": ["pyworxcloud==1.2.17"], + "documentation": "https://github.com/MTrab/landroid_cloud/blob/master/README.md", + "issue_tracker": "https://github.com/MTrab/landroid_cloud/issues", + "requirements": ["pyworxcloud==1.2.21"], + "version": "1.6.5", "codeowners": ["@MTrab"] } diff --git a/custom_components/landroid_cloud/sensor.py b/custom_components/landroid_cloud/sensor.py index 3eeb6d5..d3bed19 100644 --- a/custom_components/landroid_cloud/sensor.py +++ b/custom_components/landroid_cloud/sensor.py @@ -1,127 +1,132 @@ -"""Support for monitoring Worx Landroid Sensors.""" -import async_timeout -import asyncio -import logging - -from homeassistant.components import sensor -from homeassistant.const import STATE_UNKNOWN -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity - -from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL - -_LOGGER = logging.getLogger(__name__) - -STATE_INITIALIZING = "Initializing" -STATE_OFFLINE = "Offline" - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the available sensors for Worx Landroid.""" - if discovery_info is None: - return - - entities = [] - - info = discovery_info[0] - for tSensor in API_WORX_SENSORS: - name = "{}_{}".format(info["name"].lower(), tSensor.lower()) - friendly_name = "{} {}".format(info["friendly"], tSensor) - dev_id = info["id"] - api = hass.data[LANDROID_API][dev_id] - sensor_type = tSensor - _LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"]) - entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id) - entities.append(entity) - - async_add_entities(entities, True) - - -class LandroidSensor(Entity): - """Class to create and populate a Landroid Sensor.""" - - def __init__(self, api, name, sensor_type, friendly_name, dev_id): - """Init new sensor.""" - - self._api = api - self._attributes = {} - self._available = False - self._name = friendly_name - self._state = STATE_INITIALIZING - self._sensor_type = sensor_type - self._dev_id = dev_id - self.entity_id = sensor.ENTITY_ID_FORMAT.format(name) - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return sensor attributes.""" - return self._attributes - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return API_WORX_SENSORS[self._sensor_type]["unit"] - - @property - def icon(self): - """Icon to use in the frontend.""" - return API_WORX_SENSORS[self._sensor_type]["icon"] - - @property - def should_poll(self): - """Return False as entity is updated from the component.""" - return False - - @property - def state(self): - """Return sensor state.""" - return self._state - - @callback - def update_callback(self): - """Get new data and update state.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Connect update callbacks.""" - async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback) - - def _get_data(self): - """Return new data from the api cache.""" - data = self._api.get_data(self._sensor_type) - self._available = True - return data - - async def async_update(self): - """Update the sensor.""" - _LOGGER.debug("Updating %s", self.entity_id) - data = self._get_data() - if "state" in data: - _LOGGER.debug(data) - state = data.pop("state") - _LOGGER.debug("Mower %s State %s", self._name, state) - self._attributes.update(data) - self._state = state - else: - _LOGGER.debug("No data received for %s", self.entity_id) - reachable = self._api._client.online - if not reachable: - if "_battery" in self.entity_id: - self._state = "Unknown" - else: - self._state = STATE_OFFLINE - #else: - # attrs = vars(self._api._client) - # for item in attrs: - # _LOGGER.debug("%s : %s", item, attrs[item]) \ No newline at end of file +"""Support for monitoring Worx Landroid Sensors.""" +import async_timeout +import asyncio +import logging + +from homeassistant.components import sensor +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level + +from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL + +_LOGGER = logging.getLogger(__name__) + +STATE_INITIALIZING = "Initializing" +STATE_OFFLINE = "Offline" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the available sensors for Worx Landroid.""" + if discovery_info is None: + return + + entities = [] + + info = discovery_info[0] + for tSensor in API_WORX_SENSORS: + name = "{}_{}".format(info["name"].lower(), tSensor.lower()) + friendly_name = "{} {}".format(info["friendly"], tSensor) + dev_id = info["id"] + api = hass.data[LANDROID_API][dev_id] + sensor_type = tSensor + _LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"]) + entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id) + entities.append(entity) + + async_add_entities(entities, True) + + +class LandroidSensor(Entity): + """Class to create and populate a Landroid Sensor.""" + + def __init__(self, api, name, sensor_type, friendly_name, dev_id): + """Init new sensor.""" + + self._api = api + self._attributes = {} + self._available = False + self._name = friendly_name + self._state = STATE_INITIALIZING + self._sensor_type = sensor_type + self._dev_id = dev_id + self.entity_id = sensor.ENTITY_ID_FORMAT.format(name) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return sensor attributes.""" + return self._attributes + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return API_WORX_SENSORS[self._sensor_type]["unit"] + + @property + def icon(self): + """Icon to use in the frontend.""" + if self._sensor_type == "battery" and isinstance(self.state, int): + charging = self._attributes["charging"] + return icon_for_battery_level(battery_level=self.state, charging=charging) + + return API_WORX_SENSORS[self._sensor_type]["icon"] + + @property + def should_poll(self): + """Return False as entity is updated from the component.""" + return False + + @property + def state(self): + """Return sensor state.""" + return self._state + + @callback + def update_callback(self): + """Get new data and update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Connect update callbacks.""" + async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback) + + def _get_data(self): + """Return new data from the api cache.""" + data = self._api.get_data(self._sensor_type) + self._available = True + return data + + async def async_update(self): + """Update the sensor.""" + _LOGGER.debug("Updating %s", self.entity_id) + data = self._get_data() + if "state" in data: + _LOGGER.debug(data) + state = data.pop("state") + _LOGGER.debug("Mower %s State %s", self._name, state) + self._attributes.update(data) + self._state = state + if "latitude" in self._attributes: + if self._attributes["latitude"] == None: + del self._attributes["latitude"] + del self._attributes["longitude"] + else: + _LOGGER.debug("No data received for %s", self.entity_id) + reachable = self._api._client.online + if not reachable: + if "_battery" in self.entity_id: + self._state = STATE_UNKNOWN + else: + self._state = STATE_OFFLINE diff --git a/custom_components/landroid_cloud/services.yaml b/custom_components/landroid_cloud/services.yaml index 2061315..42f949b 100644 --- a/custom_components/landroid_cloud/services.yaml +++ b/custom_components/landroid_cloud/services.yaml @@ -28,3 +28,9 @@ config: timeextension: description: Set time extension. Extension in % ranging from -100 to 100 example: -23 + multizone_distances: + description: Set multizone distances. Distances in meter. 0 = Disabled + example: '[15, 80, 120, 155]' + multizone_probabilities: + description: Set multizone probabilities. Probabilities in parts-of-ten. 1 = 10%, 2 = 20%, ... + example: '[5, 1, 2, 2]' diff --git a/custom_components/reolink_dev/ReolinkPyPi/__init__.py b/custom_components/reolink_dev/ReolinkPyPi/__init__.py deleted file mode 100644 index 7461c90..0000000 --- a/custom_components/reolink_dev/ReolinkPyPi/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Reolink Camera component for HomeAssistant.""" \ No newline at end of file diff --git a/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index e655c5c7cc6ca3c1e5081c190a7a189294d97f31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 203 zcmZ?b<>g`kf_zVzI71-)7{q}AMj*ohh>JOZL<&O`LkeRsgCa(-?>eqL%`i9%X_k%C8lZmMH(ab|HzVqS@!pC;oi_W1ae{N(ufl?+8pK$F14 zFI)ZO{JgZxbp7Pg;*$K_c(@_O`b7}q<5N=0^r2P-R0d?~$H!;pWtPOp>lIYq;;_lh QPbtkwwF5cwGY~TX0Alw!Pyhe` diff --git a/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-38.pyc b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index b0cc501c87542ad765f0621e1f602eded8941142..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207 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 T2lP6LFrqBkz=a6^w2Jm0p=Wc~ diff --git a/custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-38.pyc b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-38.pyc deleted file mode 100644 index f029e3ad6e87e56ac17c399915c7d2248be3ba76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7238 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 E54$61r~m)} diff --git a/custom_components/reolink_dev/ReolinkPyPi/camera.py b/custom_components/reolink_dev/ReolinkPyPi/camera.py deleted file mode 100644 index 67d0f2a..0000000 --- a/custom_components/reolink_dev/ReolinkPyPi/camera.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Reolink Camera API -""" -import requests -import datetime -import json -import logging - -_LOGGER = logging.getLogger(__name__) - -class ReolinkApi(object): - def __init__(self, ip, channel): - self._url = "http://" + ip + "/cgi-bin/api.cgi" - self._ip = ip - self._channel = channel - self._token = None - self._motion_state = False - self._last_motion = 0 - self._device_info = None - self._motion_state = None - self._ftp_state = None - self._email_state = None - self._ir_state = None - self._rtspport = None - self._rtmpport = None - self._ptzpresets = dict() - - def status(self): - if self._token is None: - return - - param_channel = {"channel": self._channel} - body = [{"cmd": "GetDevInfo", "action":1, "param": param_channel}, - {"cmd": "GetNetPort", "action": 1, "param": param_channel}, - {"cmd": "GetFtp", "action": 1, "param": param_channel}, - {"cmd": "GetEmail", "action": 1, "param": param_channel}, - {"cmd": "GetIrLights", "action": 1, "param": param_channel}, - {"cmd": "GetPtzPreset", "action": 1, "param": param_channel}] - - param = {"token": self._token} - response = self.send(body, param) - - try: - json_data = json.loads(response.text) - except: - _LOGGER.error(f"Error translating response to json") - return - - for data in json_data: - try: - if data["cmd"] == "GetDevInfo": - self._device_info = data - - elif data["cmd"] == "GetNetPort": - self._netport_settings = data - self._rtspport = data["value"]["NetPort"]["rtspPort"] - self._rtmpport = data["value"]["NetPort"]["rtmpPort"] - - elif data["cmd"] == "GetFtp": - self._ftp_settings = data - if (data["value"]["Ftp"]["schedule"]["enable"] == 1): - self._ftp_state = True - else: - self._ftp_state = False - - elif data["cmd"] == "GetEmail": - self._email_settings = data - if (data["value"]["Email"]["schedule"]["enable"] == 1): - self._email_state = True - else: - self._email_state = False - - elif data["cmd"] == "GetIrLights": - self._ir_settings = data - if (data["value"]["IrLights"]["state"] == "Auto"): - self._ir_state = True - else: - self._ir_state = False - - elif data["cmd"] == "GetPtzPreset": - self._ptzpresets_settings = data - for preset in data["value"]["PtzPreset"]: - if int(preset["enable"]) == 1: - preset_name = preset["name"] - preset_id = int(preset["id"]) - self._ptzpresets[preset_name] = preset_id - _LOGGER.debug(f"Got preset {preset_name} with ID {preset_id}") - else: - _LOGGER.debug(f"Preset is not enabled: {preset}") - except: - continue - - @property - def motion_state(self): - body = [{"cmd": "GetMdState", "action": 0, "param":{"channel":self._channel}}] - param = {"token": self._token} - - response = self.send(body, param) - - try: - json_data = json.loads(response.text) - - if json_data is None: - _LOGGER.error(f"Unable to get Motion detection state at IP {self._ip}") - self._motion_state = False - return self._motion_state - - if json_data[0]["value"]["state"] == 1: - self._motion_state = True - self._last_motion = datetime.datetime.now() - else: - self._motion_state = False - except: - self._motion_state = False - - return self._motion_state - - @property - def still_image(self): - response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=True) - response.raw.decode_content = True - return response.raw - - @property - def snapshot(self): - response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=False) - return response.content - - @property - def ftp_state(self): - return self._ftp_state - - @property - def email_state(self): - return self._email_state - - @property - def ir_state(self): - return self._ir_state - - @property - def rtmpport(self): - return self._rtmpport - - @property - def rtspport(self): - return self._rtspport - - @property - def last_motion(self): - return self._last_motion - - @property - def ptzpresets(self): - return self._ptzpresets - - def login(self, username, password): - body = [{"cmd": "Login", "action": 0, "param": {"User": {"userName": username, "password": password}}}] - param = {"cmd": "Login", "token": "null"} - - response = self.send(body, param) - - try: - json_data = json.loads(response.text) - except: - _LOGGER.error(f"Error translating login response to json") - return - - if json_data is not None: - if json_data[0]["code"] == 0: - self._token = json_data[0]["value"]["Token"]["name"] - _LOGGER.info(f"Reolink camera logged in at IP {self._ip}") - else: - _LOGGER.error(f"Failed to login at IP {self._ip}. No token available") - else: - _LOGGER.error(f"Failed to login at IP {self._ip}. Connection error.") - - def logout(self): - body = [{"cmd":"Logout","action":0,"param":{}}] - param = {"cmd": "Logout", "token": self._token} - - self.send(body, param) - - def set_ftp(self, enabled): - self.status() - - if not self._ftp_settings: - _LOGGER.error("Error while fetching current FTP settings") - return - - if enabled == True: - newValue = 1 - else: - newValue = 0 - - body = [{"cmd":"SetFtp","action":0,"param": self._ftp_settings["value"] }] - body[0]["param"]["Ftp"]["schedule"]["enable"] = newValue - - response = self.send(body, {"cmd": "SetFtp", "token": self._token} ) - try: - json_data = json.loads(response.text) - if json_data[0]["value"]["rspCode"] == 200: - return True - else: - return False - except: - _LOGGER.error(f"Error translating FTP response to json") - return False - - def set_email(self, enabled): - self.status() - - if not self._email_settings: - _LOGGER.error("Error while fetching current email settings") - return - - if enabled == True: - newValue = 1 - else: - newValue = 0 - - body = [{"cmd":"SetEmail","action":0,"param": self._email_settings["value"] }] - body[0]["param"]["Email"]["schedule"]["enable"] = newValue - - response = self.send(body, {"cmd": "SetEmail", "token": self._token} ) - try: - json_data = json.loads(response.text) - if json_data[0]["value"]["rspCode"] == 200: - return True - else: - return False - except: - _LOGGER.error(f"Error translating Email response to json") - return False - - def set_ir_lights(self, enabled): - self.status() - - if not self._ir_settings: - _LOGGER.error("Error while fetching current IR light settings") - return - - if enabled == True: - newValue = "Auto" - else: - newValue = "Off" - - body = [{"cmd":"SetIrLights","action":0,"param": self._ir_settings["value"] }] - body[0]["param"]["IrLights"]["state"] = newValue - - response = self.send(body, {"cmd": "SetIrLights", "token": self._token} ) - try: - json_data = json.loads(response.text) - if json_data[0]["value"]["rspCode"] == 200: - return True - else: - return False - except requests.exceptions.RequestException: - _LOGGER.error(f"Error translating IR Lights response to json") - return False - - def send(self, body, param, stream=False): - try: - if (self._token is None and - (body is None or body[0]["cmd"] != "Login")): - _LOGGER.info(f"Reolink camera at IP {self._ip} is not logged in") - return - - if body is None: - response = requests.get(self._url, params=param, stream=stream) - else: - response = requests.post(self._url, data=json.dumps(body), params=param) - - return response - except Exception: - _LOGGER.error(f"Exception while calling Reolink camera API at ip {self._ip}") - return None - diff --git a/custom_components/reolink_dev/__init__.py b/custom_components/reolink_dev/__init__.py index 7461c90..0900ebc 100644 --- a/custom_components/reolink_dev/__init__.py +++ b/custom_components/reolink_dev/__init__.py @@ -1 +1,160 @@ -"""Reolink Camera component for HomeAssistant.""" \ No newline at end of file +"""Reolink integration for HomeAssistant.""" +import asyncio +from datetime import timedelta +import logging + +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_TIMEOUT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .base import ReolinkBase, ReolinkPush +from .const import ( + BASE, + CONF_CHANNEL, + CONF_MOTION_OFF_DELAY, + CONF_PLAYBACK_MONTHS, + CONF_PLAYBACK_THUMBNAILS, + CONF_PROTOCOL, + CONF_STREAM, + CONF_THUMBNAIL_OFFSET, + COORDINATOR, + DEFAULT_PLAYBACK_THUMBNAILS, + DEFAULT_THUMBNAIL_OFFSET, + DOMAIN, + EVENT_DATA_RECEIVED, + PUSH_MANAGER, + SERVICE_PTZ_CONTROL, + SERVICE_SET_DAYNIGHT, + SERVICE_SET_SENSITIVITY, +) + +SCAN_INTERVAL = timedelta(minutes=1) + + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["camera", "switch", "binary_sensor"] + + +async def async_setup( + hass: HomeAssistant, config: dict +): # pylint: disable=unused-argument + """Set up the Reolink component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Reolink from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + + base = ReolinkBase(hass, entry.data, entry.options) + base.sync_functions.append(entry.add_update_listener(update_listener)) + + if not await base.connect_api(): + return False + hass.data[DOMAIN][entry.entry_id] = {BASE: base} + + try: + """Get a push manager, there should be one push manager per mac address""" + push = hass.data[DOMAIN][base.push_manager] + except KeyError: + push = ReolinkPush( + hass, + base.api.host, + base.api.onvif_port, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + ) + await push.subscribe(base.event_id) + hass.data[DOMAIN][base.push_manager] = push + + async def async_update_data(): + """Perform the actual updates.""" + + async with async_timeout.timeout(base.timeout): + await push.renew() + await base.update_states() + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="reolink", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, base.stop()) + + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Update the configuration at the base entity and API.""" + base: ReolinkBase = hass.data[DOMAIN][entry.entry_id][BASE] + + base.motion_off_delay = entry.options[CONF_MOTION_OFF_DELAY] + base.playback_months = entry.options[CONF_PLAYBACK_MONTHS] + base.playback_thumbnails = entry.options.get( + CONF_PLAYBACK_THUMBNAILS, DEFAULT_PLAYBACK_THUMBNAILS + ) + base.playback_thumbnail_offset = entry.options.get( + CONF_THUMBNAIL_OFFSET, DEFAULT_THUMBNAIL_OFFSET + ) + + await base.set_timeout(entry.options[CONF_TIMEOUT]) + await base.set_protocol(entry.options[CONF_PROTOCOL]) + await base.set_stream(entry.options[CONF_STREAM]) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + base = hass.data[DOMAIN][entry.entry_id][BASE] + push = hass.data[DOMAIN][base.push_manager] + + if not await push.count_members() > 1: + await push.unsubscribe() + hass.data[DOMAIN].pop(base.push_manager) + + await base.stop() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + if len(hass.data[DOMAIN]) == 0: + hass.services.async_remove(DOMAIN, SERVICE_PTZ_CONTROL) + hass.services.async_remove(DOMAIN, SERVICE_SET_DAYNIGHT) + hass.services.async_remove(DOMAIN, SERVICE_SET_SENSITIVITY) + + return unload_ok diff --git a/custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc b/custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 61393d381d42a232d701b09b491965d6c2b77068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 191 zcmZ?b<>g`kf_zVzI71-)7{q}AMj*ohh>JOZL<&O`LkeRsgCa(-?>eqL%`i9%X_k%C8lZmMH(ab|HzVqS@!pC;oi_W1ae{N(ufl?+8pK$F14 zFGKz0{JgZxbp7Pg;*$K_c(@_O`b7}q<5N=0^yA|*^D;}~g`kf_zVzI71-)7{oyaj6jY95EpX*i4=w?h7`tN22G|a?V!~BoXosz1?R-v z)S^U%5{0z_Zb#JmzcKTXD4?D6p_`N{F|D;bKIfF^;7 zUxxb0`FUxX>H5i~#U=T<@o+Bq-s=4F<|$LkeT-r}&y%}*)KNwotx J>@yHE0072&G|~V7 diff --git a/custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc b/custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc deleted file mode 100644 index 4d778bf1e0b034a360758d54c035bdd2d502b508..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10496 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 MKiUs^8a4O-7r|j&IRF3v diff --git a/custom_components/reolink_dev/base.py b/custom_components/reolink_dev/base.py new file mode 100644 index 0000000..b799e7a --- /dev/null +++ b/custom_components/reolink_dev/base.py @@ -0,0 +1,366 @@ +"""This component updates the camera API and subscription.""" +import logging +import re + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_TIMEOUT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import get_url +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_entity_registry, +) + +from reolink.camera_api import Api +from reolink.subscription_manager import Manager + +from .const import ( + BASE, + CONF_PLAYBACK_MONTHS, + CONF_PLAYBACK_THUMBNAILS, + CONF_THUMBNAIL_OFFSET, + DEFAULT_PLAYBACK_MONTHS, + DEFAULT_PLAYBACK_THUMBNAILS, + DEFAULT_THUMBNAIL_OFFSET, + EVENT_DATA_RECEIVED, + CONF_CHANNEL, + CONF_MOTION_OFF_DELAY, + CONF_PROTOCOL, + CONF_STREAM, + DEFAULT_CHANNEL, + DEFAULT_MOTION_OFF_DELAY, + DEFAULT_PROTOCOL, + DEFAULT_STREAM, + DEFAULT_TIMEOUT, + DOMAIN, + PUSH_MANAGER, + SESSION_RENEW_THRESHOLD, +) + +_LOGGER = logging.getLogger(__name__) + + +class ReolinkBase: + """The implementation of the Reolink IP base class.""" + + def __init__( + self, hass: HomeAssistant, config: dict, options: dict + ): # pylint: disable=too-many-arguments + """Initialize a Reolink camera.""" + + self._username = config[CONF_USERNAME] + self._password = config[CONF_PASSWORD] + + if CONF_CHANNEL not in config: + self._channel = DEFAULT_CHANNEL + else: + self._channel = config[CONF_CHANNEL] + + if CONF_TIMEOUT not in options: + self._timeout = DEFAULT_TIMEOUT + else: + self._timeout = options[CONF_TIMEOUT] + + if CONF_STREAM not in options: + self._stream = DEFAULT_STREAM + else: + self._stream = options[CONF_STREAM] + + if CONF_PROTOCOL not in options: + self._protocol = DEFAULT_PROTOCOL + else: + self._protocol = options[CONF_PROTOCOL] + + self._api = Api( + config[CONF_HOST], + config[CONF_PORT], + self._username, + self._password, + channel=self._channel - 1, + stream=self._stream, + protocol=self._protocol, + timeout=self._timeout, + ) + + self._hass = hass + self.sync_functions = list() + self.motion_detection_state = True + + if CONF_MOTION_OFF_DELAY not in options: + self.motion_off_delay = DEFAULT_MOTION_OFF_DELAY + else: + self.motion_off_delay = options[CONF_MOTION_OFF_DELAY] + + if CONF_PLAYBACK_MONTHS not in options: + self.playback_months = DEFAULT_PLAYBACK_MONTHS + else: + self.playback_months = options[CONF_PLAYBACK_MONTHS] + + if CONF_PLAYBACK_THUMBNAILS not in options: + self.playback_thumbnails = DEFAULT_PLAYBACK_THUMBNAILS + else: + self.playback_thumbnails = options[CONF_PLAYBACK_THUMBNAILS] + + if CONF_THUMBNAIL_OFFSET not in options: + self.playback_thumbnail_offset = DEFAULT_THUMBNAIL_OFFSET + else: + self.playback_thumbnail_offset = options[CONF_THUMBNAIL_OFFSET] + + @property + def name(self): + """Create the device name.""" + return self._api.name + + @property + def unique_id(self): + """Create the unique ID, base for all entities.""" + id = self._api.mac_address.replace(":", "") + return f"{id}-{self.channel}" + + @property + def event_id(self): + """Create the event ID string.""" + event_id = self._api.mac_address.replace(":", "") + return f"{EVENT_DATA_RECEIVED}-{event_id}" + + @property + def push_manager(self): + """Create the event ID string.""" + push_id = self._api.mac_address.replace(":", "") + return f"{PUSH_MANAGER}-{push_id}" + + @property + def timeout(self): + """Return the timeout setting.""" + return self._timeout + + @property + def channel(self): + """Return the channel setting.""" + return self._channel + + @property + def api(self): + """Return the API object.""" + return self._api + + async def connect_api(self): + """Connect to the Reolink API and fetch initial dataset.""" + if not await self._api.get_settings(): + return False + if not await self._api.get_states(): + return False + + await self._api.is_admin() + return True + + async def set_channel(self, channel): + """Set the API channel.""" + self._channel = channel + await self._api.set_channel(channel - 1) + + async def set_protocol(self, protocol): + """Set the protocol.""" + self._protocol = protocol + await self._api.set_protocol(protocol) + + async def set_stream(self, stream): + """Set the stream.""" + self._stream = stream + await self._api.set_stream(stream) + + async def set_timeout(self, timeout): + """Set the API timeout.""" + self._timeout = timeout + await self._api.set_timeout(timeout) + + async def update_states(self): + """Call the API of the camera device to update the states.""" + await self._api.get_states() + + async def update_settings(self): + """Call the API of the camera device to update the settings.""" + await self._api.get_settings() + + async def disconnect_api(self): + """Disconnect from the API, so the connection will be released.""" + await self._api.logout() + + async def stop(self): + """Disconnect the API and deregister the event listener.""" + await self.disconnect_api() + for func in self.sync_functions: + await self._hass.async_add_executor_job(func) + + +class ReolinkPush: + """The implementation of the Reolink IP base class.""" + + def __init__( + self, hass: HomeAssistant, host, port, username, password + ): # pylint: disable=too-many-arguments + """Initialize a Reolink camera.""" + self._host = host + self._port = port + self._username = username + self._password = password + self._hass = hass + + self._sman = None + self._webhook_url = None + self._webhook_id = None + self._event_id = None + + @property + def sman(self): + """Return the session manager object.""" + return self._sman + + async def subscribe(self, event_id): + """Subscribe to motion events and set the webhook as callback.""" + self._event_id = event_id + self._webhook_id = await self.register_webhook() + self._webhook_url = "{}{}".format( + get_url(self._hass, prefer_external=False), + self._hass.components.webhook.async_generate_path(self._webhook_id), + ) + + self._sman = Manager(self._host, self._port, self._username, self._password) + if await self._sman.subscribe(self._webhook_url): + _LOGGER.info( + "Host %s subscribed successfully to webhook %s", + self._host, + self._webhook_url, + ) + await self.set_available(True) + else: + await self.set_available(False) + return True + + async def register_webhook(self): + """ + Register a webhook for motion events if it does not exist yet (in case of NVR). + The webhook name (in info) contains the event id (contains mac address op the camera). + So when motion triggers the webhook, it triggers this event. The event is handled by + the binary sensor, in case of NVR the binary sensor also figures out what channel has + the motion. So the flow is: camera onvif event->webhook->HA event->binary sensor. + """ + _LOGGER.debug("Registering webhook for event ID %s", self._event_id) + + webhook_id = self._hass.components.webhook.async_generate_id() + self._hass.components.webhook.async_register( + DOMAIN, self._event_id, webhook_id, handle_webhook + ) + + return webhook_id + + async def renew(self): + """Renew the subscription of the motion events (lease time is set to 15 minutes).""" + if self._sman.renewtimer <= SESSION_RENEW_THRESHOLD: + if not await self._sman.renew(): + _LOGGER.error( + "Host %s error renewing the Reolink subscription", + self._host, + ) + await self.set_available(False) + await self._sman.subscribe(self._webhook_url) + else: + await self.set_available(True) + else: + await self.set_available(True) + + async def set_available(self, available: bool): + """Set the availability state to the base object.""" + self._hass.bus.async_fire(self._event_id, {"available": available}) + + async def unsubscribe(self): + """Unsubscribe from the motion events.""" + await self.set_available(False) + await self.unregister_webhook() + return await self._sman.unsubscribe() + + async def unregister_webhook(self): + """Unregister the webhook for motion events.""" + + _LOGGER.debug("Unregistering webhook %s", self._webhook_id) + self._hass.components.webhook.async_unregister(self._webhook_id) + + async def count_members(self): + """Count the number of camera's using this push manager.""" + members = 0 + for entry_id in self._hass.data[DOMAIN]: + _LOGGER.debug("Got data entry: %s", entry_id) + + if PUSH_MANAGER in entry_id: + continue # Count config entries only + + try: + base = self._hass.data[DOMAIN][entry_id][BASE] + if base.event_id == self._event_id: + members += 1 + except AttributeError: + pass + except KeyError: + pass + _LOGGER.debug("Found %d listeners for event %s", members, self._event_id) + return members + + +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook from Reolink for inbound messages and calls.""" + _LOGGER.debug("Reolink webhook triggered") + + if not request.body_exists: + _LOGGER.debug("Webhook triggered without payload") + + data = await request.text() + if not data: + _LOGGER.debug("Webhook triggered with unknown payload") + return + + _LOGGER.debug(data) + matches = re.findall(r'Name="IsMotion" Value="(.+?)"', data) + if matches: + is_motion = matches[0] == "true" + else: + _LOGGER.debug("Webhook triggered with unknown payload") + return + + event_id = await get_event_by_webhook(hass, webhook_id) + if not event_id: + _LOGGER.error("Webhook triggered without event to fire") + + hass.bus.async_fire(event_id, {"motion": is_motion}) + + +async def get_webhook_by_event(hass: HomeAssistant, event_id): + """Find the webhook_id by the event_id.""" + try: + handlers = hass.data["webhook"] + except KeyError: + return + + for wid, info in handlers.items(): + _LOGGER.debug("Webhook: %s", wid) + _LOGGER.debug(info) + if info["name"] == event_id: + return wid + + +async def get_event_by_webhook(hass: HomeAssistant, webhook_id): + """Find the event_id by the webhook_id.""" + try: + handlers = hass.data["webhook"] + except KeyError: + return + + for wid, info in handlers.items(): + if wid == webhook_id: + event_id = info["name"] + return event_id diff --git a/custom_components/reolink_dev/binary_sensor.py b/custom_components/reolink_dev/binary_sensor.py new file mode 100644 index 0000000..1cfa156 --- /dev/null +++ b/custom_components/reolink_dev/binary_sensor.py @@ -0,0 +1,106 @@ +"""This component provides support for Reolink motion events.""" +import asyncio +import datetime +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from .const import EVENT_DATA_RECEIVED +from .entity import ReolinkEntity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_DEVICE_CLASS = "motion" + + +@asyncio.coroutine +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the Reolink IP Camera switches.""" + sensor = MotionSensor(hass, config_entry) + async_add_devices([sensor], update_before_add=False) + + +class MotionSensor(ReolinkEntity, BinarySensorEntity): + """An implementation of a Reolink IP camera motion sensor.""" + + def __init__(self, hass, config): + """Initialize a the switch.""" + ReolinkEntity.__init__(self, hass, config) + BinarySensorEntity.__init__(self) + + self._available = False + self._event_state = False + self._last_motion = datetime.datetime.min + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_motion_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} motion" + + @property + def is_on(self): + """Return the state of the sensor.""" + if not self._base.motion_detection_state: + self._state = False + return self._state + + if self._event_state or self._base.motion_off_delay == 0: + self._state = self._event_state + return self._state + + if ( + datetime.datetime.now() - self._last_motion + ).total_seconds() < self._base.motion_off_delay: + self._state = True + else: + self._state = False + + return self._state + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_class(self): + """Return the class of this device.""" + return DEFAULT_DEVICE_CLASS + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + self.hass.bus.async_listen(self._base.event_id, self.handle_event) + + async def handle_event(self, event): + """Handle incoming event for motion detection and availability.""" + try: + self._available = event.data["available"] + return + except KeyError: + pass + + if not self._available: + return + + try: + self._event_state = event.data["motion"] + except KeyError: + return + + if self._base.api.channels > 1: + # Pull the motion state for the NVR channel, it has only 1 event + self._event_state = await self._base.api.get_motion_state() + + if self._event_state: + self._last_motion = datetime.datetime.now() + else: + if self._base.motion_off_delay > 0: + await asyncio.sleep(self._base.motion_off_delay) + + self.async_schedule_update_ha_state() diff --git a/custom_components/reolink_dev/camera.py b/custom_components/reolink_dev/camera.py index 955239d..6262530 100644 --- a/custom_components/reolink_dev/camera.py +++ b/custom_components/reolink_dev/camera.py @@ -1,174 +1,147 @@ -"""This component provides basic support for Reolink IP cameras.""" -import logging +"""This component provides support for Reolink IP cameras.""" import asyncio -import voluptuous as vol -import datetime - -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM, ENTITY_IMAGE_URL -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv +from datetime import datetime +import logging from haffmpeg.camera import CameraMjpeg +import voluptuous as vol + +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from custom_components.reolink_dev.ReolinkPyPi.camera import ReolinkApi +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_web, + async_get_clientsession, +) + +from .const import ( + SERVICE_PTZ_CONTROL, + SERVICE_SET_BACKLIGHT, + SERVICE_SET_DAYNIGHT, + SERVICE_SET_SENSITIVITY, +) +from .entity import ReolinkEntity _LOGGER = logging.getLogger(__name__) -STATE_MOTION = "motion" -STATE_NO_MOTION = "no_motion" -STATE_IDLE = "idle" - -DEFAULT_NAME = "Reolink Camera" -DEFAULT_STREAM = "main" -DEFAULT_PROTOCOL = "rtmp" -DEFAULT_CHANNEL = 0 -CONF_STREAM = "stream" -CONF_PROTOCOL = "protocol" -CONF_CHANNEL = "channel" -DOMAIN = "camera" -SERVICE_ENABLE_FTP = 'enable_ftp' -SERVICE_DISABLE_FTP = 'disable_ftp' -SERVICE_ENABLE_EMAIL = 'enable_email' -SERVICE_DISABLE_EMAIL = 'disable_email' -SERVICE_ENABLE_IR_LIGHTS = 'enable_ir_lights' -SERVICE_DISABLE_IR_LIGHTS = 'disable_ir_lights' - -DEFAULT_BRAND = 'Reolink' -DOMAIN_DATA = 'reolink_devices' - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STREAM, default=DEFAULT_STREAM): vol.In(["main", "sub"]), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(["rtmp", "rtsp"]), - vol.Optional(CONF_CHANNEL, default=DEFAULT_CHANNEL): cv.positive_int, - } -) @asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up a Reolink IP Camera.""" - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - stream = config.get(CONF_STREAM) - protocol = config.get(CONF_PROTOCOL) - channel = config.get(CONF_CHANNEL) - name = config.get(CONF_NAME) + platform = entity_platform.current_platform.get() + camera = ReolinkCamera(hass, config_entry) - session = ReolinkApi(host, channel) - session.login(username, password) + platform.async_register_entity_service( + SERVICE_SET_SENSITIVITY, + { + vol.Required("sensitivity"): cv.positive_int, + vol.Optional("preset"): cv.positive_int, + }, + SERVICE_SET_SENSITIVITY, + ) - async_add_devices([ReolinkCamera(hass, session, host, username, password, stream, protocol, channel, name)], update_before_add=True) + platform.async_register_entity_service( + SERVICE_SET_DAYNIGHT, + { + vol.Required("mode"): cv.string, + }, + SERVICE_SET_DAYNIGHT, + ) -# Event enable FTP - def handler_enable_ftp(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + platform.async_register_entity_service( + SERVICE_SET_BACKLIGHT, + { + vol.Required("mode"): cv.string, + }, + SERVICE_SET_BACKLIGHT, + ) - if entity: - entity.enable_ftp_upload() - hass.services.async_register(DOMAIN, SERVICE_ENABLE_FTP, handler_enable_ftp) + platform.async_register_entity_service( + SERVICE_PTZ_CONTROL, + { + vol.Required("command"): cv.string, + vol.Optional("preset"): cv.positive_int, + vol.Optional("speed"): cv.positive_int, + }, + SERVICE_PTZ_CONTROL, + ) -# Event disable FTP - def handler_disable_ftp(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) - - if entity: - entity.disable_ftp_upload() - - hass.services.async_register(DOMAIN, SERVICE_DISABLE_FTP, handler_disable_ftp) - -# Event enable email - def handler_enable_email(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) - - if entity: - entity.enable_email() - hass.services.async_register(DOMAIN, SERVICE_ENABLE_EMAIL, handler_enable_email) - -# Event disable email - def handler_disable_email(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) - - if entity: - entity.disable_email() - - hass.services.async_register(DOMAIN, SERVICE_DISABLE_EMAIL, handler_disable_email) - -# Event enable ir lights - def handler_enable_ir_lights(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) - - if entity: - entity.enable_ir_lights() - hass.services.async_register(DOMAIN, SERVICE_ENABLE_IR_LIGHTS, handler_enable_ir_lights) - -# Event disable ir lights - def handler_disable_ir_lights(call): - component = hass.data.get(DOMAIN) - entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) - - if entity: - entity.disable_ir_lights() - - hass.services.async_register(DOMAIN, SERVICE_DISABLE_IR_LIGHTS, handler_disable_ir_lights) + async_add_devices([camera]) -class ReolinkCamera(Camera): +class ReolinkCamera(ReolinkEntity, Camera): """An implementation of a Reolink IP camera.""" - def __init__(self, hass, session, host, username, password, stream, protocol, channel, name): + def __init__(self, hass, config): """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + Camera.__init__(self) - super().__init__() - self._host = host - self._username = username - self._password = password - self._stream = stream - self._protocol = protocol - self._channel = channel - self._name = name - self._reolinkSession = session self._hass = hass - self._manager = self._hass.data[DATA_FFMPEG] - - self._last_update = 0 + self._ffmpeg = self._hass.data[DATA_FFMPEG] self._last_image = None - self._last_motion = 0 - self._ftp_state = None - self._email_state = None - self._ir_state = None - self._ptzpresets = dict() - self._state = STATE_IDLE + self._ptz_commands = { + "AUTO": "Auto", + "DOWN": "Down", + "FOCUSDEC": "FocusDec", + "FOCUSINC": "FocusInc", + "LEFT": "Left", + "LEFTDOWN": "LeftDown", + "LEFTUP": "LeftUp", + "RIGHT": "Right", + "RIGHTDOWN": "RightDown", + "RIGHTUP": "RightUp", + "STOP": "Stop", + "TOPOS": "ToPos", + "UP": "Up", + "ZOOMDEC": "ZoomDec", + "ZOOMINC": "ZoomInc", + } + self._daynight_modes = { + "AUTO": "Auto", + "COLOR": "Color", + "BLACKANDWHITE": "Black&White", + } - self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.disconnect) + self._backlight_modes = { + "BACKLIGHTCONTROL": "BackLightControl", + "DYNAMICRANGECONTROL": "DynamicRangeControl", + "OFF": "Off", + } @property - def state_attributes(self): + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_camera_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return self._base.name + + @property + def ptz_support(self): + """Return whether the camera has PTZ support.""" + return self._base.api.ptz_support + + @property + def device_state_attributes(self): """Return the camera state attributes.""" - attrs = {"access_token": self.access_tokens[-1]} + attrs = {} + if self._base.api.ptz_support: + attrs["ptz_presets"] = self._base.api.ptz_presets - if self._last_motion: - attrs["last_motion"] = self._last_motion - - if self._last_update: - attrs["last_update"] = self._last_update + for key, value in self._backlight_modes.items(): + if value == self._base.api.backlight_state: + attrs["backlight_state"] = key - attrs["ftp_enabled"] = self._ftp_state - attrs["email_enabled"] = self._email_state - attrs["ir_lights_enabled"] = self._ir_state - attrs["ptzpresets"] = self._ptzpresets + for key, value in self._daynight_modes.items(): + if value == self._base.api.daynight_state: + attrs["daynight_state"] = key + + if self._base.api.sensitivity_presets: + attrs["sensitivity"] = self.get_sensitivity_presets() return attrs @@ -177,136 +150,72 @@ class ReolinkCamera(Camera): """Return supported features.""" return SUPPORT_STREAM - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def should_poll(self): - """Polling needed for the device status.""" - return True - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def ftp_state(self): - """Camera Motion recording Status.""" - return self._ftp_state - - @property - def email_state(self): - """Camera email Status.""" - return self._email_state - - @property - def ptzpresets(self): - """Camera PTZ presets list.""" - return self._ptzpresets - async def stream_source(self): """Return the source of the stream.""" - if self._protocol == "rtsp": - rtspChannel = f"{self._channel+1:02d}" - stream_source = f"rtsp://{self._username}:{self._password}@{self._host}:{self._reolinkSession.rtspport}/h264Preview_{rtspChannel}_{self._stream}" - else: - stream_source = f"rtmp://{self._host}:{self._reolinkSession.rtmpport}/bcs/channel{self._channel}_{self._stream}.bcs?channel={self._channel}&stream=0&user={self._username}&password={self._password}" - - return stream_source + return await self._base.api.get_stream_source() async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" stream_source = await self.stream_source() - stream = CameraMjpeg(self._manager.binary, loop=self._hass.loop) - await stream.open_camera(stream_source) + websession = async_get_clientsession(self._hass) + stream_coro = websession.get(stream_source, timeout=10) - try: - stream_reader = await stream.get_reader() - return await async_aiohttp_proxy_stream( - self._hass, - request, - stream_reader, - self._manager.ffmpeg_stream_content_type, - ) - finally: - await stream.close() - - def camera_image(self): - """Return bytes of camera image.""" - return self._reolinkSession.still_image + return await async_aiohttp_proxy_web(self._hass, request, stream_coro) async def async_camera_image(self): """Return a still image response from the camera.""" - return self._reolinkSession.snapshot + return await self._base.api.get_snapshot() - def enable_ftp_upload(self): - """Enable motion ftp recording in camera.""" - if self._reolinkSession.set_ftp(True): - self._ftp_state = True - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + async def ptz_control(self, command, **kwargs): + """Pass PTZ command to the camera.""" + if not self.ptz_support: + _LOGGER.error("PTZ is not supported on this device") + return - def disable_ftp_upload(self): - """Disable motion ftp recording.""" - if self._reolinkSession.set_ftp(False): - self._ftp_state = False - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + await self._base.api.set_ptz_command( + command=self._ptz_commands[command], **kwargs + ) - def enable_email(self): - """Enable email motion detection in camera.""" - if self._reolinkSession.set_email(True): - self._email_state = True - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + def get_sensitivity_presets(self): + """Get formatted sensitivity presets.""" + presets = list() + preset = dict() - def disable_email(self): - """Disable email motion detection.""" - if self._reolinkSession.set_email(False): - self._email_state = False - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + for api_preset in self._base.api.sensitivity_presets: + preset["id"] = api_preset["id"] + preset["sensitivity"] = api_preset["sensitivity"] - def enable_ir_lights(self): - """Enable IR lights.""" - if self._reolinkSession.set_ir_lights(True): - self._ir_state = True - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + time_string = f'{api_preset["beginHour"]}:{api_preset["beginMin"]}' + begin = datetime.strptime(time_string, "%H:%M") + preset["begin"] = begin.strftime("%H:%M") - def disable_ir_lights(self): - """Disable IR lights.""" - if self._reolinkSession.set_ir_lights(False): - self._ir_state = False - self._hass.states.set(self.entity_id, self.state, self.state_attributes) + time_string = f'{api_preset["endHour"]}:{api_preset["endMin"]}' + end = datetime.strptime(time_string, "%H:%M") + preset["end"] = end.strftime("%H:%M") - async def update_motion_state(self): - if self._reolinkSession.motion_state == True: - self._state = STATE_MOTION - self._last_motion = self._reolinkSession.last_motion - else: - self._state = STATE_NO_MOTION - - async def update_status(self): - self._reolinkSession.status() + presets.append(preset.copy()) - self._last_update = datetime.datetime.now() - self._ftp_state = self._reolinkSession.ftp_state - self._email_state = self._reolinkSession.email_state - self._ir_state = self._reolinkSession.ir_state - self._ptzpresets = self._reolinkSession.ptzpresets + return presets - def update(self): - """Update the data from the camera.""" - try: - self._hass.loop.create_task(self.update_motion_state()) + async def set_sensitivity(self, sensitivity, **kwargs): + """Set the sensitivity to the camera.""" + if "preset" in kwargs: + kwargs["preset"] += 1 # The camera preset ID's on the GUI are always +1 + await self._base.api.set_sensitivity(value=sensitivity, **kwargs) - if (self._last_update == 0 or - (datetime.datetime.now() - self._last_update).total_seconds() >= 30): - self._hass.loop.create_task(self.update_status()) + async def set_daynight(self, mode): + """Set the day and night mode to the camera.""" + await self._base.api.set_daynight(value=self._daynight_modes[mode]) - except Exception as ex: - _LOGGER.error("Got exception while fetching the state: %s", ex) + async def set_backlight(self, mode): + """Set the backlight mode to the camera.""" + await self._base.api.set_backlight(value=self._backlight_modes[mode]) - def disconnect(self, event): - _LOGGER.info("Disconnecting from Reolink camera") - self._reolinkSession.logout() + async def async_enable_motion_detection(self): + """Predefined camera service implementation.""" + self._base.motion_detection_state = True + + async def async_disable_motion_detection(self): + """Predefined camera service implementation.""" + self._base.motion_detection_state = False diff --git a/custom_components/reolink_dev/config_flow.py b/custom_components/reolink_dev/config_flow.py new file mode 100644 index 0000000..d88bc2c --- /dev/null +++ b/custom_components/reolink_dev/config_flow.py @@ -0,0 +1,217 @@ +"""Config flow for the Reolink camera component.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries, core, data_entry_flow, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_TIMEOUT, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .base import ReolinkBase +from .const import ( + BASE, + CONF_CHANNEL, + CONF_MOTION_OFF_DELAY, + CONF_PLAYBACK_MONTHS, + CONF_PLAYBACK_THUMBNAILS, + CONF_PROTOCOL, + CONF_STREAM, + CONF_THUMBNAIL_OFFSET, + DEFAULT_MOTION_OFF_DELAY, + DEFAULT_PLAYBACK_MONTHS, + DEFAULT_PLAYBACK_THUMBNAILS, + DEFAULT_PROTOCOL, + DEFAULT_STREAM, + DEFAULT_THUMBNAIL_OFFSET, + DEFAULT_TIMEOUT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Reolink camera's.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + channels = 1 + mac_address = None + base = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return ReolinkOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + self.data = user_input + + try: + self.info = await self.async_validate_input(self.hass, user_input) + + if self.channels > 1: + return await self.async_step_nvr() + + self.data[CONF_CHANNEL] = 1 + await self.async_set_unique_id( + f"{self.mac_address}{user_input[CONF_CHANNEL]}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=self.info["title"], data=self.data) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidHost: + errors["host"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=80): cv.positive_int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_nvr(self, user_input=None): + """Configure a NVR with multiple channels.""" + errors = {} + if user_input is not None: + self.data.update(user_input) + + await self.async_set_unique_id( + f"{self.mac_address}{user_input[CONF_CHANNEL]}" + ) + self._abort_if_unique_id_configured() + + await self.base.set_channel(user_input[CONF_CHANNEL]) + await self.base.update_settings() + + return self.async_create_entry(title=self.base.name, data=self.data) + + return self.async_show_form( + step_id="nvr", + data_schema=vol.Schema( + { + vol.Required(CONF_CHANNEL): vol.All( + vol.Coerce(int), vol.Range(min=1, max=self.channels) + ), + } + ), + errors=errors, + ) + + async def async_validate_input(self, hass: core.HomeAssistant, user_input: dict): + """Validate the user input allows us to connect.""" + self.base = ReolinkBase(hass, user_input, []) + + if not await self.base.connect_api(): + raise CannotConnect + + title = self.base.api.name + self.channels = self.base.api.channels + self.mac_address = self.base.api.mac_address + + return {"title": title} + + async def async_finish_flow(self, flow, result): + """Finish flow.""" + # if result['type'] == data_entry_flow.RESULT_TYPE_ABORT: + self.base.disconnect_api() + + +class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Reolink options.""" + + def __init__(self, config_entry): + """Initialize Reolink options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): # pylint: disable=unused-argument + """Manage the Reolink options.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_PROTOCOL, + default=self.config_entry.options.get( + CONF_PROTOCOL, DEFAULT_PROTOCOL + ), + ): vol.In(["rtmp", "rtsp"]), + vol.Required( + CONF_STREAM, + default=self.config_entry.options.get( + CONF_STREAM, DEFAULT_STREAM + ), + ): vol.In(["main", "sub"]), + vol.Required( + CONF_MOTION_OFF_DELAY, + default=self.config_entry.options.get( + CONF_MOTION_OFF_DELAY, DEFAULT_MOTION_OFF_DELAY + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Required( + CONF_PLAYBACK_MONTHS, + default=self.config_entry.options.get( + CONF_PLAYBACK_MONTHS, DEFAULT_PLAYBACK_MONTHS + ), + ): cv.positive_int, + vol.Optional( + CONF_PLAYBACK_THUMBNAILS, + default=self.config_entry.options.get( + CONF_PLAYBACK_THUMBNAILS, DEFAULT_PLAYBACK_THUMBNAILS + ), + ): cv.boolean, + vol.Optional( + CONF_THUMBNAIL_OFFSET, + default=self.config_entry.options.get( + CONF_THUMBNAIL_OFFSET, DEFAULT_THUMBNAIL_OFFSET + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=60)), + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get( + CONF_TIMEOUT, DEFAULT_TIMEOUT + ), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + } + ), + ) + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate device is already configured.""" + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate there is an invalid hostname.""" diff --git a/custom_components/reolink_dev/const.py b/custom_components/reolink_dev/const.py new file mode 100644 index 0000000..138e8c2 --- /dev/null +++ b/custom_components/reolink_dev/const.py @@ -0,0 +1,31 @@ +"""Constants for the Reolink Camera integration.""" + +DOMAIN = "reolink_dev" +DOMAIN_DATA = "reolink_dev_devices" +EVENT_DATA_RECEIVED = "reolink_dev-event" +COORDINATOR = "coordinator" +BASE = "base" +PUSH_MANAGER = "push_manager" +SESSION_RENEW_THRESHOLD = 300 + +CONF_STREAM = "stream" +CONF_PROTOCOL = "protocol" +CONF_CHANNEL = "channel" +CONF_MOTION_OFF_DELAY = "motion_off_delay" +CONF_PLAYBACK_MONTHS = "playback_months" +CONF_PLAYBACK_THUMBNAILS = "playback_thumbnails" +CONF_THUMBNAIL_OFFSET = "playback_thumbnail_offset" + +DEFAULT_CHANNEL = 1 +DEFAULT_MOTION_OFF_DELAY = 60 +DEFAULT_PROTOCOL = "rtmp" +DEFAULT_STREAM = "main" +DEFAULT_TIMEOUT = 30 +DEFAULT_PLAYBACK_MONTHS = 2 +DEFAULT_PLAYBACK_THUMBNAILS = False +DEFAULT_THUMBNAIL_OFFSET = 6 + +SERVICE_PTZ_CONTROL = "ptz_control" +SERVICE_SET_BACKLIGHT = "set_backlight" +SERVICE_SET_DAYNIGHT = "set_daynight" +SERVICE_SET_SENSITIVITY = "set_sensitivity" diff --git a/custom_components/reolink_dev/entity.py b/custom_components/reolink_dev/entity.py new file mode 100644 index 0000000..98e5927 --- /dev/null +++ b/custom_components/reolink_dev/entity.py @@ -0,0 +1,41 @@ +"""Reolink parent entity class.""" + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import BASE, COORDINATOR, DOMAIN + + +class ReolinkEntity(CoordinatorEntity): + """Parent class for Reolink Entities.""" + + def __init__(self, hass, config): + """Initialize common aspects of a Reolink entity.""" + coordinator = hass.data[DOMAIN][config.entry_id][COORDINATOR] + super().__init__(coordinator) + + self._base = hass.data[DOMAIN][config.entry_id][BASE] + self._hass = hass + self._state = False + + @property + def device_info(self): + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._base.unique_id)}, + "connections": {(CONNECTION_NETWORK_MAC, self._base.api.mac_address)}, + "name": self._base.name, + "sw_version": self._base.api.sw_version, + "model": self._base.api.model, + "manufacturer": self._base.api.manufacturer, + "channel": self._base.channel + } + + @property + def available(self): + """Return True if entity is available.""" + return self._base.api.session_active + + async def request_refresh(self): + """Call the coordinator to update the API.""" + await self.coordinator.async_request_refresh() diff --git a/custom_components/reolink_dev/manifest.json b/custom_components/reolink_dev/manifest.json index 850d570..f345072 100644 --- a/custom_components/reolink_dev/manifest.json +++ b/custom_components/reolink_dev/manifest.json @@ -1,8 +1,25 @@ { - "domain": "reolink_dev", - "name": "Reolink IP camera", - "documentation": "https://www.example.com", - "dependencies": ["ffmpeg"], - "codeowners": ["@fwestenberg"], - "requirements": ["aiosmtpd==1.2"] - } + "domain": "reolink_dev", + "name": "Reolink IP camera", + "documentation": "https://github.com/fwestenberg/reolink_dev", + "issue_tracker": "https://github.com/fwestenberg/reolink_dev/issues", + "version": "0.15", + "requirements": [ + "reolink==0.0.17" + ], + "dependencies": [ + "ffmpeg", + "webhook" + ], + "after_dependencies": [ + "media_source", + "http" + ], + "codeowners": [ + "@fwestenberg" + ], + "config_flow": true, + "ssdp": [], + "zeroconf": [], + "homekit": {} +} diff --git a/custom_components/reolink_dev/media_source.py b/custom_components/reolink_dev/media_source.py new file mode 100644 index 0000000..c3a0ff1 --- /dev/null +++ b/custom_components/reolink_dev/media_source.py @@ -0,0 +1,412 @@ +"""Reolink Camera Media Source Implementation.""" +from urllib import parse +import secrets +import datetime as dt +import logging +from typing import Optional, Tuple +from aiohttp import web +from haffmpeg.tools import IMAGE_JPEG + +from dateutil import relativedelta + +from homeassistant.core import HomeAssistant, callback + +import homeassistant.util.dt as dt_utils + +from homeassistant.components.http import HomeAssistantView + +# from homeassistant.components.http.auth import async_sign_path +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_VIDEO, + MEDIA_TYPE_VIDEO, +) +from homeassistant.components.media_source.const import MEDIA_MIME_TYPES +from homeassistant.components.media_source.error import MediaSourceError, Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) + +from homeassistant.components.stream import create_stream +from homeassistant.components.ffmpeg import async_get_image + +from custom_components.reolink_dev.base import ReolinkBase + +from . import typings + +from .const import BASE, DEFAULT_THUMBNAIL_OFFSET, DOMAIN + +_LOGGER = logging.getLogger(__name__) +# MIME_TYPE = "rtmp/mp4" +# MIME_TYPE = "video/mp4" +MIME_TYPE = "application/x-mpegURL" + +NAME = "Reolink IP Camera" + + +class IncompatibleMediaSource(MediaSourceError): + """Incompatible media source attributes.""" + + +async def async_get_media_source(hass: HomeAssistant): + """Set up Reolink media source.""" + _LOGGER.debug("Creating REOLink Media Source") + source = ReolinkSource(hass) + hass.http.register_view(ReolinkSourceThumbnailView(hass, source)) + return source + + +class ReolinkSource(MediaSource): + """Provide Reolink camera recordings as media sources.""" + + name: str = NAME + + def __init__(self, hass: HomeAssistant): + """Initialize Reolink source.""" + super().__init__(DOMAIN) + self.hass = hass + self.cache = {} + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve a media item to a playable item.""" + _, camera_id, event_id = async_parse_identifier(item) + cache: typings.MediaSourceCacheEntry = self.cache[camera_id] + event = cache["playback_events"][event_id] + base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE] + url = await base.api.get_vod_source(event["file"]) + _LOGGER.debug("Load VOD %s", url) + stream = create_stream(self.hass, url) + stream.add_provider("hls", timeout=600) + url: str = stream.endpoint_url("hls") + # the media browser seems to have a problem with the master_playlist + # ( it does not load the referenced playlist ) so we will just + # force the reference playlist instead, this seems to work + # though technically wrong + url = url.replace("master_", "") + _LOGGER.debug("Proxy %s", url) + return PlayMedia(url, MIME_TYPE) + + async def async_browse_media( + self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + ) -> BrowseMediaSource: + """Browse media.""" + + try: + source, camera_id, event_id = async_parse_identifier(item) + except Unresolvable as err: + raise BrowseError(str(err)) from err + + _LOGGER.debug("Browsing %s, %s, %s", source, camera_id, event_id) + + if camera_id and camera_id not in self.cache: + raise BrowseError("Camera does not exist.") + + if ( + event_id + and not "/" in event_id + and event_id not in self.cache[camera_id]["playback_events"] + ): + raise BrowseError("Event does not exist.") + + return await self._async_browse_media(source, camera_id, event_id, False) + + async def _async_browse_media( + self, source: str, camera_id: str, event_id: str = None, no_descend: bool = True + ) -> BrowseMediaSource: + """ actual browse after input validation """ + event: typings.VodEvent = None + cache: typings.MediaSourceCacheEntry = None + start_date = None + + if camera_id and camera_id in self.cache: + cache = self.cache[camera_id] + + if cache and event_id: + if "playback_events" in cache and event_id in cache["playback_events"]: + event = cache["playback_events"][event_id] + end_date = event["end"] + start_date = event["start"] + time = start_date.time() + duration = end_date - start_date + + title = f"{time} {duration}" + else: + year, *rest = event_id.split("/", 3) + month = rest[0] if len(rest) > 0 else None + day = rest[1] if len(rest) > 1 else None + + start_date = dt.datetime.combine( + dt.date( + int(year), int(month) if month else 1, int(day) if day else 1 + ), + dt.time.min, + dt_utils.now().tzinfo, + ) + + title = f"{start_date.date()}" + + path = f"{source}/{camera_id}/{event_id}" + else: + if cache is None: + camera_id = "" + title = NAME + else: + title = cache["name"] + + path = f"{source}/{camera_id}" + + media_class = MEDIA_CLASS_DIRECTORY if event is None else MEDIA_CLASS_VIDEO + + media = BrowseMediaSource( + domain=DOMAIN, + identifier=path, + media_class=media_class, + media_content_type=MEDIA_TYPE_VIDEO, + title=title, + can_play=bool(not event is None and event.get("file")), + can_expand=event is None, + ) + + if not event is None and cache.get("playback_thumbnails", False): + url = "/api/" + DOMAIN + f"/media_proxy/{camera_id}/{event_id}" + + # TODO : I cannot find a way to get the current user context at this point + # so I will have to leave the view as unauthenticated, as a temporary + # security measure, I will add a unique token to the event to limit + # "exposure" + # url = async_sign_path(self.hass, None, url, dt.timedelta(minutes=30)) + if "token" not in event: + event["token"] = secrets.token_hex() + media.thumbnail = f"{url}?token={parse.quote_plus(event['token'])}" + + if not media.can_play and not media.can_expand: + _LOGGER.debug( + "Camera %s with event %s without media url found", camera_id, event_id + ) + raise IncompatibleMediaSource + + if not media.can_expand or no_descend: + return media + + media.children = [] + + base: ReolinkBase = None + + if cache is None: + for entry_id in self.hass.data[DOMAIN]: + entry = self.hass.data[DOMAIN][entry_id] + if not isinstance(entry, dict) or not BASE in entry: + continue + base = entry[BASE] + camera_id = base.unique_id + cache = self.cache.get(camera_id, None) + if cache is None: + cache = self.cache[camera_id] = { + "entry_id": entry_id, + "unique_id": base.unique_id, + "playback_events": {}, + } + cache["name"] = base.name + + child = await self._async_browse_media(source, camera_id) + media.children.append(child) + return media + + base = self.hass.data[DOMAIN][cache["entry_id"]][BASE] + + # TODO: the cache is one way so over time it can grow and have invalid + # records, the code should be expanded to invalidate/expire + # entries + + if base is None: + raise BrowseError("Camera does not exist.") + + if not start_date: + if ( + "playback_day_entries" not in cache + or cache.get("playback_months", -1) != base.playback_months + ): + end_date = dt_utils.now() + start_date = dt.datetime.combine(end_date.date(), dt.time.min) + cache["playback_months"] = base.playback_months + if cache["playback_months"] > 1: + start_date -= relativedelta.relativedelta( + months=int(cache["playback_months"]) + ) + + entries = cache["playback_day_entries"] = [] + + search, _ = await base.api.send_search(start_date, end_date, True) + + if not search is None: + for status in search: + year = status["year"] + month = status["mon"] + for day, flag in enumerate(status["table"], start=1): + if flag == "1": + entries.append(dt.date(year, month, day)) + + entries.sort() + else: + entries = cache["playback_day_entries"] + + for date in cache["playback_day_entries"]: + child = await self._async_browse_media( + source, camera_id, f"{date.year}/{date.month}/{date.day}" + ) + media.children.append(child) + + return media + + cache["playback_thumbnails"] = base.playback_thumbnails + + end_date = dt.datetime.combine( + start_date.date(), dt.time.max, start_date.tzinfo + ) + + _, files = await base.api.send_search(start_date, end_date) + + if not files is None: + events = cache.setdefault("playback_events", {}) + + for file in files: + dto = file["EndTime"] + end_date = dt.datetime( + dto["year"], + dto["mon"], + dto["day"], + dto["hour"], + dto["min"], + dto["sec"], + 0, + end_date.tzinfo, + ) + dto = file["StartTime"] + start_date = dt.datetime( + dto["year"], + dto["mon"], + dto["day"], + dto["hour"], + dto["min"], + dto["sec"], + 0, + end_date.tzinfo, + ) + event_id = str(start_date.timestamp()) + event = events.setdefault(event_id, {}) + event["start"] = start_date + event["end"] = end_date + event["file"] = file["name"] + + child = await self._async_browse_media(source, camera_id, event_id) + media.children.append(child) + + return media + + +class ReolinkSourceThumbnailView(HomeAssistantView): + """ Thumbnial view handler """ + + url = "/api/" + DOMAIN + "/media_proxy/{camera_id}/{event_id}" + name = "api:" + DOMAIN + ":image" + requires_auth = False + cors_allowed = True + + def __init__(self, hass: HomeAssistant, source: ReolinkSource): + """Initialize media view """ + + self.hass = hass + self.source = source + + async def get( + self, request: web.Request, camera_id: str, event_id: str + ) -> web.Response: + """ start a GET request. """ + + if not camera_id or not event_id: + raise web.HTTPNotFound() + + cache: typings.MediaSourceCacheEntry = self.source.cache.get(camera_id, None) + if cache is None or "playback_events" not in cache: + _LOGGER.debug("camera %s not found", camera_id) + raise web.HTTPNotFound() + + event = cache["playback_events"].get(event_id, None) + if event is None: + _LOGGER.debug("camera %s, event %s not found", camera_id, event_id) + raise web.HTTPNotFound() + + token = request.query.get("token") + if (token and event.get("token") != token) or ( + not token and not self.requires_auth + ): + _LOGGER.debug( + "invalid or missing token %s for camera %s, event %s", + token, + camera_id, + event_id, + ) + raise web.HTTPNotFound() + + _LOGGER.debug("thumbnail %s, %s", camera_id, event_id) + + base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE] + + image = event.get("thumbnail", None) + if ( + image is None + or cache.get("playback_thumbnail_offset", DEFAULT_THUMBNAIL_OFFSET) + != base.playback_thumbnail_offset + ): + cache["playback_thumbnails"] = base.playback_thumbnails + cache["playback_thumbnail_offset"] = base.playback_thumbnail_offset + + if not cache["playback_thumbnails"]: + _LOGGER.debug("Thumbnails not allowed on camera %s", camera_id) + raise web.HTTPInternalServerError() + + _LOGGER.debug("generating thumbnail for %s, %s", camera_id, event_id) + + extra_cmd: str = None + if cache["playback_thumbnail_offset"] > 0: + extra_cmd = f"-ss {cache['playback_thumbnail_offset']}" + + image = event["thumbail"] = await async_get_image( + self.hass, + await base.api.get_vod_source(event["file"]), + extra_cmd=extra_cmd, + ) + _LOGGER.debug("generated thumbnail for %s, %s", camera_id, event_id) + + if image: + return web.Response(body=image, content_type=IMAGE_JPEG) + + _LOGGER.debug( + "No thumbnail generated for camera %s, event %s", camera_id, event_id + ) + raise web.HTTPInternalServerError() + + +@callback +def async_parse_identifier( + item: MediaSourceItem, +) -> Tuple[str, str, Optional[str]]: + """Parse identifier.""" + if not item.identifier: + return "events", "", None + + source, path = item.identifier.lstrip("/").split("/", 1) + + if source != "events": + raise Unresolvable("Unknown source directory.") + + if "/" in path: + camera_id, event_id = path.split("/", 1) + + return source, camera_id, event_id + + return source, path, None diff --git a/custom_components/reolink_dev/services.yaml b/custom_components/reolink_dev/services.yaml index d7762fc..b863650 100644 --- a/custom_components/reolink_dev/services.yaml +++ b/custom_components/reolink_dev/services.yaml @@ -1,41 +1,84 @@ -enable_ftp: - description: Enable FTP upload on motion recording. +ptz_control: + name: Pan/Zoom/Tilt Control + description: Execute a PTZ command. + target: + entity: + integration: reolink_dev + domain: camera fields: entity_id: - description: Name of the Reolink camera entity to set. + description: Name(s) of the Reolink camera entity to execute the command on. example: 'camera.frontdoor' - -disable_ftp: - description: Disable FTP upload on motion recording. + command: + description: >- + Command to execute. Possible values are: + AUTO DOWN FOCUSDEC FOCUSINC LEFT LEFTDOWN LEFTUP + RIGHT RIGHTDOWN RIGHTUP STOP TOPOS UP ZOOMDEC ZOOMINC + example: LEFTUP + preset: + description: (Optional) In case of the command TOPOS. The available presets are listed as attribute on the camera. + example: HOME + speed: + description: (Optional) Speed at which the movement takes place. + example: 25 + +set_sensitivity: + name: Set Motion Sensitivity + description: Set the motion detection sensitivity. + target: + entity: + integration: reolink_dev + domain: camera fields: entity_id: - description: Name of the Reolink camera entity to set. + description: Name(s) of the Reolink camera entity to execute the command on. example: 'camera.frontdoor' - -enable_email: - description: Enable email functionality on motion detection. + sensitivity: + description: New sensitivity, value between 1 (low sensitivity) and 50 (high sensitivity) + example: 25 + preset: + description: >- + (Optional) Set the sensitivity of a specific preset (time schedule). When no value is supplied, + all presets will be changed. + +set_daynight: + name: Set Day/Night Mode + description: Set day and night parameter. + target: + entity: + integration: reolink_dev + domain: camera fields: entity_id: - description: Name of the Reolink camera entity to set. + description: Name(s) of the Reolink camera entity to execute the command on. example: 'camera.frontdoor' - -disable_email: - description: Disable email functionality on motion detection. + mode: + description: >- + The day and night mode parameter supports the following values: + AUTO: Auto switch between black & white mode + COLOR: Always record videos in color mode + BLACKANDWHITE: Always record videos in black & white mode + example: AUTO + +set_backlight: + name: Set backlight + description: >- + Optimizing brightness and contrast levels to compensate for differences + between dark and bright objects using either BLC or WDR mode. + This may improve image clarity in high contrast situations, + but it should be tested at different times of the day and night to ensure there is no negative effect. + target: + entity: + integration: reolink_dev + domain: camera fields: entity_id: - description: Name of the Reolink camera entity to set. + description: Name(s) of the Reolink camera entity to execute the command on. example: 'camera.frontdoor' - -enable_ir_lights: - description: Enable the infrared lights (nightvision) of the Reolink camera. - fields: - entity_id: - description: Name of the Reolink camera entity to set. - example: 'camera.frontdoor' - -disable_ir_lights: - description: Disable the infrared lights (nightvision) of the Reolink camera. - fields: - entity_id: - description: Name of the Reolink camera entity to set. - example: 'camera.frontdoor' \ No newline at end of file + mode: + description: >- + The backlight parameter supports the following values: + BACKLIGHTCONTROL: use Backlight Control + DYNAMICRANGECONTROL: use Dynamic Range Control + OFF: no optimization + example: DYNAMICRANGECONTROL diff --git a/custom_components/reolink_dev/strings.json b/custom_components/reolink_dev/strings.json new file mode 100644 index 0000000..3cebc9a --- /dev/null +++ b/custom_components/reolink_dev/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "nvr": { + "data": { + "channel": "Channel" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocol", + "stream": "Stream", + "timeout": "Timeout", + "motion_off_delay": "Motion sensor off delay (seconds)", + "playback_months": "Playback range (months)", + "playback_thumbnails": "Create thumbnails for playback items", + "playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/reolink_dev/switch.py b/custom_components/reolink_dev/switch.py new file mode 100644 index 0000000..75e36b9 --- /dev/null +++ b/custom_components/reolink_dev/switch.py @@ -0,0 +1,269 @@ +"""This component provides support many for Reolink IP cameras switches.""" +import asyncio +import logging + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH +from homeassistant.helpers.entity import ToggleEntity + +from .const import BASE, DOMAIN +from .entity import ReolinkEntity + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the Reolink IP Camera switches.""" + devices = [] + base = hass.data[DOMAIN][config_entry.entry_id][BASE] + + for capability in await base.api.get_switch_capabilities(): + if capability == "ftp": + devices.append(FTPSwitch(hass, config_entry)) + elif capability == "email": + devices.append(EmailSwitch(hass, config_entry)) + elif capability == "audio": + devices.append(AudioSwitch(hass, config_entry)) + elif capability == "irLights": + devices.append(IRLightsSwitch(hass, config_entry)) + elif capability == "recording": + devices.append(RecordingSwitch(hass, config_entry)) + else: + continue + + async_add_devices(devices, update_before_add=False) + + +class FTPSwitch(ReolinkEntity, ToggleEntity): + """An implementation of a Reolink IP camera FTP switch.""" + + def __init__(self, hass, config): + """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + ToggleEntity.__init__(self) + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_ftpSwitch_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} FTP" + + @property + def is_on(self): + """Camera Motion FTP upload Status.""" + return self._base.api.ftp_state + + @property + def device_class(self): + """Device class of the switch.""" + return DEVICE_CLASS_SWITCH + + @property + def icon(self): + """Icon of the switch.""" + if self.is_on: + return "mdi:folder-upload" + + return "mdi:folder-remove" + + async def async_turn_on(self, **kwargs): + """Enable motion ftp recording.""" + await self._base.api.set_ftp(True) + await self.request_refresh() + + async def async_turn_off(self, **kwargs): + """Disable motion ftp recording.""" + await self._base.api.set_ftp(False) + await self.request_refresh() + + +class EmailSwitch(ReolinkEntity, ToggleEntity): + """An implementation of a Reolink IP camera email switch.""" + + def __init__(self, hass, config): + """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + ToggleEntity.__init__(self) + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_emailSwitch_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} email" + + @property + def is_on(self): + """Camera Motion email upload Status.""" + return self._base.api.email_state + + @property + def device_class(self): + """Device class of the switch.""" + return DEVICE_CLASS_SWITCH + + @property + def icon(self): + """Icon of the switch.""" + if self.is_on: + return "mdi:email" + + return "mdi:email-outline" + + async def async_turn_on(self, **kwargs): + """Enable motion email notification.""" + await self._base.api.set_email(True) + await self.request_refresh() + + async def async_turn_off(self, **kwargs): + """Disable motion email notification.""" + await self._base.api.set_email(False) + await self.request_refresh() + + +class IRLightsSwitch(ReolinkEntity, ToggleEntity): + """An implementation of a Reolink IP camera ir lights switch.""" + + def __init__(self, hass, config): + """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + ToggleEntity.__init__(self) + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_irLightsSwitch_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} IR lights" + + @property + def is_on(self): + """Camera Motion ir lights Status.""" + return self._base.api.ir_state + + @property + def device_class(self): + """Device class of the switch.""" + return DEVICE_CLASS_SWITCH + + @property + def icon(self): + """Icon of the switch.""" + if self.is_on: + return "mdi:flashlight" + + return "mdi:flashlight-off" + + async def async_turn_on(self, **kwargs): + """Enable motion ir lights.""" + await self._base.api.set_ir_lights(True) + await self.request_refresh() + + async def async_turn_off(self, **kwargs): + """Disable motion ir lights.""" + await self._base.api.set_ir_lights(False) + await self.request_refresh() + + +class RecordingSwitch(ReolinkEntity, ToggleEntity): + """An implementation of a Reolink IP camera recording switch.""" + + def __init__(self, hass, config): + """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + ToggleEntity.__init__(self) + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_recordingSwitch_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} recording" + + @property + def is_on(self): + """Camera recording upload Status.""" + return self._base.api.recording_state + + @property + def device_class(self): + """Device class of the switch.""" + return DEVICE_CLASS_SWITCH + + @property + def icon(self): + """Icon of the switch.""" + if self.is_on: + return "mdi:filmstrip" + + return "mdi:filmstrip-off" + + async def async_turn_on(self, **kwargs): + """Enable recording.""" + await self._base.api.set_recording(True) + await self.request_refresh() + + async def async_turn_off(self, **kwargs): + """Disable recording.""" + await self._base.api.set_recording(False) + await self.request_refresh() + + +class AudioSwitch(ReolinkEntity, ToggleEntity): + """An implementation of a Reolink IP camera audio switch.""" + + def __init__(self, hass, config): + """Initialize a Reolink camera.""" + ReolinkEntity.__init__(self, hass, config) + ToggleEntity.__init__(self) + + @property + def unique_id(self): + """Return Unique ID string.""" + return f"reolink_audioSwitch_{self._base.unique_id}" + + @property + def name(self): + """Return the name of this camera.""" + return f"{self._base.name} record audio" + + @property + def is_on(self): + """Camera audio switch Status.""" + return self._base.api.audio_state + + @property + def device_class(self): + """Device class of the switch.""" + return DEVICE_CLASS_SWITCH + + @property + def icon(self): + """Icon of the switch.""" + if self.is_on: + return "mdi:volume-high" + + return "mdi:volume-off" + + async def async_turn_on(self, **kwargs): + """Enable audio recording.""" + await self._base.api.set_audio(True) + await self.request_refresh() + + async def async_turn_off(self, **kwargs): + """Disable audio recording.""" + await self._base.api.set_audio(False) + await self.request_refresh() diff --git a/custom_components/reolink_dev/translations/de.json b/custom_components/reolink_dev/translations/de.json new file mode 100644 index 0000000..c5e1229 --- /dev/null +++ b/custom_components/reolink_dev/translations/de.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Diese Kamera ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung zur Kamera konnte nicht hergestellt werden", + "invalid_auth": "Benutzername oder Passwort fehlerhaft", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "username": "Benutzername", + "password": "Passwort" + } + }, + "nvr": { + "data": { + "channel": "Kanal" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll", + "stream": "Stream", + "timeout": "Timeout (Sekunden)", + "motion_off_delay": "Bewegungssensor Ausschaltverzögerung (Sekunden)" + } + } + } + } +} diff --git a/custom_components/reolink_dev/translations/en.json b/custom_components/reolink_dev/translations/en.json new file mode 100644 index 0000000..4ca8545 --- /dev/null +++ b/custom_components/reolink_dev/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "This camera is already configured" + }, + "error": { + "cannot_connect": "Failed to connect with the camera", + "invalid_auth": "Invalid username or password", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "username": "Username", + "password": "Password" + } + }, + "nvr": { + "data": { + "channel": "Channel" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocol", + "stream": "Stream", + "timeout": "Timeout (seconds)", + "motion_off_delay": "Motion sensor off delay (seconds)", + "playback_months": "Playback range (months)", + "playback_thumbnails": "Create thumbnails for playback items", + "playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/reolink_dev/translations/es.json b/custom_components/reolink_dev/translations/es.json new file mode 100644 index 0000000..424ee34 --- /dev/null +++ b/custom_components/reolink_dev/translations/es.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cámara ya ha sido configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar con la cámara", + "invalid_auth": "Nombre de usuario o contraseña incorrecta", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Dirección", + "port": "Puerto", + "username": "Usuario", + "password": "Contraseña" + } + }, + "nvr": { + "data": { + "channel": "Canal" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocolo", + "stream": "Transferencia", + "timeout": "Tiempo fuera (segundos)", + "motion_off_delay": "Sensor de movimiento apagado retardo (segundos)" + } + } + } + } +} diff --git a/custom_components/reolink_dev/translations/fr.json b/custom_components/reolink_dev/translations/fr.json new file mode 100644 index 0000000..77e2190 --- /dev/null +++ b/custom_components/reolink_dev/translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cette caméra est déjà configurée" + }, + "error": { + "cannot_connect": "Impossible de se connecter à la caméra", + "invalid_auth": "Mauvais nom d'utilisateur et/ou mot de passe", + "unknown": "Erreur inconnue" + }, + "step": { + "user": { + "data": { + "host": "Hôte", + "port": "Port", + "username": "Nom d'utilisateur", + "password": "Mot de passe" + } + }, + "nvr": { + "data": { + "channel": "Chaîne" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocole", + "stream": "Flux", + "timeout": "Temporisation (en secondes)", + "motion_off_delay": "Délai de désactivation du capteur de mouvements (en secondes)" + } + } + } + } +} diff --git a/custom_components/reolink_dev/translations/il.json b/custom_components/reolink_dev/translations/il.json new file mode 100644 index 0000000..0fd9122 --- /dev/null +++ b/custom_components/reolink_dev/translations/il.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "כבר מקונפג" + }, + "error": { + "cannot_connect": "אין אפשרות להתחבר", + "invalid_auth": "הרשאה לא נכונה", + "unknown": "בעיה לא ידועה" + }, + "step": { + "user": { + "data": { + "host": "שרת", + "port": "פורט", + "username": "שם משתמש", + "password": "סיסמא" + } + }, + "nvr": { + "data": { + "channel": "ערוץ" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "פרוטוקול", + "stream": "סטרים", + "timeout": "טיימאאוט", + "motion_off_delay": "השהיית כבוי תזוזה" + } + } + } + } +} diff --git a/custom_components/reolink_dev/translations/nl.json b/custom_components/reolink_dev/translations/nl.json new file mode 100644 index 0000000..3a34eab --- /dev/null +++ b/custom_components/reolink_dev/translations/nl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Deze camera is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan niet verbinden met de camera", + "invalid_auth": "Ongeldige gebruikersnaam of wachtwoord", + "unknown": "Onbekende fout opgetreden" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort", + "username": "Gebruikersnaam", + "password": "Wachtwoord" + } + }, + "nvr": { + "data": { + "channel": "Kanaal" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocol", + "stream": "Stream", + "timeout": "Timeout (seconden)", + "motion_off_delay": "Bewegingssensor uit vertraging (seconden)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/reolink_dev/translations/pl.json b/custom_components/reolink_dev/translations/pl.json new file mode 100644 index 0000000..b9e397b --- /dev/null +++ b/custom_components/reolink_dev/translations/pl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ta kamera jest już skonfigurowana" + }, + "error": { + "cannot_connect": "Nie udało się połączyć z kamerą", + "invalid_auth": "Nieprawidłowy użytkownik lub hasło", + "unknown": "Niespodziewany błąd" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "username": "Nazwa użytkownika", + "password": "Hasło" + } + }, + "nvr": { + "data": { + "channel": "Kanał" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokół", + "stream": "Stream", + "timeout": "Timeout (sekundy)", + "motion_off_delay": "Opóźnienie wyłączenia czujnika ruchu (sekundy)" + } + } + } + } +} diff --git a/custom_components/reolink_dev/translations/se.json b/custom_components/reolink_dev/translations/se.json new file mode 100644 index 0000000..a191bf7 --- /dev/null +++ b/custom_components/reolink_dev/translations/se.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Denna kamera är redan konfiguerad" + }, + "error": { + "cannot_connect": "Misslyckades med att ansluta till kameran", + "invalid_auth": "Fel användarnamn eller lösenord", + "unknown": "Oförväntat fel" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "username": "Användarnamn", + "password": "Lösenord" + } + }, + "nvr": { + "data": { + "channel": "Kanal" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll", + "stream": "Ström", + "timeout": "Timeout (sekunder)", + "motion_off_delay": "Rörelsesensor avstängningfördröjning (sekunder)" + } + } + } + } +} diff --git a/custom_components/reolink_dev/typings.py b/custom_components/reolink_dev/typings.py new file mode 100644 index 0000000..f442a79 --- /dev/null +++ b/custom_components/reolink_dev/typings.py @@ -0,0 +1,31 @@ +""" Typing declarations for strongly typed dictionaries """ + +from typing import Any, Dict, List, TypedDict +from datetime import datetime, date + +VodEvent = TypedDict( + "VodEvent", + { + "start": datetime, + "end": datetime, + "file": str, + "thumbnail": Any, + }, + total=False, +) + +MediaSourceCacheEntry = TypedDict( + "MediaSourceCacheEntry", + { + "entry_id": str, + "unique_id": str, + "event_id": str, + "name": str, + "playback_months": int, + "playback_thumbnails": bool, + "playback_thumbnail_offset": int, + "playback_day_entries": List[date], + "playback_events": Dict[str, VodEvent], + }, + total=False, +) diff --git a/ui-lovelace.yaml b/ui-lovelace.yaml index 70b05d2..79fd880 100644 --- a/ui-lovelace.yaml +++ b/ui-lovelace.yaml @@ -1,28 +1,16 @@ # Icons @ https://materialdesignicons.com/ +# FontAwesome implementation @ https://github.com/thomasloven/hass-fontawesome title: Making Home Great Again views: - !include config/views/overview.yaml - - !include config/views/floorplan.yaml - !include config/views/lights.yaml - - !include config/views/livingroom.yaml + - !include config/views/media.yaml + - !include config/views/robots.yaml - !include config/views/humidity.yaml - - !include config/views/spotify.yaml + - !include config/views/floorplan.yaml - !include config/views/instagram.yaml - !include config/views/devices.yaml - - !include config/views/landroid.yaml - !include config/views/cctv.yaml - !include config/views/network.yaml - !include config/views/alerts.yaml - -resources: - - url: /local/lovelace/resources/weather-card.js - type: module - - url: /local/lovelace/resources/auto-entities.js - type: module - - url: /local/lovelace/resources/color-lite-card.js - type: js - # https://github.com/bradcrc/color-lite-card/ - - url: /local/lovelace/resources/now-playing-card.js - type: js - # https://github.com/bradcrc/Now-Playing-Card diff --git a/www/images/playstation.jpg b/www/images/playstation.jpg deleted file mode 100644 index b7d0bdec5a4aa43be682c0b28bfcb42b2607296b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34380 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 diff --git a/www/lovelace/custom/auto-entities/auto-entities.js b/www/lovelace/custom/auto-entities/auto-entities.js new file mode 100644 index 0000000..329c9f8 --- /dev/null +++ b/www/lovelace/custom/auto-entities/auto-entities.js @@ -0,0 +1,194 @@ +function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,n);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(r=(o<3?s(r):o>3?s(e,i,r):s(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r}const e="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,i=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},n=`{{lit-${String(Math.random()).slice(2)}}}`,s=`\x3c!--${n}--\x3e`,o=new RegExp(`${n}|${s}`);class r{constructor(t,e){this.parts=[],this.element=e;const i=[],s=[],r=document.createTreeWalker(e.content,133,null,!1);let c=0,h=-1,u=0;const{strings:p,values:{length:f}}=t;for(;u0;){const e=p[u],i=d.exec(e)[2],n=i.toLowerCase()+"$lit$",s=t.getAttribute(n);t.removeAttribute(n);const r=s.split(o);this.parts.push({type:"attribute",index:h,name:i,strings:r}),u+=r.length-1}}"TEMPLATE"===t.tagName&&(s.push(t),r.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(n)>=0){const n=t.parentNode,s=e.split(o),r=s.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},c=t=>-1!==t.index,l=()=>document.createComment(""),d=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function h(t,e){const{element:{content:i},parts:n}=t,s=document.createTreeWalker(i,133,null,!1);let o=p(n),r=n[o],a=-1,c=0;const l=[];let d=null;for(;s.nextNode();){a++;const t=s.currentNode;for(t.previousSibling===d&&(d=null),e.has(t)&&(l.push(t),null===d&&(d=t)),null!==d&&c++;void 0!==r&&r.index===a;)r.index=null!==d?-1:r.index-c,o=p(n,o),r=n[o]}l.forEach((t=>t.parentNode.removeChild(t)))}const u=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},p=(t,e=-1)=>{for(let i=e+1;i"function"==typeof t&&f.has(t),_={},m={};class v{constructor(t,e,i){this.__parts=[],this.template=t,this.processor=e,this.options=i}update(t){let e=0;for(const i of this.__parts)void 0!==i&&i.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const t=e?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],n=this.template.parts,s=document.createTreeWalker(t,133,null,!1);let o,r=0,a=0,l=s.nextNode();for(;rt}),w=` ${n} `;class b{constructor(t,e,i,n){this.strings=t,this.values=e,this.type=i,this.processor=n}getHTML(){const t=this.strings.length-1;let e="",i=!1;for(let o=0;o-1||i)&&-1===t.indexOf("--\x3e",r+1);const a=d.exec(t);e+=null===a?t+(i?w:s):t.substr(0,a.index)+a[1]+a[2]+"$lit$"+a[3]+n}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==y&&(e=y.createHTML(e)),t.innerHTML=e,t}}const S=t=>null===t||!("object"==typeof t||"function"==typeof t),C=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class E{constructor(t,e,i){this.dirty=!0,this.element=t,this.name=e,this.strings=i,this.parts=[];for(let t=0;t{try{const t={get capture(){return T=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class k{constructor(t,e,i){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=i,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;g(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=_,t(this)}if(this.__pendingValue===_)return;const t=this.__pendingValue,e=this.value,i=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),n=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=A(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=_}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const A=t=>t&&(T?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function j(t){let e=M.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},M.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const s=t.strings.join(n);return i=e.keyString.get(s),void 0===i&&(i=new r(t,t.getTemplateElement()),e.keyString.set(s,i)),e.stringsArray.set(t.strings,i),i}const M=new Map,F=new WeakMap;const U=new class{handleAttributeExpressions(t,e,i,n){const s=e[0];if("."===s){return new $(t,e.slice(1),i).parts}if("@"===s)return[new k(t,e.slice(1),n.eventContext)];if("?"===s)return[new O(t,e.slice(1),i)];return new E(t,e,i).parts}handleTextExpression(t){return new P(t)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");const V=(t,...e)=>new b(t,e,"html",U),I=(t,e)=>`${t}--${e}`;let R=!0;void 0===window.ShadyCSS?R=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),R=!1);const D=t=>e=>{const i=I(e.type,t);let s=M.get(i);void 0===s&&(s={stringsArray:new WeakMap,keyString:new Map},M.set(i,s));let o=s.stringsArray.get(e.strings);if(void 0!==o)return o;const a=e.strings.join(n);if(o=s.keyString.get(a),void 0===o){const i=e.getTemplateElement();R&&window.ShadyCSS.prepareTemplateDom(i,t),o=new r(e,i),s.keyString.set(a,o)}return s.stringsArray.set(e.strings,o),o},q=["html","svg"],G=new Set,W=(t,e,i)=>{G.add(t);const n=i?i.element:document.createElement("template"),s=e.querySelectorAll("style"),{length:o}=s;if(0===o)return void window.ShadyCSS.prepareTemplateStyles(n,t);const r=document.createElement("style");for(let t=0;t{q.forEach((e=>{const i=M.get(I(e,t));void 0!==i&&i.keyString.forEach((t=>{const{element:{content:e}}=t,i=new Set;Array.from(e.querySelectorAll("style")).forEach((t=>{i.add(t)})),h(t,i)}))}))})(t);const a=n.content;i?function(t,e,i=null){const{element:{content:n},parts:s}=t;if(null==i)return void n.appendChild(e);const o=document.createTreeWalker(n,133,null,!1);let r=p(s),a=0,c=-1;for(;o.nextNode();)for(c++,o.currentNode===i&&(a=u(e),i.parentNode.insertBefore(e,i));-1!==r&&s[r].index===c;){if(a>0){for(;-1!==r;)s[r].index+=a,r=p(s,r);return}r=p(s,r)}}(i,r,a.firstChild):a.insertBefore(r,a.firstChild),window.ShadyCSS.prepareTemplateStyles(n,t);const c=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==c)e.insertBefore(c.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(r,a.firstChild);const t=new Set;t.add(r),h(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const z={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},L=(t,e)=>e!==t&&(e==e||t==t),B={attribute:!0,type:String,converter:z,reflect:!1,hasChanged:L};class J extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const n=this._attributeNameForProperty(i,e);void 0!==n&&(this._attributeToPropertyMap.set(n,i),t.push(n))})),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach(((t,e)=>this._classProperties.set(e,t)))}}static createProperty(t,e=B){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():`__${t}`,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const s=this[t];this[e]=n,this.requestUpdateInternal(t,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||B}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty("finalized")||t.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const i of e)this.createProperty(i,t[i])}}static _attributeNameForProperty(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,i=L){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,n=e.converter||z,s="function"==typeof n?n:n.fromAttribute;return s?s(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,n=e.converter;return(n&&n.toAttribute||z.toAttribute)(t,i)}initialize(){this._updateState=0,this._updatePromise=new Promise((t=>this._enableUpdatingResolver=t)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((t,e)=>this[e]=t)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,i){e!==i&&this._attributeToProperty(t,i)}_propertyToAttribute(t,e,i=B){const n=this.constructor,s=n._attributeNameForProperty(t,i);if(void 0!==s){const t=n._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(s):this.setAttribute(s,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,n=i._attributeToPropertyMap.get(t);if(void 0!==n){const t=i.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let n=!0;if(void 0!==t){const s=this.constructor;i=i||s.getPropertyOptions(t),s._valueHasChanged(this[t],e,i.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==i.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,i))):n=!1}!this._hasRequestedUpdate&&n&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this.requestUpdateInternal(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(t){}const t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{t=this.shouldUpdate(e),t?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((t,e)=>this._propertyToAttribute(e,this[e],t))),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}J.finalized=!0;const H=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object.assign(Object.assign({},e),{finisher(i){i.createProperty(e.key,t)}}):{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function K(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):H(t,e)}function Y(t){return K({attribute:!1,hasChanged:null==t?void 0:t.hasChanged})}const Q=(t,e,i)=>{Object.defineProperty(e,i,t)},X=(t,e)=>({kind:"method",placement:"prototype",key:e.key,descriptor:t}),Z=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,tt=Symbol();class et{constructor(t,e){if(e!==tt)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(Z?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const it=(t,...e)=>{const i=e.reduce(((e,i,n)=>e+(t=>{if(t instanceof et)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[n+1]),t[0]);return new et(i,tt)};(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const nt={};class st extends J{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(Array.isArray(t)){const e=(t,i)=>t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),n=[];i.forEach((t=>n.unshift(t))),this._styles=n}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!Z){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new et(String(e),tt)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?Z?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==nt&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return nt}}function ot(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}st.finalized=!0,st.render=(t,e,n)=>{if(!n||"object"!=typeof n||!n.scopeName)throw new Error("The `scopeName` option is required.");const s=n.scopeName,o=F.has(e),r=R&&11===e.nodeType&&!!e.host,a=r&&!G.has(s),c=a?document.createDocumentFragment():e;if(((t,e,n)=>{let s=F.get(e);void 0===s&&(i(e,e.firstChild),F.set(e,s=new P(Object.assign({templateFactory:j},n))),s.appendInto(e)),s.setValue(t),s.commit()})(t,c,Object.assign({templateFactory:D(s)},n)),a){const t=F.get(c);F.delete(c);const n=t.value instanceof v?t.value.template:void 0;W(s,c,n),i(e,e.firstChild),e.appendChild(c),F.set(e,t)}!o&&r&&window.ShadyCSS.styleElement(e.host)};const rt="lovelace-player-device-id";function at(){if(!localStorage[rt]){const t=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage[rt]=fully.getDeviceId():localStorage[rt]=`${t()}${t()}-${t()}${t()}`}return localStorage[rt]}let ct=at();const lt=new URLSearchParams(window.location.search);var dt;function ht(t){return!!String(t).includes("{%")||(!!String(t).includes("{{")||void 0)}lt.get("deviceID")&&null!==(dt=lt.get("deviceID"))&&("clear"===dt?localStorage.removeItem(rt):localStorage[rt]=dt,ct=at()),window.cardMod_template_cache=window.cardMod_template_cache||{};const ut=window.cardMod_template_cache;async function pt(t,e,i){const n=ot().connection,s=JSON.stringify([e,i]);let o=ut[s];o?(o.callbacks.has(t)||ft(t),t(o.value),o.callbacks.add(t)):(ft(t),t(""),i=Object.assign({user:ot().user.name,browser:ct,hash:location.hash.substr(1)||""},i),ut[s]=o={template:e,variables:i,value:"",callbacks:new Set([t]),unsubscribe:n.subscribeMessage((t=>function(t,e){const i=ut[t];i&&(i.value=e.result,i.callbacks.forEach((t=>t(e.result))))}(s,t)),{type:"render_template",template:e,variables:i})})}async function ft(t){let e;for(const[i,n]of Object.entries(ut))if(n.callbacks.has(t)){n.callbacks.delete(t),0==n.callbacks.size&&(e=n.unsubscribe,delete ut[i]);break}e&&await(await e)()}var gt;function _t(t,e){if("string"==typeof e&&"string"==typeof t&&(t.startsWith("/")&&t.endsWith("/")||-1!==t.indexOf("*"))){return t.startsWith("/")||(t=`/^${t=t.replace(/\./g,".").replace(/\*/g,".*")}$/`),new RegExp(t.slice(1,-1)).test(e)}if("string"==typeof t){if(t.startsWith("<="))return parseFloat(e)<=parseFloat(t.substr(2));if(t.startsWith(">="))return parseFloat(e)>=parseFloat(t.substr(2));if(t.startsWith("<"))return parseFloat(e)"))return parseFloat(e)>parseFloat(t.substr(1));if(t.startsWith("!"))return parseFloat(e)!=parseFloat(t.substr(1));if(t.startsWith("="))return parseFloat(e)==parseFloat(t.substr(1))}return t===e}window.autoEntities_cache=null!==(gt=window.autoEntities_cache)&&void 0!==gt?gt:{};const mt=window.autoEntities_cache;async function vt(t){var e;return mt.areas=null!==(e=mt.areas)&&void 0!==e?e:await t.callWS({type:"config/area_registry/list"}),mt.areas}async function yt(t){var e;return mt.devices=null!==(e=mt.devices)&&void 0!==e?e:await t.callWS({type:"config/device_registry/list"}),mt.devices}async function wt(t){var e;return mt.entities=null!==(e=mt.entities)&&void 0!==e?e:await t.callWS({type:"config/entity_registry/list"}),mt.entities}const bt={options:async()=>!0,sort:async()=>!0,domain:async(t,e,i)=>_t(e,i.entity_id.split(".")[0]),entity_id:async(t,e,i)=>_t(e,i.entity_id),state:async(t,e,i)=>_t(e,i.state),name:async(t,e,i)=>{var n;return _t(e,null===(n=i.attributes)||void 0===n?void 0:n.friendly_name)},group:async(t,e,i)=>{var n,s,o;return null===(o=null===(s=null===(n=t.states[e])||void 0===n?void 0:n.attributes)||void 0===s?void 0:s.entity_id)||void 0===o?void 0:o.includes(i.entity_id)},attributes:async(t,e,i)=>{for(const[t,n]of Object.entries(e)){let e=t.split(" ")[0],s=i.attributes;for(const t of e.split(":"))s=s?s[t]:void 0;if(void 0===s||!_t(n,s))return!1}return!0},not:async(t,e,i)=>!await St(t,e,i.entity_id),or:async(t,e,i)=>{for(const n of e)if(await St(t,n,i.entity_id))return!0;return!1},device:async(t,e,i)=>{const n=(await wt(t)).find((t=>t.entity_id===i.entity_id));if(!n)return!1;const s=(await yt(t)).find((t=>t.id===n.device_id));return!!s&&(_t(e,s.name_by_user)||_t(e,s.name))},area:async(t,e,i)=>{const n=(await wt(t)).find((t=>t.entity_id===i.entity_id));if(!n)return!1;let s=(await vt(t)).find((t=>t.area_id===n.area_id));if(s)return _t(e,s.name);const o=(await yt(t)).find((t=>t.id===n.device_id));return!!o&&(s=(await vt(t)).find((t=>t.area_id===o.area_id)),!!s&&_t(e,s.name))},last_changed:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_changed).getTime())/6e4),last_updated:async(t,e,i)=>_t(e,((new Date).getTime()-new Date(i.last_updated).getTime())/6e4),last_triggered:async(t,e,i)=>{if(null==i.attributes.last_triggered)return!1;return _t(e,((new Date).getTime()-new Date(i.attributes.last_triggered).getTime())/6e4)}};async function St(t,e,i){var n;if(!t.states[i])return!1;for(let[s,o]of Object.entries(e))if(s=s.trim().split(" ")[0].trim(),!await(null===(n=bt[s])||void 0===n?void 0:n.call(bt,t,o,t.states[i])))return!1;return!0}function Ct(t,e,i){var n,s,o,r;const[a,c]=i.reverse?[-1,1]:[1,-1];return i.ignore_case&&(t=null!==(s=null===(n=null==t?void 0:t.toLowerCase)||void 0===n?void 0:n.call(t))&&void 0!==s?s:t,e=null!==(r=null===(o=null==e?void 0:e.toLowerCase)||void 0===o?void 0:o.call(e))&&void 0!==r?r:e),i.numeric&&(isNaN(parseFloat(t))&&isNaN(parseFloat(e))||(t=isNaN(parseFloat(t))?void 0:parseFloat(t),e=isNaN(parseFloat(e))?void 0:parseFloat(e))),void 0===t&&void 0===e?0:void 0===t?a:void 0===e||te?a:0}const Et={none:()=>0,domain:(t,e,i)=>{var n,s;return Ct(null===(n=null==t?void 0:t.entity_id)||void 0===n?void 0:n.split(".")[0],null===(s=null==e?void 0:e.entity_id)||void 0===s?void 0:s.split(".")[0],i)},entity_id:(t,e,i)=>Ct(null==t?void 0:t.entity_id,null==e?void 0:e.entity_id,i),friendly_name:(t,e,i)=>{var n,s,o,r;return Ct((null===(n=null==t?void 0:t.attributes)||void 0===n?void 0:n.friendly_name)||(null===(s=null==t?void 0:t.entity_id)||void 0===s?void 0:s.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},name:(t,e,i)=>{var n,s,o,r;return Ct((null===(n=null==t?void 0:t.attributes)||void 0===n?void 0:n.friendly_name)||(null===(s=null==t?void 0:t.entity_id)||void 0===s?void 0:s.split(".")[1]),(null===(o=null==e?void 0:e.attributes)||void 0===o?void 0:o.friendly_name)||(null===(r=null==e?void 0:e.entity_id)||void 0===r?void 0:r.split(".")[1]),i)},state:(t,e,i)=>Ct(null==t?void 0:t.state,null==e?void 0:e.state,i),attribute:(t,e,i)=>{var n;const[s,o]=(null==i?void 0:i.reverse)?[-1,1]:[1,-1];let r=null==t?void 0:t.attributes,a=null==e?void 0:e.attributes;for(const t of null===(n=null==i?void 0:i.attribute)||void 0===n?void 0:n.split(":")){if(void 0===r&&void 0===a)return 0;if(void 0===r)return s;if(void 0===a)return o;[r,a]=[r[t],a[t]]}return Ct(r,a,i)},last_changed:(t,e,i)=>(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_changed).getTime(),new Date(null==e?void 0:e.last_changed).getTime(),i)),last_updated:(t,e,i)=>(i.numeric=!0,Ct(new Date(null==t?void 0:t.last_updated).getTime(),new Date(null==e?void 0:e.last_updated).getTime(),i)),last_triggered:(t,e,i)=>{var n,s,o,r;return null==(null===(n=null==t?void 0:t.attributes)||void 0===n?void 0:n.last_triggered)||null==(null===(s=null==e?void 0:e.attributes)||void 0===s?void 0:s.last_triggered)?0:(i.numeric=!0,Ct(new Date(null===(o=null==t?void 0:t.attributes)||void 0===o?void 0:o.last_triggered).getTime(),new Date(null===(r=null==e?void 0:e.attributes)||void 0===r?void 0:r.last_triggered).getTime(),i))}};function xt(t,e){return function(i,n){var s,o;return null!==(o=null===(s=Et[e.method])||void 0===s?void 0:s.call(Et,t.states[i.entity],t.states[n.entity],e))&&void 0!==o?o:0}}var Pt="1.8.4";const Ot=["domain","entity_id","state","name","group","device","area","last_changed","last_updated","last_triggered"],$t=["none","domain","entity_id","friendly_name","state","last_changed","last_updated","last_triggered"];class Nt extends st{constructor(){super(...arguments),this._selectedTab=0,this._cardGUIMode=!0,this._cardGUIModeAvailable=!0}setConfig(t){this._config=JSON.parse(JSON.stringify(t))}_handleSwitchTab(t){this._selectedTab=parseInt(t.detail.index,10)}_addFilterGroup(){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include.push({domain:""}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_deleteFilterGroup(t){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include.splice(t,1),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_moveFilterGroup(t,e){this._config&&(this._config=Object.assign({},this._config),[this._config.filter.include[t],this._config.filter.include[t+e]]=[this._config.filter.include[t+e],this._config.filter.include[t]],this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_addSpecialEntry(){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include.push({type:""}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}async _changeSpecialEntry(t,e){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include[t]=e.detail.value,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}async _changeGroupOptions(t,e){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include[t].options=e.detail.value,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_addFilter(t){if(!this._config)return;this._config=Object.assign({},this._config);const e=Ot.find((e=>void 0===this._config.filter.include[t][e]));void 0!==e&&(this._config.filter.include[t][e]="",this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_removeFilter(t,e){if(this._config){if(this._config=Object.assign({},this._config),delete this._config.filter.include[t][e],0===Object.keys(this._config.filter.include[t]).length)return this._deleteFilterGroup(t);this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}}_changeFilterKey(t,e,i){if(!this._config)return;const n=Ot[i.target.selected];void 0!==n&&n!==e&&void 0!==this._config.filter.include[t][e]&&(this._config=Object.assign({},this._config),this._config.filter.include[t][n]=this._config.filter.include[t][e],delete this._config.filter.include[t][e],this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_changeFilterValue(t,e,i){this._config&&(this._config=Object.assign({},this._config),this._config.filter.include[t][e]=i.target.value,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_changeSortMethod(t){var e;if(!this._config)return;this._config=Object.assign({},this._config);const i=$t[t.target.selected];this._config.sort=null!==(e=this._config.sort)&&void 0!==e?e:{},this._config.sort.method=i,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_sortOptionToggle(t,e){var i;this._config&&(this._config=Object.assign({},this._config),this._config.sort=null!==(i=this._config.sort)&&void 0!==i?i:{},this._config.sort[t]=e.target.checked,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_showEmptyToggle(){this._config&&(this._config=Object.assign(Object.assign({},this._config),{show_empty:!1===this._config.show_empty}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_changeCardParam(t){if(!this._config)return;const e=""==t.target.value?void 0:t.target.value;this._config=Object.assign(Object.assign({},this._config),{card_param:e}),delete this._config.card[null!=e?e:"entities"],this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_getCardConfig(){const t=Object.assign({},this._config.card);return t[this._config.card_param||"entities"]=[],t}_handleCardPicked(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e.entities,this._config=Object.assign(Object.assign({},this._config),{card:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_handleCardConfigChanged(t){if(t.stopPropagation(),!this._config)return;const e=Object.assign({},t.detail.config);delete e[this._config.card_param||"entities"],this._config=Object.assign(Object.assign({},this._config),{card:e}),this._cardGUIModeAvailable=t.detail.guiModeAvailable,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}_deleteCard(t){this._config&&(this._config=Object.assign({},this._config),delete this._config.card,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}_toggleCardMode(t){var e;null===(e=this._cardEditorEl)||void 0===e||e.toggleMode()}_cardGUIModeChanged(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}render(){return this.hass&&this._config?V` +
+
+ + + + + +
+
+ ${[this._renderFilterEditor,this._renderSortEditor,this._renderCardEditor][this._selectedTab].bind(this)()} +
+
+ `:V``}_renderFilterEditor(){return V` + ${this._config.filter.include.map(((t,e)=>V` +
+
+ this._moveFilterGroup(e,-1)} + > + + + this._moveFilterGroup(e,1)} + > + + + this._deleteFilterGroup(e)} + > + + +
+ ${void 0===t.type?V` + ${Object.entries(t).map((([t,i],n)=>V` + ${Ot.includes(t)?V` +
+ + this._changeFilterKey(e,t,i)} + > + ${Ot.map((t=>V` ${t} `))} + + + this._changeFilterValue(e,t,i)} + > + this._removeFilter(e,t)} + > + + + +
+ `:"options"===t?V``:V`

Some filters are not shown

+

+ Please switch to the CODE EDITOR to access all + options. +

`} + `))} + this._addFilter(e)}> + Add filter + + this._changeGroupOptions(e,t)} + > + `:V`this._changeSpecialEntry(e,t)} + >`} +
+ `))} + + Add filter group + + + Add non-filter entry + + `}_renderSortEditor(){var t,e,i,n;return V` +
+ ${(null===(t=this._config.sort)||void 0===t?void 0:t.method)&&!$t.includes(this._config.sort.method)?V`

+ Your sort method is not handled by the GUI editor. +

+

Please switch to the CODE EDITOR to access all options.

`:V` + Method: + + + ${$t.map((t=>V` ${t} `))} + + + + this._sortOptionToggle("reverse",t)} + > + + `} +
+ `}_renderCardEditor(){var t;return V` +
+ + + + + + ${this._config.card?V` +
+ + ${!this._cardEditorEl||this._cardGUIMode?"Show code editor":"Show Visual Editor"} + + + + +
+ + `:V` + + `} +
+ `}static get styles(){return[it` + mwc-tab-bar { + border-bottom: 1px solid var(--divider-color); + } + + .filter, + .card { + margin-top: 8px; + border: 1px solid var(--divider-color); + padding: 12px; + } + .filter .option { + display: flex; + align-items: flex-end; + } + .filter .option paper-dropdown-menu { + margin-right: 16px; + width: 150px; + } + .filter .option paper-input { + flex-grow: 2; + } + + .filter .toolbar, + .card .card-options { + display: flex; + justify-content: flex-end; + width: 100%; + } + .gui-mode-button { + margin-right: auto; + } + `]}}function Tt(t,e){if(t===e)return!0;if(typeof t!=typeof e)return!1;if(!(t instanceof Object&&e instanceof Object))return!1;for(const i in t)if(t.hasOwnProperty(i)){if(!e.hasOwnProperty(i))return!1;if(t[i]!==e[i]){if("object"!=typeof t[i])return!1;if(!Tt(t[i],e[i]))return!1}}for(const i in e)if(e.hasOwnProperty(i)&&!t.hasOwnProperty(i))return!1;return!0}t([Y()],Nt.prototype,"_config",void 0),t([K()],Nt.prototype,"lovelace",void 0),t([K()],Nt.prototype,"hass",void 0),t([Y()],Nt.prototype,"_selectedTab",void 0),t([Y()],Nt.prototype,"_cardGUIMode",void 0),t([Y()],Nt.prototype,"_cardGUIModeAvailable",void 0),t([function(t,e){return(i,n)=>{const s={get(){return this.renderRoot.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof n?Symbol():`__${n}`;s.get=function(){return void 0===this[e]&&(this[e]=this.renderRoot.querySelector(t)),this[e]}}return void 0!==n?Q(s,i,n):X(s,i)}}("hui-card-element-editor")],Nt.prototype,"_cardEditorEl",void 0),customElements.define("auto-entities-editor",Nt),window.customCards=window.customCards||[],window.customCards.push({type:"auto-entities",name:"Auto Entities",preview:!1,description:"Entity Filter on Steroids. Auto Entities allows you to fill other cards with entities automatically, based on a number of attributes."});class kt extends st{constructor(){super(...arguments),this._updateCooldown={timer:void 0,rerun:!1},this._renderer=t=>{this._template="string"==typeof t?t.split(/[\s,]+/):t}}static getConfigElement(){return document.createElement("auto-entities-editor")}static getStubConfig(){return{card:{type:"entities"},filter:{include:[],exclude:[]}}}setConfig(t){var e,i;if(!t)throw new Error("No configuration.");if(!(null===(e=t.card)||void 0===e?void 0:e.type))throw new Error("No card type specified.");if(!t.filter&&!t.entities)throw new Error("No filters specified.");t=JSON.parse(JSON.stringify(t)),this._config=t,(null===(i=this._config.filter)||void 0===i?void 0:i.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:t}),this._cardBuilt=new Promise((t=>this._cardBuiltResolve=t)),queueMicrotask((()=>this.update_all()))}connectedCallback(){var t,e;super.connectedCallback(),(null===(e=null===(t=this._config)||void 0===t?void 0:t.filter)||void 0===e?void 0:e.template)&&ht(this._config.filter.template)&&pt(this._renderer,this._config.filter.template,{config:this._config})}disconnectedCallback(){super.disconnectedCallback(),ft(this._renderer)}async update_all(){if(this.card&&(this.card.hass=this.hass),this._updateCooldown.timer)return void(this._updateCooldown.rerun=!0);this._updateCooldown.rerun=!1,this._updateCooldown.timer=window.setTimeout((()=>{this._updateCooldown.timer=void 0,this._updateCooldown.rerun&&this.update_all()}),500);const t=await this.update_entities();this.update_card(t)}async update_card(t){var e,i;if(this._entities&&Tt(t,this._entities)&&Tt(this._cardConfig,this._config.card))return;const n=(null===(e=this._cardConfig)||void 0===e?void 0:e.type)!==this._config.card.type;this._entities=t,this._cardConfig=JSON.parse(JSON.stringify(this._config.card));const s=Object.assign({[this._config.card_param||"entities"]:t},this._config.card);if(!this.card||n){const t=await window.loadCardHelpers();this.card=await t.createCardElement(s)}else this.card.setConfig(s);null===(i=this._cardBuiltResolve)||void 0===i||i.call(this),this.card.hass=this.hass;const o=0===t.length&&!1===this._config.show_empty;this.style.display=o?"none":null,this.style.margin=o?"0":null}async update_entities(){var t,e,i;const n=t=>t?"string"==typeof t?{entity:t.trim()}:t:null;let s=[...(null===(e=null===(t=this._config)||void 0===t?void 0:t.entities)||void 0===e?void 0:e.map(n))||[]];if(!this.hass||!this._config.filter)return s;if(this._template&&(s=s.concat(this._template.map(n))),s=s.filter(Boolean),this._config.filter.include){const t=Object.keys(this.hass.states).map(n);for(const e of this._config.filter.include){if(e.type){s.push(e);continue}let i=[];for(const n of t)await St(this.hass,e,n.entity)&&i.push(JSON.parse(JSON.stringify(Object.assign(Object.assign({},n),e.options)).replace(/this.entity_id/g,n.entity)));e.sort&&(i=i.sort(xt(this.hass,e.sort))),s=s.concat(i)}}if(this._config.filter.exclude)for(const t of this._config.filter.exclude){const e=[];for(const i of s)void 0!==i.entity&&await St(this.hass,t,i.entity)||e.push(i);s=e}if(this._config.sort&&(s=s.sort(xt(this.hass,this._config.sort)),this._config.sort.count)){const t=null!==(i=this._config.sort.first)&&void 0!==i?i:0;s=s.slice(t,t+this._config.sort.count)}if(this._config.unique){let t=[];for(const e of s)"entity"===this._config.unique&&t.some((t=>t.entity===e.entity))||t.some((t=>Tt(t,e)))||t.push(e);s=t}return s}async updated(t){(t.has("_template")||t.has("hass")&&this.hass)&&queueMicrotask((()=>this.update_all()))}createRenderRoot(){return this}render(){return V`${this.card}`}async getCardSize(){var t,e;let i=0;return await this._cardBuilt,this.card&&this.card.getCardSize&&(i=await this.card.getCardSize()),1===i&&(null===(t=this._entities)||void 0===t?void 0:t.length)&&(i=this._entities.length),0===i&&(null===(e=this._config.filter)||void 0===e?void 0:e.include)&&(i=Object.keys(this._config.filter.include).length),i||5}}t([K()],kt.prototype,"_config",void 0),t([K()],kt.prototype,"hass",void 0),t([K()],kt.prototype,"card",void 0),t([K()],kt.prototype,"_template",void 0),customElements.get("auto-entities")||(customElements.define("auto-entities",kt),console.info(`%cAUTO-ENTITIES ${Pt} IS INSTALLED`,"color: green; font-weight: bold","")); diff --git a/www/lovelace/custom/color-lite-card/color-lite-card.js b/www/lovelace/custom/color-lite-card/color-lite-card.js new file mode 100644 index 0000000..07cd0a0 --- /dev/null +++ b/www/lovelace/custom/color-lite-card/color-lite-card.js @@ -0,0 +1,69 @@ +class ColorLite extends HTMLElement { + set hass(hass) { + if (!this.content) { + const card = document.createElement('ha-card'); + this.content = document.createElement('div'); + card.appendChild(this.content); + card.style.background = 'none'; + this.appendChild(card); + } + + const entityId = this.config.entity; + const state = hass.states[entityId]; + + +// if the light is on +if(state){ + if(state.state == 'on'){ + + const imageURLId = this.config.image; + var ImURL = imageURLId; + const imageURLCId = this.config.color_image; + var rgbval = state.attributes.rgb_color; + var hsval = state.attributes.hs_color; + var hsar = ""; + var min_bright = (this.config.min_brightness * 2.5); + var bright = state.attributes.brightness; + if (hsval) { + if (rgbval != "255,255,255") { + var hsar = ' hue-rotate(' + hsval[0] + 'deg)'; + if (imageURLCId) { + ImURL = imageURLCId; + } + } + } + var bbritef = bright; + if (min_bright > bright) { + bbritef = min_bright; + } + var bbrite = (bbritef / 205); + + this.content.innerHTML = ` + + +`; + + } else { + this.content.innerHTML = ` + + `; + } + } +} + + + setConfig(config) { + if (!config.entity) { + throw new Error('You need to define an entity'); + } + this.config = config; + } + + // The height of your card. Home Assistant uses this to automatically + // distribute all cards over the available columns. + getCardSize() { + return 3; + } +} + +customElements.define('color-lite-card', ColorLite); \ No newline at end of file diff --git a/www/lovelace/custom/now-playing-card/now-playing-card.js b/www/lovelace/custom/now-playing-card/now-playing-card.js new file mode 100644 index 0000000..8bfe224 --- /dev/null +++ b/www/lovelace/custom/now-playing-card/now-playing-card.js @@ -0,0 +1,102 @@ +class NowPlayingPoster extends HTMLElement { + set hass(hass) { + if (!this.content) { + const card = document.createElement('ha-card'); + this.content = document.createElement('div'); + + + //this.content.style = "!important;"; + + + card.appendChild(this.content); + card.style = "background: none;"; + this.appendChild(card); + + + } + + const offposter = this.config.off_image; + const entityId = this.config.entity; + const state = hass.states[entityId]; + const stateStr = state ? state.state : 'unavailable'; + + + + if (state) { + + const movposter = state.attributes.entity_picture; + + if (["playing", "on"].indexOf(stateStr) > -1 ) { + if ( !movposter ) { + if ( offposter ) { + this.content.innerHTML = ` + + + `; + } + else + { + this.content.innerHTML = ` + + `; + } + } + else + { + this.content.innerHTML = ` + + + `; + } + } + else + { + + if ( offposter ) { + this.content.innerHTML = ` + + + `; + } + else + { + this.content.innerHTML = ` + + `; + } + + } + + + } + else + { + + + this.content.innerHTML = ` + + `; + + } + + } + + + + + setConfig(config) { + if (!config.entity) { + throw new Error('You need to define an entity'); + } + this.config = config; + } + + + // The height of your card. Home Assistant uses this to automatically + // distribute all cards over the available columns. + getCardSize() { + return 3; + } +} + +customElements.define('now-playing-poster', NowPlayingPoster); diff --git a/www/lovelace/custom/vacuum-card/vacuum-card.js b/www/lovelace/custom/vacuum-card/vacuum-card.js new file mode 100644 index 0000000..301e288 --- /dev/null +++ b/www/lovelace/custom/vacuum-card/vacuum-card.js @@ -0,0 +1,674 @@ +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +const t="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,e=(t,e,a=null)=>{for(;e!==a;){const a=e.nextSibling;t.removeChild(e),e=a}},a=`{{lit-${String(Math.random()).slice(2)}}}`,o=`\x3c!--${a}--\x3e`,n=new RegExp(`${a}|${o}`);class r{constructor(t,e){this.parts=[],this.element=e;const o=[],r=[],s=document.createTreeWalker(e.content,133,null,!1);let u=0,p=-1,d=0;const{strings:h,values:{length:m}}=t;for(;d0;){const e=h[d],a=c.exec(e)[2],o=a.toLowerCase()+"$lit$",r=t.getAttribute(o);t.removeAttribute(o);const i=r.split(n);this.parts.push({type:"attribute",index:p,name:a,strings:i}),d+=i.length-1}}"TEMPLATE"===t.tagName&&(r.push(t),s.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(a)>=0){const a=t.parentNode,r=e.split(n),s=r.length-1;for(let e=0;e{const a=t.length-e.length;return a>=0&&t.slice(a)===e},s=t=>-1!==t.index,l=()=>document.createComment(""),c=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function u(t,e){const{element:{content:a},parts:o}=t,n=document.createTreeWalker(a,133,null,!1);let r=d(o),i=o[r],s=-1,l=0;const c=[];let u=null;for(;n.nextNode();){s++;const t=n.currentNode;for(t.previousSibling===u&&(u=null),e.has(t)&&(c.push(t),null===u&&(u=t)),null!==u&&l++;void 0!==i&&i.index===s;)i.index=null!==u?-1:i.index-l,r=d(o,r),i=o[r]}c.forEach(t=>t.parentNode.removeChild(t))}const p=t=>{let e=11===t.nodeType?0:1;const a=document.createTreeWalker(t,133,null,!1);for(;a.nextNode();)e++;return e},d=(t,e=-1)=>{for(let a=e+1;a"function"==typeof t&&h.has(t),f={},g={}; +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +class _{constructor(t,e,a){this.__parts=[],this.template=t,this.processor=e,this.options=a}update(t){let e=0;for(const a of this.__parts)void 0!==a&&a.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const e=t?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),a=[],o=this.template.parts,n=document.createTreeWalker(e,133,null,!1);let r,i=0,l=0,c=n.nextNode();for(;i-1||n)&&-1===t.indexOf("--\x3e",i+1);const s=c.exec(t);e+=null===s?t+(n?v:o):t.substr(0,s.index)+s[1]+s[2]+"$lit$"+s[3]+a}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");return t.innerHTML=this.getHTML(),t}} +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */const y=t=>null===t||!("object"==typeof t||"function"==typeof t),S=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class w{constructor(t,e,a){this.dirty=!0,this.element=t,this.name=e,this.strings=a,this.parts=[];for(let t=0;t{try{const t={get capture(){return x=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class j{constructor(t,e,a){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=a,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;m(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=f,t(this)}if(this.__pendingValue===f)return;const t=this.__pendingValue,e=this.value,a=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),o=null!=t&&(null==e||a);a&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),o&&(this.__options=W(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=f}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const W=t=>t&&(x?{capture:t.capture,passive:t.passive,once:t.once}:t.capture) +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */;function M(t){let e=C.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},C.set(t.type,e));let o=e.stringsArray.get(t.strings);if(void 0!==o)return o;const n=t.strings.join(a);return o=e.keyString.get(n),void 0===o&&(o=new r(t,t.getTemplateElement()),e.keyString.set(n,o)),e.stringsArray.set(t.strings,o),o}const C=new Map,P=new WeakMap; +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */const q=new +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +class{handleAttributeExpressions(t,e,a,o){const n=e[0];if("."===n){return new z(t,e.slice(1),a).parts}return"@"===n?[new j(t,e.slice(1),o.eventContext)]:"?"===n?[new N(t,e.slice(1),a)]:new w(t,e,a).parts}handleTextExpression(t){return new V(t)}}; +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.2.1");const K=(t,...e)=>new b(t,e,"html",q) +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */,I=(t,e)=>`${t}--${e}`;let U=!0;void 0===window.ShadyCSS?U=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),U=!1);const O=t=>e=>{const o=I(e.type,t);let n=C.get(o);void 0===n&&(n={stringsArray:new WeakMap,keyString:new Map},C.set(o,n));let i=n.stringsArray.get(e.strings);if(void 0!==i)return i;const s=e.strings.join(a);if(i=n.keyString.get(s),void 0===i){const a=e.getTemplateElement();U&&window.ShadyCSS.prepareTemplateDom(a,t),i=new r(e,a),n.keyString.set(s,i)}return n.stringsArray.set(e.strings,i),i},Z=["html","svg"],T=new Set,A=(t,e,a)=>{T.add(t);const o=a?a.element:document.createElement("template"),n=e.querySelectorAll("style"),{length:r}=n;if(0===r)return void window.ShadyCSS.prepareTemplateStyles(o,t);const i=document.createElement("style");for(let t=0;t{Z.forEach(e=>{const a=C.get(I(e,t));void 0!==a&&a.keyString.forEach(t=>{const{element:{content:e}}=t,a=new Set;Array.from(e.querySelectorAll("style")).forEach(t=>{a.add(t)}),u(t,a)})})})(t);const s=o.content;a?function(t,e,a=null){const{element:{content:o},parts:n}=t;if(null==a)return void o.appendChild(e);const r=document.createTreeWalker(o,133,null,!1);let i=d(n),s=0,l=-1;for(;r.nextNode();){for(l++,r.currentNode===a&&(s=p(e),a.parentNode.insertBefore(e,a));-1!==i&&n[i].index===l;){if(s>0){for(;-1!==i;)n[i].index+=s,i=d(n,i);return}i=d(n,i)}}}(a,i,s.firstChild):s.insertBefore(i,s.firstChild),window.ShadyCSS.prepareTemplateStyles(o,t);const l=s.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(a){s.insertBefore(i,s.firstChild);const t=new Set;t.add(i),u(a,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const R={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},J=(t,e)=>e!==t&&(e==e||t==t),G={attribute:!0,type:String,converter:R,reflect:!1,hasChanged:J};class Y extends HTMLElement{constructor(){super(),this._updateState=0,this._instanceProperties=void 0,this._updatePromise=new Promise(t=>this._enableUpdatingResolver=t),this._changedProperties=new Map,this._reflectingProperties=void 0,this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach((e,a)=>{const o=this._attributeNameForProperty(a,e);void 0!==o&&(this._attributeToPropertyMap.set(o,a),t.push(o))}),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach((t,e)=>this._classProperties.set(e,t))}}static createProperty(t,e=G){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const a="symbol"==typeof t?Symbol():"__"+t,o=this.getPropertyDescriptor(t,a,e);void 0!==o&&Object.defineProperty(this.prototype,t,o)}static getPropertyDescriptor(t,e,a){return{get(){return this[e]},set(a){const o=this[t];this[e]=a,this._requestUpdate(t,o)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||G}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty("finalized")||t.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const a of e)this.createProperty(a,t[a])}}static _attributeNameForProperty(t,e){const a=e.attribute;return!1===a?void 0:"string"==typeof a?a:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,a=J){return a(t,e)}static _propertyValueFromAttribute(t,e){const a=e.type,o=e.converter||R,n="function"==typeof o?o:o.fromAttribute;return n?n(t,a):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const a=e.type,o=e.converter;return(o&&o.toAttribute||R.toAttribute)(t,a)}initialize(){this._saveInstanceProperties(),this._requestUpdate()}_saveInstanceProperties(){this.constructor._classProperties.forEach((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}})}_applyInstanceProperties(){this._instanceProperties.forEach((t,e)=>this[e]=t),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,a){e!==a&&this._attributeToProperty(t,a)}_propertyToAttribute(t,e,a=G){const o=this.constructor,n=o._attributeNameForProperty(t,a);if(void 0!==n){const t=o._propertyValueToAttribute(e,a);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(n):this.setAttribute(n,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const a=this.constructor,o=a._attributeToPropertyMap.get(t);if(void 0!==o){const t=a.getPropertyOptions(o);this._updateState=16|this._updateState,this[o]=a._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}_requestUpdate(t,e){let a=!0;if(void 0!==t){const o=this.constructor,n=o.getPropertyOptions(t);o._valueHasChanged(this[t],e,n.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==n.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,n))):a=!1}!this._hasRequestedUpdate&&a&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this._requestUpdate(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(t){}const t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{t=this.shouldUpdate(e),t?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach((t,e)=>this._propertyToAttribute(e,this[e],t)),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}Y.finalized=!0; +/** +@license +Copyright (c) 2019 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at +http://polymer.github.io/LICENSE.txt The complete set of authors may be found at +http://polymer.github.io/AUTHORS.txt The complete set of contributors may be +found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as +part of the polymer project is also subject to an additional IP rights grant +found at http://polymer.github.io/PATENTS.txt +*/ +const X="adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,D=Symbol();class L{constructor(t,e){if(e!==D)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(X?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const F=(t,...e)=>{const a=e.reduce((e,a,o)=>e+(t=>{if(t instanceof L)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(a)+t[o+1],t[0]);return new L(a,D)}; +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ +(window.litElementVersions||(window.litElementVersions=[])).push("2.3.1");const Q={};class H extends Y{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(void 0===t)this._styles=[];else if(Array.isArray(t)){const e=(t,a)=>t.reduceRight((t,a)=>Array.isArray(a)?e(a,t):(t.add(a),t),a),a=e(t,new Set),o=[];a.forEach(t=>o.unshift(t)),this._styles=o}else this._styles=[t]}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?X?this.renderRoot.adoptedStyleSheets=t.map(t=>t.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map(t=>t.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==Q&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)}))}render(){return Q}}H.finalized=!0,H.render=(t,a,o)=>{if(!o||"object"!=typeof o||!o.scopeName)throw new Error("The `scopeName` option is required.");const n=o.scopeName,r=P.has(a),i=U&&11===a.nodeType&&!!a.host,s=i&&!T.has(n),l=s?document.createDocumentFragment():a;if(((t,a,o)=>{let n=P.get(a);void 0===n&&(e(a,a.firstChild),P.set(a,n=new V(Object.assign({templateFactory:M},o))),n.appendInto(a)),n.setValue(t),n.commit()})(t,l,Object.assign({templateFactory:O(n)},o)),s){const t=P.get(l);P.delete(l);const o=t.value instanceof _?t.value.template:void 0;A(n,l,o),e(a,a.firstChild),a.appendChild(l),P.set(a,t)}!r&&i&&window.ShadyCSS.styleElement(a.host)};var B=/d{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|ZZ|Z|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g,$="[^\\s]+",tt=/\[([^]*?)\]/gm;function et(t,e){for(var a=[],o=0,n=t.length;o-1?o:null}};function ot(t){for(var e=[],a=1;a3?0:(t-t%10!=10?1:0)*t%10]}},lt=ot({},st),ct=function(t,e){for(void 0===e&&(e=2),t=String(t);t.length0?"-":"+")+ct(100*Math.floor(Math.abs(e)/60)+Math.abs(e)%60,4)},Z:function(t){var e=t.getTimezoneOffset();return(e>0?"-":"+")+ct(Math.floor(Math.abs(e)/60),2)+":"+ct(Math.abs(e)%60,2)}},pt=function(t){return+t-1},dt=[null,"[1-9]\\d?"],ht=[null,$],mt=["isPm",$,function(t,e){var a=t.toLowerCase();return a===e.amPm[0]?0:a===e.amPm[1]?1:null}],ft=["timezoneOffset","[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z?",function(t){var e=(t+"").match(/([+-]|\d\d)/gi);if(e){var a=60*+e[1]+parseInt(e[2],10);return"+"===e[0]?a:-a}return 0}],gt=(at("monthNamesShort"),at("monthNames"),{default:"ddd MMM DD YYYY HH:mm:ss",shortDate:"M/D/YY",mediumDate:"MMM D, YYYY",longDate:"MMMM D, YYYY",fullDate:"dddd, MMMM D, YYYY",isoDate:"YYYY-MM-DD",isoDateTime:"YYYY-MM-DDTHH:mm:ssZ",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"});var _t=function(t,e,a){if(void 0===e&&(e=gt.default),void 0===a&&(a={}),"number"==typeof t&&(t=new Date(t)),"[object Date]"!==Object.prototype.toString.call(t)||isNaN(t.getTime()))throw new Error("Invalid Date pass to format");var o=[];e=(e=gt[e]||e).replace(tt,(function(t,e){return o.push(e),"@@@"}));var n=ot(ot({},lt),a);return(e=e.replace(B,(function(e){return ut[e](t,n)}))).replace(/@@@/g,(function(){return o.shift()}))},vt=(function(){try{(new Date).toLocaleDateString("i")}catch(t){return"RangeError"===t.name}}(),function(){try{(new Date).toLocaleString("i")}catch(t){return"RangeError"===t.name}}(),function(){try{(new Date).toLocaleTimeString("i")}catch(t){return"RangeError"===t.name}}(),function(t,e,a,o){o=o||{},a=null==a?{}:a;var n=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return n.detail=a,t.dispatchEvent(n),n});var bt="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},yt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,St=/^\w*$/,wt=/^\./,kt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Vt=/\\(\\)?/g,Nt=/^\[object .+?Constructor\]$/,zt="object"==typeof bt&&bt&&bt.Object===Object&&bt,Et="object"==typeof self&&self&&self.Object===Object&&self,xt=zt||Et||Function("return this")();var jt,Wt=Array.prototype,Mt=Function.prototype,Ct=Object.prototype,Pt=xt["__core-js_shared__"],qt=(jt=/[^.]+$/.exec(Pt&&Pt.keys&&Pt.keys.IE_PROTO||""))?"Symbol(src)_1."+jt:"",Kt=Mt.toString,It=Ct.hasOwnProperty,Ut=Ct.toString,Ot=RegExp("^"+Kt.call(It).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Zt=xt.Symbol,Tt=Wt.splice,At=Bt(xt,"Map"),Rt=Bt(Object,"create"),Jt=Zt?Zt.prototype:void 0,Gt=Jt?Jt.toString:void 0;function Yt(t){var e=-1,a=t?t.length:0;for(this.clear();++e-1},Xt.prototype.set=function(t,e){var a=this.__data__,o=Lt(a,t);return o<0?a.push([t,e]):a[o][1]=e,this},Dt.prototype.clear=function(){this.__data__={hash:new Yt,map:new(At||Xt),string:new Yt}},Dt.prototype.delete=function(t){return Ht(this,t).delete(t)},Dt.prototype.get=function(t){return Ht(this,t).get(t)},Dt.prototype.has=function(t){return Ht(this,t).has(t)},Dt.prototype.set=function(t,e){return Ht(this,t).set(t,e),this};var $t=ee((function(t){var e;t=null==(e=t)?"":function(t){if("string"==typeof t)return t;if(ne(t))return Gt?Gt.call(t):"";var e=t+"";return"0"==e&&1/t==-1/0?"-0":e}(e);var a=[];return wt.test(t)&&a.push(""),t.replace(kt,(function(t,e,o,n){a.push(o?n.replace(Vt,"$1"):e||t)})),a}));function te(t){if("string"==typeof t||ne(t))return t;var e=t+"";return"0"==e&&1/t==-1/0?"-0":e}function ee(t,e){if("function"!=typeof t||e&&"function"!=typeof e)throw new TypeError("Expected a function");var a=function(){var o=arguments,n=e?e.apply(this,o):o[0],r=a.cache;if(r.has(n))return r.get(n);var i=t.apply(this,o);return a.cache=r.set(n,i),i};return a.cache=new(ee.Cache||Dt),a}ee.Cache=Dt;var ae=Array.isArray;function oe(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function ne(t){return"symbol"==typeof t||function(t){return!!t&&"object"==typeof t}(t)&&"[object Symbol]"==Ut.call(t)}var re=function(t,e,a){var o=null==t?void 0:Ft(t,e);return void 0===o?a:o},ie={cleaning:"Cleaning",paused:"Paused",idle:"Idle",charging:"Charging","returning home":"Returning home",docked:"Docked"},se={gentle:"Gentle",silent:"Silent",standard:"Standard",medium:"Medium",turbo:"Turbo"},le={name:"Vacuum Card",description:"Vacuum card allows you to control your robot vacuum.",start:"Clean",continue:"Continue",pause:"Pause",stop:"Stop",return_to_base:"Dock",locate:"Locate vacuum",not_available:"Vacuum is not available"},ce={missing_entity:"Specifying entity is required!"},ue={entity:"Entity (Required)",map:"Map Camera (Optional)",image:"Image (Optional)",compact_view:"Compact View",compact_view_aria_label_on:"Toggle compact view on",compact_view_aria_label_off:"Toggle compact view off",show_name:"Show Name",show_name_aria_label_on:"Toggle display name on",show_name_aria_label_off:"Toggle display name off",show_status:"Show Status",show_status_aria_label_on:"Toggle display status on",show_status_aria_label_off:"Toggle display status off",show_toolbar:"Show Toolbar",show_toolbar_aria_label_on:"Toggle display toolbar on",show_toolbar_aria_label_off:"Toggle display toolbar off",code_only_note:"Note: Setting actions and stats options are available exclusively using Code Editor."},pe={status:ie,source:se,common:le,error:ce,editor:ue},de={cleaning:"Прибирає",paused:"Пауза",idle:"Очікує",charging:"Заряджається","returning home":"Повертається","segment cleaning":"Зоноване прибирання"},he={gentle:"Делікатний",silent:"Тихий",standard:"Стандартний",medium:"Середній",turbo:"Турбо"},me={name:"Пилосос",description:'Картка "пилосос" дозволяє керувати роботом-пилососом.',start:"Clean",continue:"Продовжити",pause:"Пауза",stop:"Стоп",return_to_base:"На базу",locate:"Знайти",not_available:"Пилосос недоступний"},fe={missing_entity:"Об’єкт є обов’язковим полем!"},ge={entity:"Об’єкт (Required)",map:"Камера для карти (Додатково)",image:"Зображення (Додатково)",compact_view:"Компактний перегляд",compact_view_aria_label_on:"Увімкнути компактний перегляд",compact_view_aria_label_off:"Вимкнути компактний перегляд",show_name:"Показувати ім’я?",show_name_aria_label_on:"Показати ім’я",show_name_aria_label_off:"Приховати ім’я",show_status:"Показувати статус?",show_status_aria_label_on:"Показати статус",show_status_aria_label_off:"Приховати статус",show_toolbar:"Показувати панель дій?",show_toolbar_aria_label_on:"Показати панель дій",show_toolbar_aria_label_off:"Приховати панель дій",code_only_note:"Увага: Опції actions та stats доступні виключно через редактор коду."},_e={status:de,source:he,common:me,error:fe,editor:ge},ve={cleaning:"Aan het schoonmaken",paused:"Gepauzeerd",idle:"Inactief",charging:"Aan het opladen","returning home":"Keert terug naar dock"},be={name:"Stofzuiger kaart",description:"Stofzuiger kaart maakt het makkelijk om je robotstofzuiger te bedienen.",start:"Start",continue:"Doorgaan",pause:"Pauze",stop:"Stop",return_to_base:"Terugkeren",locate:"Zoek stofzuiger"},ye={missing_entity:"Het specificeren van een entiteit is verplicht!"},Se={entity:"Entiteit (Verplicht)",map:"Kaart Camera (Optioneel)",image:"Afbeelding (Optioneel)",compact_view:"Compacte weergave",compact_view_aria_label_on:"Zet compacte weergave aan",compact_view_aria_label_off:"Zet compacte weergave uit",show_name:"Naam laten zien?",show_name_aria_label_on:"Zet weergavenaam aan",show_name_aria_label_off:"Zet weergavenaam uit",show_toolbar:"Werkbalk laten zien?",show_toolbar_aria_label_on:"Zet werkbalk aan",show_toolbar_aria_label_off:"Zet werkbalk uit",code_only_note:"Notitie: Instel acties en status opties zijn alleen beschikbaar in de Code Editor"},we={status:ve,common:be,error:ye,editor:Se},ke={cleaning:"Reinigen",paused:"Pausiert",idle:"Untätig",charging:"Aufladen","returning home":"Rückkehr zu Dockingstation","segment cleaning":"Zimmerreinigung",docked:"Angedockt"},Ve={name:"Vacuum Card",description:"Vacuum card ermöglicht es Ihnen, Ihr Staubsaugerroboter zu steuern.",start:"Reinigen",continue:"Weiter",pause:"Pause",stop:"Stop",return_to_base:"Dock",locate:"Staubsauger lokalisieren"},Ne={missing_entity:"Angabe der Entität ist erforderlich!"},ze={entity:"Entität (Erforderlich)",map:"Map Camera (Optional)",image:"Bild (Optional)",compact_view:"kompakte Ansicht",compact_view_aria_label_on:"Schalte kompakte Ansicht ein",compact_view_aria_label_off:"Schalte kompakte Ansicht aus",show_name:"Zeige Namen",show_name_aria_label_on:"Schalte 'Zeige Namen' ein",show_name_aria_label_off:"Schalte 'Zeige Namen' aus",show_toolbar:"Zeige Toolbar",show_toolbar_aria_label_on:"Schalte 'Zeige Toolbar' ein",show_toolbar_aria_label_off:"Schalte 'Zeige Toolbar' aus",code_only_note:"Hinweis: Das Festlegen von Aktionen und Statistikoptionen ist ausschließlich mit dem Code-Editor möglich."},Ee={status:ke,common:Ve,error:Ne,editor:ze},xe={cleaning:"Nettoyage",paused:"En pause",idle:"Inactif",charging:"En charge","returning home":"Retour à la base"},je={gentle:"Doux",silent:"Silencieux",standard:"Standard",medium:"Moyen",turbo:"Turbo"},We={name:"Vacuum Carte",description:"Vacuum carte vous permet de contrôler votre robot aspirateur.",start:"Nettoyer",continue:"Continuer",pause:"Pause",stop:"Stop",return_to_base:"Retour base",locate:"Localiser aspirateur",not_available:"L'aspirateur n'est pas disponible"},Me={missing_entity:"La spécification de l'entité est requise !"},Ce={entity:"Entité (obligatoire)",map:"Caméra de carte (facultatif)",image:"Image (facultatif)",compact_view:"Vue compacte",compact_view_aria_label_on:"Activer la vue compacte",compact_view_aria_label_off:"Désactiver la vue compacte",show_name:"Afficher le nom",show_name_aria_label_on:"Activer affichage du nom",show_name_aria_label_off:"Désactiver affichage du nom",show_status:"Afficher l'état",show_status_aria_label_on:"Activer l'affichage de l'état",show_status_aria_label_off:"Désactiver l'affichage de l'état",show_toolbar:"Afficher la barre d'outils",show_toolbar_aria_label_on:"Activer l'affichage de la barre d'outils",show_toolbar_aria_label_off:"Désactiver l'affichage de la barre d'outils",code_only_note:"Remarque: Les options de réglage des actions et statistiques sont disponibles exclusivement en utilisant l'éditeur de code."},Pe={status:xe,source:je,common:We,error:Me,editor:Ce},qe={cleaning:"Sprzątanie",paused:"Wstrzymany",idle:"Bezczynny",charging:"Ładowanie","returning home":"Powrót do bazy"},Ke={name:"Vacuum Card",description:"Vacuum card pozwala zdalnie kontrolować odkurzacz.",start:"Sprzątaj",continue:"Kontyntynuj",pause:"Wstrzymaj",stop:"Zatrzymaj",return_to_base:"Powrót",locate:"Zlokalizuj odkurzacz"},Ie={missing_entity:"Ustawienie encji jest wymagane!"},Ue={entity:"Encja (wymagane)",map:"Kamera (opcjonalne)",image:"Obrazek (opcjonalne)",compact_view:"Widok kompaktowy",compact_view_aria_label_on:"Włącz widok kompaktowy",compact_view_aria_label_off:"Wyłącz widok kompaktowy",show_name:"Pokaż nazwę",show_name_aria_label_on:"Włącz widok nazwy",show_name_aria_label_off:"Wyłącz widok nazwy",show_toolbar:"Pasek narzędzi",show_toolbar_aria_label_on:"Włącz pasek narzędzi",show_toolbar_aria_label_off:"Wyłącz pasek narzędzi",code_only_note:"Uwaga: Ustawianie opcji i informacji statystyk jest dostępne tylko poprzez edytor kodu YAML."},Oe={status:qe,common:Ke,error:Ie,editor:Ue},Ze={cleaning:"In pulizia",paused:"In pausa",idle:"Inattivo",charging:"In carica","returning home":"In rientro alla base"},Te={name:"Vacuum Card",description:"Vacuum card consente di controllare il tuo aspirapolvere.",start:"Pulisci",continue:"Continua",pause:"Pausa",stop:"Stop",return_to_base:"Base",locate:"Trova aspirapolvere"},Ae={missing_entity:"È necessario specificare l'entità!"},Re={entity:"Entità (Richiesto)",map:"Mappa (Opzionale)",image:"Immagine (Opzionale)",compact_view:"Vista compatta",compact_view_aria_label_on:"Attiva vista compatta",compact_view_aria_label_off:"Disattiva vista compatta",show_name:"Mostra Nome",show_name_aria_label_on:"Attiva nome",show_name_aria_label_off:"Disattiva nome",show_toolbar:"Mostra barra degli strumenti",show_toolbar_aria_label_on:"Attiva barra degli strumenti",show_toolbar_aria_label_off:"Disattiva barra degli strumenti",code_only_note:"NB: La configurazione di azioni e statistiche sono disponibili soltanto nell'editor di codice."},Je={status:Ze,common:Te,error:Ae,editor:Re},Ge={cleaning:"Убирает",paused:"Пауза",idle:"Ожидает",charging:"Заряжается","returning home":"Возвращается","segment cleaning":"Уборка зоны/комнаты"},Ye={gentle:"Деликатный",silent:"Тихий",standard:"Стандартный",medium:"Средний",turbo:"Турбо"},Xe={name:"Пылесос",description:'Карта "пылесос" позволяет управлять роботом-пылесосом.',start:"Запуск",continue:"Продолжить",pause:"Пауза",stop:"Остановить",return_to_base:"На базу",locate:"Найти",not_available:"Пылесос недоступен"},De={missing_entity:"Объект является обязательным полем!"},Le={entity:"Объект (Обязательное)",map:"Камера для карты (Опциональное)",image:"Изображение (Опциональное)",compact_view:"Компактный просмотр",compact_view_aria_label_on:"Включить компактный просмотр",compact_view_aria_label_off:"Выключить компактный просмотр",show_name:"Показать название?",show_name_aria_label_on:"Показать название",show_name_aria_label_off:"Скрыть название",show_status:"Показать статус?",show_status_aria_label_on:"Показать статус",show_status_aria_label_off:"Скрыть статус",show_toolbar:"Показать панель действий?",show_toolbar_aria_label_on:"Показать панель действий",show_toolbar_aria_label_off:"Скрыть панель действий",code_only_note:"Внимание: Опции actions и stats доступны исключительно через редактор кода."},Fe={status:Ge,source:Ye,common:Xe,error:De,editor:Le},Qe={cleaning:"Limpiando",paused:"En pausa",idle:"Inactivo",charging:"Cargando","returning home":"Volviendo a la base",docked:"En la base","segment cleaning":"Limpiando zona"},He={gentle:"Delicado",silent:"Silencioso",standard:"Estándar",medium:"Medio",turbo:"Turbo"},Be={name:"Vacuum Card",description:"Vacuum card te permite controlar tu robot aspirador.",start:"Comenzar",continue:"Continuar",pause:"Pausar",stop:"Detener",return_to_base:"Volver a la base",locate:"Localizar",not_available:"Vacuum no está disponible"},$e={missing_entity:"¡Se requiere especificar una entidad!"},ta={entity:"Entidad (Requerido)",map:"Map Camera (Opcional)",image:"Imagen (Opcional)",compact_view:"Vista compacta",compact_view_aria_label_on:"Activar vista compacta",compact_view_aria_label_off:"Desactivar vista compacta",show_name:"Nombre a mostrar",show_name_aria_label_on:"Mostrar nombre",show_name_aria_label_off:"Ocultar nombre",show_status:"Mostrar estado",show_status_aria_label_on:"Activar estado de la pantalla",show_status_aria_label_off:"Desactivar estado de la pantalla",show_toolbar:"Mostrar barra de herramientas",show_toolbar_aria_label_on:"Activar la barra de herramientas",show_toolbar_aria_label_off:"Desactivar la barra de herramientas",code_only_note:"Nota: La configuración de las acciones y estadísticas está únicamente disponible a través del Editor de Código."},ea={status:Qe,source:He,common:Be,error:$e,editor:ta},aa={cleaning:"Vysává se",paused:"Pozastaveno",idle:"Nečinný",charging:"Nabíjí se","returning home":"Vrací se domů"},oa={gentle:"Mírný",silent:"Tichý",standard:"Standardní",medium:"Střední",turbo:"Turbo"},na={name:"Karta vysavače",description:"Karta vysavače vám dovolí ovládat svůj vysavač.",start:"Začni vysávat",continue:"Pokračuj",pause:"Pozastav",stop:"Zastav",return_to_base:"Vrať se domů",locate:"Lokalizuj",not_available:"Vysavač není dostupný"},ra={missing_entity:"Je vyžadováno specifikování entity!"},ia={entity:"Entita (Povinný)",map:"Mapa (Nepovinný)",image:"Fotka (Nepovinný)",compact_view:"Kompaktní zobrazení",compact_view_aria_label_on:"Zapni kompaktní zobrazení",compact_view_aria_label_off:"Vypni kompaktní zobrazení",show_name:"Zobraz název",show_name_aria_label_on:"Zapni zobrazení názvu",show_name_aria_label_off:"Vypni zobrazení názvu",show_status:"Zobraz status",show_status_aria_label_on:"Zapni zobrazení statusu",show_status_aria_label_off:"Vypni zobrazení statusu",show_toolbar:"Zobraz lištu",show_toolbar_aria_label_on:"Zapni zobrazení lišty",show_toolbar_aria_label_off:"Vypni zobrazení lišty",code_only_note:"Poznámka: Nastavení akcí a infa je dostupné pouze v editoru kódu."},sa={status:aa,source:oa,common:na,error:ra,editor:ia},la={cleaning:"Tisztítás",paused:"Szünet",idle:"Tétlen",charging:"Töltés","returning home":"Hazatérés"},ca={gentle:"Gyengéd",silent:"Csendes",standard:"Alap",medium:"Közepes",turbo:"Turbo"},ua={name:"Porszívó Kártya",description:"Ez a kártya lehetővé teszi, hogy robot porszívódat irányítsd.",start:"Tisztítás",continue:"Folytatás",pause:"Szünet",stop:"Megszakítás",return_to_base:"Hazatérés",locate:"Porszívó megkeresése",not_available:"A porszívó nem elérhető"},pa={missing_entity:"Entitás megadása kötelező!"},da={entity:"Entitás (Kötelező)",map:"Térkép kamera (Opcionális)",image:"Kép (Opcionális)",compact_view:"Kompakt nézet",compact_view_aria_label_on:"Kompakt nézet bekapcsolása",compact_view_aria_label_off:"Kompakt nézet kikapcsolása",show_name:"Név megjelenítése",show_name_aria_label_on:"Név megjelenítése",show_name_aria_label_off:"Név elrejtése",show_status:"Állapot megjelenítése",show_status_aria_label_on:"Állapot megjelenítése",show_status_aria_label_off:"Állapot elrejtése",show_toolbar:"Eszköztár megjelenítése",show_toolbar_aria_label_on:"Eszköztár megjelenítése",show_toolbar_aria_label_off:"Eszköztár elrejtése",code_only_note:"Megjegyzés: Parancsok és statisztikák beállítása csak a kódszerkesztőben elérhetőek."},ha={status:la,source:ca,common:ua,error:pa,editor:da},ma={cleaning:"מנקה",paused:"מושהה",idle:"סרק",charging:"בטעינה","returning home":"בחזרה הביתה"},fa={gentle:"עדין",silent:"שקט",standard:"רגיל",medium:"בינוני",turbo:"טורבו"},ga={name:"כרטיס שואב",description:"כרטיס שואב מאפשר לך שליטה על שואב האבק שלך.",start:"נקה",continue:"המשך",pause:"השהה",stop:"עצור",return_to_base:"הגינה",locate:"אתר שואב",not_available:"השואב אינו זמין"},_a={missing_entity:"יש צורך לציין ישות!"},va={entity:"ישות (נדרש)",map:"מצלמת מפה (אפשרי)",image:"תמונה (אפשרי)",compact_view:"תצוגה קומפקטית",compact_view_aria_label_on:"החלף תצוגה קומפקטית",compact_view_aria_label_off:"כבה את התצוגה הקומפקטית",show_name:"שם תצוגה",show_name_aria_label_on:"הפעל את שם התצוגה למצב מופעל",show_name_aria_label_off:"כבה את שם התצוגה",show_status:"הצג סטטוס",show_status_aria_label_on:"הפעל את מצב התצוגה למצב פעיל",show_status_aria_label_off:"כבה את מצב התצוגה",show_toolbar:"הצג סרגל כלים",show_toolbar_aria_label_on:"הפעל את סרגל הכלים לתצוגה",show_toolbar_aria_label_off:"כבה את סרגל הכלים לתצוגה",code_only_note:"הערה: הגדרת פעולות ואפשרויות סטטיסטיקה זמינות אך ורק באמצעות עורך הקוד."},ba={status:ma,source:fa,common:ga,error:_a,editor:va},ya={cleaning:"Städar",paused:"Pausad",idle:"Inaktiv",charging:"Laddar","returning home":"Återvänder hem"},Sa={gentle:"Extra försiktig",silent:"Eco - tyst",standard:"Standard",medium:"Medium",turbo:"Turbo"},wa={name:"Dammsugarkort",description:"Dammsugarkort låter dig att kontrollera din robotdammsugare.",start:"Städa",continue:"Fortsätt",pause:"Paus",stop:"Stopp",return_to_base:"Docka",locate:"Lokalisera dammsugare",not_available:"Dammsugare är inte tillgänglig"},ka={missing_entity:"Specificera entitet är obligatoriskt!"},Va={entity:"Entitet (Obligatoriskt)",map:"Kartkamera (Valfritt)",image:"Bild (Valfritt)",compact_view:"Kompakt vy",compact_view_aria_label_on:"Aktivera kompakt vy",compact_view_aria_label_off:"Inaktivera kompakt vy",show_name:"Visa namn",show_name_aria_label_on:"Aktivera namn",show_name_aria_label_off:"Inaktivera namn",show_status:"Visa status",show_status_aria_label_on:"Aktivera status",show_status_aria_label_off:"Inaktivera status",show_toolbar:"Visa verktygsvält",show_toolbar_aria_label_on:"Aktivera verktygsfält",show_toolbar_aria_label_off:"Inaktivera verktygsfält",code_only_note:"Obs! Inställningar för händelser och statistikalternativ är enbart tillgängliga med kodredigeraren."},Na={status:ya,source:Sa,common:wa,error:ka,editor:Va},za={cleaning:"Rengjøring",paused:"Pauset",idle:"Tomgang",charging:"Lader","returning home":"Returnerer hjem"},Ea={gentle:"Skånsom",silent:"Stille",standard:"Standard",medium:"Medium",turbo:"Turbo"},xa={name:"Støvsuger kort",description:"Støvsugerkortet lar deg kontrollere robotstøvsugeren din",start:"Rengjør",continue:"fortsett",pause:"Pause",stop:"Stop",return_to_base:"Dock",locate:"Lokaliser støvsuger",not_available:"Støvsugeren er ikke tilgjengelig"},ja={missing_entity:"Spesifiserende enhet kreves!"},Wa={entity:"Enhet (påkrevd)",map:"Kartkamera (valgfritt)",image:"Bilde (Valgfritt)",compact_view:"Kompakt visning",compact_view_aria_label_on:"Slå på kompakt visning",compact_view_aria_label_off:"Slå av kompakt visningf",show_name:"Vis navn",show_name_aria_label_on:"Slå visningsnavnet på",show_name_aria_label_off:"Slå visningsnavnet av",show_status:"Vis Status",show_status_aria_label_on:"Slå skjermstatus på",show_status_aria_label_off:"Slå skjermstatus av",show_toolbar:"Vis verktøylinjen",show_toolbar_aria_label_on:"Slå skjermverktøylinjen på",show_toolbar_aria_label_off:"Slå skjermverktøylinjen av",code_only_note:"Merk: Innstillingshandlinger og statistikkalternativer er eksklusivt tilgjengelige ved hjelp av Code Editor."},Ma={status:za,source:Ea,common:xa,error:ja,editor:Wa},Ca={cleaning:"Støvsuger",paused:"Pauset",idle:"Inaktiv",charging:"Lader","returning home":"Returnerer til dock"},Pa={gentle:"Mild",silent:"Stille",standard:"Standard",medium:"Medium",turbo:"Turbo"},qa={name:"Vacuum Card",description:"Vacuum card lader dig kontrollere din robotstøvsuger.",start:"Start",continue:"Fortsæt",pause:"Pause",stop:"Stop",return_to_base:"Gå til dock",locate:"Find støvsuger",not_available:"Støvsuger er ikke tilgængelig"},Ka={missing_entity:"En enhed skal specificeres!"},Ia={entity:"Enhed (Påkrævet)",map:"Map Camera (Valgfrit)",image:"Billede (Valgfrit)",compact_view:"Kompakt visning",compact_view_aria_label_on:"Slå kompakt visning til",compact_view_aria_label_off:"Slå kompakt visning fra",show_name:"Vis navn",show_name_aria_label_on:"Slå visning af navn til",show_name_aria_label_off:"Slå visning af navn fra",show_status:"Vis Status",show_status_aria_label_on:"Slå visning af status til",show_status_aria_label_off:"Slå visning af status fra",show_toolbar:"Vis værktøjslinje",show_toolbar_aria_label_on:"Slå visning af værktøjslinje til",show_toolbar_aria_label_off:"Slå visning af værktøjslinje fra",code_only_note:"Bemærk: Indstilling af actions og statistik er udelukkende muligt via Code Editor."},Ua={status:Ca,source:Pa,common:qa,error:Ka,editor:Ia},Oa={cleaning:"청소중",paused:"일시정지",idle:"대기중",charging:"충전중","returning home":"복귀중"},Za={gentle:"물걸레",silent:"저소음",standard:"밸런스",medium:"터보",turbo:"최강"},Ta={name:"청소기 카드",description:"청소기 카드는 로봇 청소기를 제어합니다.",start:"청소 시작",continue:"청소 재개",pause:"일시정지",stop:"정지",return_to_base:"복귀",locate:"청소기 위치",not_available:"청소기 사용 불가"},Aa={missing_entity:"구성요소를 선택해주세요."},Ra={entity:"구성요소 (필수 요소)",map:"지도 (선택 사항)",image:"이미지 (선택 사항)",compact_view:"간단히 보기",compact_view_aria_label_on:"간단히 보기 켜기",compact_view_aria_label_off:"간단히 보기 끄기",show_name:"이름 표시",show_name_aria_label_on:"이름 표시 켜기",show_name_aria_label_off:"이름 표시 끄기",show_status:"상태 표시",show_status_aria_label_on:"상태 표시 켜기",show_status_aria_label_off:"상태 표시 끄기",show_toolbar:"툴바 표시",show_toolbar_aria_label_on:"툴바 표시 켜기",show_toolbar_aria_label_off:"툴바 표시 끄기",code_only_note:"동작과 상태 설정은 코드 에디터에서 수정할 수 있습니다."},Ja={status:Oa,source:Za,common:Ta,error:Aa,editor:Ra},Ga={Cleaning:"Siivoaa",Paused:"Pysäytetty",Idle:"Toimeton",Charging:"Latauksessa","Returning home":"Palaa kotiin"},Ya={Gentle:"Hellävarainen",Silent:"Hiljainen",Standard:"Normaali",Medium:"Keskitaso",Turbo:"Turbo"},Xa={name:"Pölynimurikortti",description:"Pölynimurikortti sallii robotti imurin ohjauksen.",start:"Siivoa",continue:"Jatka",pause:"Tauko",stop:"Pysähdy",return_to_base:"Latausasemaan",locate:"Paikanna imuri",not_available:"Imuri ei saatavilla"},Da={missing_entity:"Entiteetti puuttuu!"},La={entity:"Entiteetti (Vaaditaan)",map:"Kartan kamera (Valinnainen)",image:"Kuva (Valinnainen)",compact_view:"Kompakti näkymä",compact_view_aria_label_on:"Kompakti näkymä päälle",compact_view_aria_label_off:"Kompakti näkymä pois",show_name:"Näytä Nimi",show_name_aria_label_on:"Näyttönimi päälle",show_name_aria_label_off:"Näyttönimi pois",show_status:"Näytä Tila",show_status_aria_label_on:"Tilanäyttö päälle",show_status_aria_label_off:"Tilanäyttö pois",show_toolbar:"Näytä työkalurivi",show_toolbar_aria_label_on:"Työkalurivi päälle",show_toolbar_aria_label_off:"Työkalurivi pois",code_only_note:"Huom: Toimintojen ja tilastojen optiot ovat saatavilla ainoastaan koodieditorissa"},Fa={status:Ga,source:Ya,common:Xa,error:Da,editor:La},Qa={cleaning:"Netejant",paused:"En pausa",idle:"Inactiu",charging:"Carregant","returning home":"Tornant a la base",docked:"A la base"},Ha={gentle:"Delicat",silent:"Silenciós",standard:"Estàndard",medium:"Mitjà",turbo:"Turbo"},Ba={name:"Vacuum Card",description:"Vacuum card us permet controlar el robot aspirador.",start:"Neteja",continue:"Continua",pause:"Pausa",stop:"Atura",return_to_base:"Torna a la base",locate:"Localitza",not_available:"No disponible"},$a={missing_entity:"Cal especificar una entitat."},to={entity:"Entitat (Requerit)",map:"Càmera de mapa (Opcional)",image:"Imatge (Opcional)",compact_view:"Visualització compacta",compact_view_aria_label_on:"Activar visualització compacta",compact_view_aria_label_off:"Desactivar visualització compacta",show_name:"Mostrar nom",show_name_aria_label_on:"Mostra nom",show_name_aria_label_off:"Amaga nom",show_status:"Mostrar estat",show_status_aria_label_on:"Mostra estat",show_status_aria_label_off:"Amaga estat",show_toolbar:"Mostrar barra d'eines",show_toolbar_aria_label_on:"Mostra barra d'eines",show_toolbar_aria_label_off:"Amaga barra d'eines",code_only_note:"Nota: Configuració de les accions i estadístiques només disponible des de l'Editor de Codi."},eo={status:Qa,source:Ha,common:Ba,error:$a,editor:to},ao={en:Object.freeze({__proto__:null,status:ie,source:se,common:le,error:ce,editor:ue,default:pe}),uk:Object.freeze({__proto__:null,status:de,source:he,common:me,error:fe,editor:ge,default:_e}),nl:Object.freeze({__proto__:null,status:ve,common:be,error:ye,editor:Se,default:we}),de:Object.freeze({__proto__:null,status:ke,common:Ve,error:Ne,editor:ze,default:Ee}),fr:Object.freeze({__proto__:null,status:xe,source:je,common:We,error:Me,editor:Ce,default:Pe}),pl:Object.freeze({__proto__:null,status:qe,common:Ke,error:Ie,editor:Ue,default:Oe}),it:Object.freeze({__proto__:null,status:Ze,common:Te,error:Ae,editor:Re,default:Je}),ru:Object.freeze({__proto__:null,status:Ge,source:Ye,common:Xe,error:De,editor:Le,default:Fe}),es:Object.freeze({__proto__:null,status:Qe,source:He,common:Be,error:$e,editor:ta,default:ea}),cs:Object.freeze({__proto__:null,status:aa,source:oa,common:na,error:ra,editor:ia,default:sa}),hu:Object.freeze({__proto__:null,status:la,source:ca,common:ua,error:pa,editor:da,default:ha}),he:Object.freeze({__proto__:null,status:ma,source:fa,common:ga,error:_a,editor:va,default:ba}),sv:Object.freeze({__proto__:null,status:ya,source:Sa,common:wa,error:ka,editor:Va,default:Na}),nb:Object.freeze({__proto__:null,status:za,source:Ea,common:xa,error:ja,editor:Wa,default:Ma}),da:Object.freeze({__proto__:null,status:Ca,source:Pa,common:qa,error:Ka,editor:Ia,default:Ua}),ko:Object.freeze({__proto__:null,status:Oa,source:Za,common:Ta,error:Aa,editor:Ra,default:Ja}),fi:Object.freeze({__proto__:null,status:Ga,source:Ya,common:Xa,error:Da,editor:La,default:Fa}),ca:Object.freeze({__proto__:null,status:Qa,source:Ha,common:Ba,error:$a,editor:to,default:eo})};function oo(t,e,a){const[o,n]=t.toLowerCase().split(".");let r;try{r=JSON.parse(localStorage.getItem("selectedLanguage"))}catch(t){r=localStorage.getItem("selectedLanguage")}const i=(r||navigator.language.split("-")[0]||"en").replace(/['"]+/g,"").replace("-","_");let s;try{s=ao[i][o][n]}catch(t){s=ao.en[o][n]}if(void 0===s&&(s=ao.en[o][n]),void 0!==s)return""!==e&&""!==a&&(s=s.replace(e,a)),s}customElements.define("vacuum-card-editor",class extends H{static get properties(){return{hass:Object,_config:Object,_toggle:Boolean}}setConfig(t){this._config=t,this._config.entity||(this._config.entity=this.getEntitiesByType("vacuum")[0]||"",vt(this,"config-changed",{config:this._config}))}get _entity(){return this._config&&this._config.entity||""}get _map(){return this._config&&this._config.map||""}get _image(){return this._config&&this._config.image||""}get _show_name(){return this._config?this._config.show_name||!0:""}get _show_status(){return this._config?this._config.show_status||!0:""}get _show_toolbar(){return this._config&&this._config.show_toolbar||!0}get _compact_view(){return this._config&&this._config.compact_view||!1}getEntitiesByType(t){return Object.keys(this.hass.states).filter(e=>e.substr(0,e.indexOf("."))===t)}render(){if(!this.hass)return K``;const t=this.getEntitiesByType("vacuum"),e=this.getEntitiesByType("camera");return K` +
+ + + ${t.map(t=>K` ${t} `)} + + + + + + ${e.map(t=>K` ${t} `)} + + + + + +

+ + + ${oo("editor.compact_view")} +

+ +

+ + + ${oo("editor.show_name")} +

+ +

+ + + ${oo("editor.show_status")} +

+ +

+ + + ${oo("editor.show_toolbar")} +

+ + + ${oo("editor.code_only_note")} + +
+ `}_valueChanged(t){if(!this._config||!this.hass)return;const e=t.target;this["_"+e.configValue]!==e.value&&(e.configValue&&(""===e.value?delete this._config[e.configValue]:this._config={...this._config,[e.configValue]:void 0!==e.checked?e.checked:e.value}),vt(this,"config-changed",{config:this._config}))}static get styles(){return F` + .card-config paper-dropdown-menu { + width: 100%; + } + + .option { + display: flex; + align-items: center; + } + + .option ha-switch { + margin-right: 10px; + } + `}});var no=F` + :host { + display: flex; + flex: 1; + flex-direction: column; + } + + ha-card { + flex-direction: column; + flex: 1; + position: relative; + padding: 0px; + border-radius: 4px; + overflow: hidden; + } + + .preview { + background: var(--primary-color); + cursor: pointer; + overflow: hidden; + position: relative; + text-align: center; + } + + .preview.not-available { + filter: grayscale(1); + } + + .map { + max-width: 95%; + image-rendering: crisp-edges; + } + + @keyframes cleaning { + 0% { + transform: rotate(0) translate(0); + } + 5% { + transform: rotate(0) translate(0, -10px); + } + 10% { + transform: rotate(0) translate(0, 5px); + } + 15% { + transform: rotate(0) translate(0); + } + /* Turn left */ + 20% { + transform: rotate(30deg) translate(0); + } + 25% { + transform: rotate(30deg) translate(0, -10px); + } + 30% { + transform: rotate(30deg) translate(0, 5px); + } + 35% { + transform: rotate(30deg) translate(0); + } + 40% { + transform: rotate(0) translate(0); + } + /* Turn right */ + 45% { + transform: rotate(-30deg) translate(0); + } + 50% { + transform: rotate(-30deg) translate(0, -10px); + } + 55% { + transform: rotate(-30deg) translate(0, 5px); + } + 60% { + transform: rotate(-30deg) translate(0); + } + 70% { + transform: rotate(0deg) translate(0); + } + /* Staying still */ + 100% { + transform: rotate(0deg); + } + } + + @keyframes returning { + 0% { + transform: rotate(0); + } + 25% { + transform: rotate(10deg); + } + 50% { + transform: rotate(0); + } + 75% { + transform: rotate(-10deg); + } + 100% { + transform: rotate(0); + } + } + + .vacuum { + display: block; + max-width: 90%; + max-height: 200px; + image-rendering: crisp-edges; + margin: 30px auto 20px auto; + } + + .vacuum.cleaning, + .vacuum.on { + animation: cleaning 5s linear infinite; + } + + .vacuum.returning { + animation: returning 2s linear infinite; + } + + .vacuum.paused { + opacity: 100%; + } + + .vacuum.docked { + opacity: 50%; + } + + .fill-gap { + flex-grow: 1; + } + + .header { + height: 40px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + color: var(--text-primary-color); + } + + .battery { + text-align: right; + font-weight: bold; + padding: 8px; + } + + .source { + text-align: center; + } + + .status { + display: flex; + align-items: center; + justify-content: center; + } + + .status-text { + color: var(--text-primary-color); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin-left: calc(20px + 9px); /* size + margin of spinner */ + } + + .status ha-circular-progress { + --mdc-theme-primary: var( + --card-background-color + ); /* hack to override the color */ + min-width: 24px; + width: 24px; + height: 24px; + margin-left: 9px; + } + + .vacuum-name { + text-align: center; + font-weight: bold; + color: var(--text-primary-color); + font-size: 16px; + } + + .not-available { + text-align: center; + color: var(--text-primary-color); + font-size: 16px; + } + + .metadata { + margin: 10px auto; + } + + .stats { + border-top: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + flex-direction: row; + justify-content: space-evenly; + color: var(--text-primary-color); + } + + .stats-block { + margin: 10px 0px; + text-align: center; + border-right: 1px solid rgba(255, 255, 255, 0.2); + flex-grow: 1; + } + + .stats-block:last-child { + border: 0px; + } + + .stats-value { + font-size: 20px; + font-weight: bold; + } + + ha-icon { + color: #fff; + } + + .toolbar { + background: var(--lovelace-background, var(--primary-background-color)); + min-height: 30px; + display: flex; + flex-direction: row; + justify-content: space-evenly; + } + + .toolbar ha-icon-button { + color: var(--primary-color); + flex-direction: column; + width: 44px; + height: 44px; + --mdc-icon-button-size: 44px; + margin: 5px 0; + } + + .toolbar ha-icon-button:first-child { + margin-left: 5px; + } + + .toolbar ha-icon-button:last-child { + margin-right: 5px; + } + + .toolbar paper-button { + color: var(--primary-color); + flex-direction: column; + margin-right: 10px; + padding: 15px 10px; + cursor: pointer; + } + + .toolbar ha-icon-button:active, + .toolbar paper-button:active { + opacity: 0.4; + background: rgba(0, 0, 0, 0.1); + } + + .toolbar paper-button { + color: var(--primary-color); + flex-direction: row; + } + + .toolbar ha-icon { + color: var(--primary-color); + padding-right: 15px; + } +`;const ro="data:image/png;base64,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";customElements.get("ha-icon-button")||customElements.define("ha-icon-button",class extends(customElements.get("paper-icon-button")){});customElements.define("vacuum-card",class extends H{static get properties(){return{hass:Object,config:Object,mapUrl:String,requestInProgress:Boolean}}static get styles(){return no}static async getConfigElement(){return document.createElement("vacuum-card-editor")}static getStubConfig(t,e){const[a]=e.filter(t=>"vacuum"===t.substr(0,t.indexOf(".")));return{entity:a||"",image:"default"}}get entity(){return this.hass.states[this.config.entity]}get map(){return this.hass?this.hass.states[this.config.map]:null}get image(){return"default"===this.config.image?ro:this.config.image||ro}get showName(){return void 0===this.config.show_name||this.config.show_name}get showStatus(){return void 0===this.config.show_status||this.config.show_status}get showToolbar(){return void 0===this.config.show_toolbar||this.config.show_toolbar}get compactView(){return void 0!==this.config.compact_view&&this.config.compact_view}setConfig(t){if(!t.entity)throw new Error(oo("error.missing_entity"));this.config=t}getCardSize(){return 2}shouldUpdate(t){return function(t,e,a){if(e.has("config")||a)return!0;if(t.config.entity){var o=e.get("hass");return!o||o.states[t.config.entity]!==t.hass.states[t.config.entity]}return!1}(this,t)}updated(t){t.get("hass")&&t.get("hass").states[this.config.entity].state!==this.hass.states[this.config.entity].state&&(this.requestInProgress=!1)}updateCameraImage(){this.hass.callWS({type:"camera_thumbnail",entity_id:this.config.map}).then(t=>{const{content_type:e,content:a}=t;this.mapUrl=`data:${e};base64, ${a}`,this.requestUpdate()})}connectedCallback(){super.connectedCallback(),!this.compactView&&this.map&&(this.updateCameraImage(),this.thumbUpdater=setInterval(()=>this.updateCameraImage(),1e3*(this.config.map_refresh||5)))}disconnectedCallback(){super.disconnectedCallback(),this.map&&(clearInterval(this.thumbUpdater),this.map_image=null)}handleMore(){vt(this,"hass-more-info",{entityId:this.entity.entity_id},{bubbles:!0,composed:!0})}handleSpeed(t){const e=t.target.getAttribute("value");this.callService("set_fan_speed",!1,{fan_speed:e})}callService(t,e=!0,a={}){this.hass.callService("vacuum",t,{entity_id:this.config.entity,...a}),e&&(this.requestInProgress=!0,this.requestUpdate())}getAttributes(t){const{status:e,state:a,fan_speed:o,fan_speed_list:n,battery_level:r,battery_icon:i,friendly_name:s}=t.attributes;return{status:e||a||t.state,fan_speed:o,fan_speed_list:n,battery_level:r,battery_icon:i,friendly_name:s}}renderSource(){const{fan_speed:t,fan_speed_list:e}=this.getAttributes(this.entity);if(!e)return K``;const a=e.indexOf(t);return K` + + + + + ${oo("source."+t)||t} + + + + ${e.map(t=>K`${oo("source."+t)||t}`)} + + + `}renderMapOrImage(t){return this.compactView?K``:this.map?K` `:this.image?K` `:K``}renderStats(t){const{stats:e={}}=this.config;return(e[t]||e.default||[]).map(({entity_id:t,attribute:e,unit:a,subtitle:o})=>{if(!t&&!e)return K``;const n=t?this.hass.states[t].state:re(this.entity.attributes,e);return K` +
+ ${n} + ${a} +
${o}
+
+ `})}renderName(){const{friendly_name:t}=this.getAttributes(this.entity);return this.showName?K` +
+ ${t} +
+ `:K``}renderStatus(){const{status:t}=this.getAttributes(this.entity),e=oo("status."+t)||t;return this.showStatus?K` +
+ + ${e} + + +
+ `:K``}renderToolbar(t){if(!this.showToolbar)return K``;switch(t){case"on":case"cleaning":return K` +
+ + + ${oo("common.pause")} + + + + ${oo("common.stop")} + + + + ${oo("common.return_to_base")} + +
+ `;case"paused":return K` +
+ + + ${oo("common.continue")} + + + + ${oo("common.return_to_base")} + +
+ `;case"returning":return K` +
+ + + ${oo("common.continue")} + + + + ${oo("common.pause")} + +
+ `;case"docked":case"idle":default:{const{actions:e=[]}=this.config,a=e.map(({name:t,service:e,icon:a,service_data:o})=>K``),o=K` + + + `;return K` +
+ + + + + + + ${"idle"===t?o:""} +
+ ${a} +
+ `}}}render(){if(!this.entity)return K` + +
+