From 319d4eeb3e4ba0baedf9972a8746b54c5019bac2 Mon Sep 17 00:00:00 2001 From: g4st3r Date: Sat, 7 Mar 2026 17:28:59 +0000 Subject: [PATCH] Initialize docker stack repo --- .gitignore | 42 + README.md | 17 + _legacy/docker-compose-no-envs.yml | 241 ++ bitwarden/docker-compose.yml | 17 + gitlab/docker-compose.yml | 19 + homeassistant/config/.HA_VERSION | 1 + homeassistant/config/.ha_run.lock | 1 + homeassistant/config/automations.yaml | 395 ++ .../homeassistant/motion_light.yaml | 58 + .../homeassistant/notify_leaving_zone.yaml | 50 + .../confirmable_notification.yaml | 86 + .../homeassistant/inverted_binary_sensor.yaml | 27 + homeassistant/config/configuration.yaml | 63 + .../configuration.yaml.bak.2026-01-31_233007 | 38 + .../configuration.yaml.bak.2026-02-01_022515 | 37 + ....yaml.bak_before_mqttfix.2026-02-01_022622 | 41 + .../config/custom_components/hacs/__init__.py | 229 ++ .../config/custom_components/hacs/base.py | 1110 ++++++ .../custom_components/hacs/config_flow.py | 225 ++ .../config/custom_components/hacs/const.py | 294 ++ .../custom_components/hacs/coordinator.py | 38 + .../custom_components/hacs/data_client.py | 98 + .../custom_components/hacs/diagnostics.py | 80 + .../config/custom_components/hacs/entity.py | 143 + .../config/custom_components/hacs/enums.py | 71 + .../custom_components/hacs/exceptions.py | 49 + .../config/custom_components/hacs/frontend.py | 67 + .../config/custom_components/hacs/icons.json | 12 + .../config/custom_components/hacs/iconset.js | 21 + .../custom_components/hacs/manifest.json | 26 + .../config/custom_components/hacs/repairs.py | 58 + .../hacs/repositories/__init__.py | 21 + .../hacs/repositories/appdaemon.py | 93 + .../hacs/repositories/base.py | 1454 +++++++ .../hacs/repositories/integration.py | 217 + .../hacs/repositories/plugin.py | 246 ++ .../hacs/repositories/python_script.py | 111 + .../hacs/repositories/template.py | 106 + .../hacs/repositories/theme.py | 119 + .../config/custom_components/hacs/switch.py | 73 + .../custom_components/hacs/system_health.py | 52 + .../hacs/translations/en.json | 84 + .../config/custom_components/hacs/types.py | 10 + .../config/custom_components/hacs/update.py | 158 + .../custom_components/hacs/utils/__init__.py | 1 + .../custom_components/hacs/utils/backup.py | 110 + .../hacs/utils/configuration_schema.py | 9 + .../custom_components/hacs/utils/data.py | 323 ++ .../custom_components/hacs/utils/decode.py | 8 + .../custom_components/hacs/utils/decorator.py | 43 + .../hacs/utils/file_system.py | 42 + .../custom_components/hacs/utils/filters.py | 47 + .../hacs/utils/github_graphql_query.py | 19 + .../custom_components/hacs/utils/json.py | 5 + .../custom_components/hacs/utils/logger.py | 7 + .../custom_components/hacs/utils/path.py | 41 + .../hacs/utils/queue_manager.py | 82 + .../custom_components/hacs/utils/regex.py | 17 + .../custom_components/hacs/utils/store.py | 79 + .../custom_components/hacs/utils/url.py | 30 + .../custom_components/hacs/utils/validate.py | 215 + .../custom_components/hacs/utils/version.py | 36 + .../hacs/utils/workarounds.py | 37 + .../custom_components/hacs/validate/README.md | 30 + .../hacs/validate/__init__.py | 1 + .../hacs/validate/archived.py | 25 + .../custom_components/hacs/validate/base.py | 54 + .../custom_components/hacs/validate/brands.py | 35 + .../hacs/validate/description.py | 25 + .../hacs/validate/hacsjson.py | 35 + .../custom_components/hacs/validate/images.py | 33 + .../hacs/validate/information.py | 33 + .../hacs/validate/integration_manifest.py | 39 + .../custom_components/hacs/validate/issues.py | 25 + .../hacs/validate/manager.py | 81 + .../custom_components/hacs/validate/topics.py | 25 + .../hacs/websocket/__init__.py | 123 + .../hacs/websocket/critical.py | 59 + .../hacs/websocket/repositories.py | 216 + .../hacs/websocket/repository.py | 369 ++ .../custom_components/localtuya/__init__.py | 378 ++ .../localtuya/binary_sensor.py | 76 + .../custom_components/localtuya/climate.py | 522 +++ .../custom_components/localtuya/cloud_api.py | 139 + .../custom_components/localtuya/common.py | 607 +++ .../localtuya/config_flow.py | 819 ++++ .../custom_components/localtuya/const.py | 143 + .../custom_components/localtuya/cover.py | 233 ++ .../localtuya/diagnostics.py | 65 + .../custom_components/localtuya/discovery.py | 90 + .../config/custom_components/localtuya/fan.py | 259 ++ .../custom_components/localtuya/light.py | 506 +++ .../custom_components/localtuya/manifest.json | 14 + .../custom_components/localtuya/number.py | 113 + .../localtuya/pytuya/__init__.py | 1196 ++++++ .../custom_components/localtuya/select.py | 123 + .../custom_components/localtuya/sensor.py | 75 + .../custom_components/localtuya/services.yaml | 15 + .../custom_components/localtuya/strings.json | 139 + .../custom_components/localtuya/switch.py | 91 + .../localtuya/translations/en.json | 238 ++ .../localtuya/translations/it.json | 216 + .../localtuya/translations/pt-BR.json | 216 + .../custom_components/localtuya/vacuum.py | 241 ++ .../midea_dehumidifier_lan/__init__.py | 285 ++ .../appliance_coordinator.py | 294 ++ .../appliance_discovery.py | 402 ++ .../midea_dehumidifier_lan/binary_sensor.py | 118 + .../midea_dehumidifier_lan/climate.py | 254 ++ .../midea_dehumidifier_lan/config_flow.py | 727 ++++ .../midea_dehumidifier_lan/const.py | 111 + .../midea_dehumidifier_lan/fan.py | 143 + .../midea_dehumidifier_lan/hub.py | 303 ++ .../midea_dehumidifier_lan/humidifier.py | 125 + .../midea_dehumidifier_lan/manifest.json | 19 + .../midea_dehumidifier_lan/sensor.py | 101 + .../midea_dehumidifier_lan/switch.py | 182 + .../translations/en.json | 96 + .../translations/it.json | 96 + .../translations/pt.json | 96 + .../translations/sk.json | 96 + .../midea_dehumidifier_lan/util.py | 244 ++ .../custom_components/polaris/__init__.py | 42 + .../polaris/binary_sensor.py | 324 ++ .../custom_components/polaris/button.py | 364 ++ .../custom_components/polaris/climate.py | 493 +++ .../custom_components/polaris/common.py | 35 + .../custom_components/polaris/config_flow.py | 194 + .../config/custom_components/polaris/const.py | 3524 +++++++++++++++++ .../custom_components/polaris/humidifier.py | 224 ++ .../custom_components/polaris/icons.json | 274 ++ .../config/custom_components/polaris/image.py | 310 ++ .../polaris/input_datetime.py | 114 + .../config/custom_components/polaris/light.py | 260 ++ .../custom_components/polaris/manifest.json | 19 + .../custom_components/polaris/number.py | 291 ++ .../custom_components/polaris/select.py | 482 +++ .../custom_components/polaris/sensor.py | 529 +++ .../custom_components/polaris/services.yaml | 15 + .../custom_components/polaris/switch.py | 752 ++++ .../config/custom_components/polaris/time.py | 114 + .../polaris/translations/en.json | 669 ++++ .../polaris/translations/ru.json | 669 ++++ .../custom_components/polaris/vacuum.py | 523 +++ .../custom_components/polaris/water_heater.py | 244 ++ .../prometheus_sensor/__init__.py | 60 + .../prometheus_sensor/binary_sensor.py | 125 + .../prometheus_sensor/const.py | 10 + .../prometheus_sensor/manifest.json | 9 + .../prometheus_sensor/sensor.py | 112 + .../ui_lovelace_minimalist/.gitignore | 6 + .../ui_lovelace_minimalist/__init__.py | 215 + .../ui_lovelace_minimalist/base.py | 611 +++ .../blueprints/set_theme.yaml | 40 + .../cards/button-card/button-card.js | 56 + .../light-entity-card/light-entity-card.js | 481 +++ .../lovelace-auto-entities.js | 1 + .../lovelace-card-mod/lovelace-card-mod.js | 1 + .../lovelace-layout-card.js | 1 + .../lovelace-state-switch.js | 139 + .../cards/mini-graph-card/mini-graph-card.js | 1 + .../mini-media-player/mini-media-player.js | 1649 ++++++++ .../cards/my-cards/my-cards.js | 495 +++ .../simple-weather-card.js | 52 + .../weather-radar-card/weather-radar-card.js | 1334 +++++++ .../ui_lovelace_minimalist/config_flow.py | 324 ++ .../ui_lovelace_minimalist/const.py | 128 + .../ui_lovelace_minimalist/enums.py | 19 + .../lovelace/adaptive-dash/adaptive-ui.yaml | 40 + .../lovelace/adaptive-dash/popup/popup.yaml | 149 + .../adaptive-dash/views/livingroom.yaml | 89 + .../lovelace/adaptive-dash/views/main.yaml | 105 + .../lovelace/custom_actions.yaml | 12 + .../minimalist-desktop.yaml | 133 + .../minimalist-ios-tapbar.yaml | 200 + .../minimalist-mobile-tapbar.yaml | 196 + .../minimalist-mobile/minimalist-mobile.yaml | 140 + .../lovelace/translations/cn.yaml | 14 + .../lovelace/translations/cs.yaml | 14 + .../lovelace/translations/da.yaml | 17 + .../lovelace/translations/de.yaml | 14 + .../lovelace/translations/default.yaml | 258 ++ .../lovelace/translations/en.yaml | 17 + .../lovelace/translations/es.yaml | 17 + .../lovelace/translations/fi.yaml | 14 + .../lovelace/translations/fr.yaml | 14 + .../lovelace/translations/he.yaml | 17 + .../lovelace/translations/it.yaml | 14 + .../lovelace/translations/ko-KR.yaml | 17 + .../lovelace/translations/nl.yaml | 17 + .../lovelace/translations/no.yaml | 14 + .../lovelace/translations/pl.yaml | 14 + .../lovelace/translations/pt-BR.yaml | 14 + .../lovelace/translations/pt.yaml | 14 + .../lovelace/translations/ru.yaml | 14 + .../lovelace/translations/sk.yaml | 14 + .../lovelace/translations/sv.yaml | 14 + .../lovelace/translations/tr.yaml | 14 + .../lovelace/translations/uk.yaml | 14 + .../lovelace/translations/zh-CN.yaml | 17 + .../lovelace/ui-lovelace.yaml | 118 + .../ulm_templates/actions/actions_card.yaml | 342 ++ .../actions/actions_card_overlay.yaml | 46 + .../ulm_templates/actions/actions_icon.yaml | 345 ++ .../ulm_templates/actions/actions_name.yaml | 342 ++ .../2-line_cards/card_graph.yaml | 91 + .../card_templates/cards/card_battery.yaml | 166 + .../cards/card_binary_sensor.yaml | 112 + .../cards/card_binary_sensor_alert.yaml | 113 + .../card_templates/cards/card_cover.yaml | 808 ++++ .../card_templates/cards/card_fan.yaml | 318 ++ .../card_templates/cards/card_generic.yaml | 110 + .../cards/card_generic_swap.yaml | 110 + .../cards/card_input_boolean.yaml | 109 + .../card_templates/cards/card_light.yaml | 446 +++ .../cards/card_media_player.yaml | 912 +++++ .../card_templates/cards/card_navigate.yaml | 25 + .../card_templates/cards/card_person.yaml | 101 + .../cards/card_power_outlet.yaml | 115 + .../card_templates/cards/card_room.yaml | 239 ++ .../cards/card_scenes_welcome.yaml | 219 + .../card_templates/cards/card_script.yaml | 18 + .../card_templates/cards/card_thermostat.yaml | 788 ++++ .../card_templates/cards/card_vacuum.yaml | 378 ++ .../cards/card_vertical_button.yaml | 100 + .../card_templates/cards/card_weather.yaml | 49 + .../cards/card_weather_ulm.yaml | 205 + .../cards/card_welcome_scenes.yaml | 548 +++ .../card_templates/chips/chip_alarm.yaml | 37 + .../card_templates/chips/chip_back.yaml | 17 + .../chips/chip_icon_double_state.yaml | 26 + .../card_templates/chips/chip_icon_label.yaml | 23 + .../card_templates/chips/chip_icon_only.yaml | 12 + .../card_templates/chips/chip_icon_state.yaml | 18 + .../chips/chip_mdi_icon_only.yaml | 14 + .../chips/chip_mdi_icon_state.yaml | 26 + .../card_templates/chips/chip_navigate.yaml | 16 + .../chips/chip_power_consumption.yaml | 18 + .../chips/chip_presence_detection.yaml | 15 + .../chips/chip_short_date_with_day.yaml | 20 + .../chips/chip_temperature.yaml | 49 + .../chips/chip_weather_date.yaml | 39 + .../card_templates/colors/blue_no_card.yaml | 17 + .../card_templates/colors/blue_no_state.yaml | 12 + .../card_templates/colors/blue_off.yaml | 14 + .../card_templates/colors/blue_on.yaml | 20 + .../card_templates/colors/green_no_state.yaml | 11 + .../card_templates/colors/green_off.yaml | 14 + .../card_templates/colors/green_on.yaml | 20 + .../card_templates/colors/grey_no_state.yaml | 11 + .../card_templates/colors/grey_off.yaml | 14 + .../card_templates/colors/grey_on.yaml | 14 + .../card_templates/colors/pink_no_state.yaml | 11 + .../card_templates/colors/pink_off.yaml | 14 + .../card_templates/colors/pink_on.yaml | 14 + .../colors/purple_no_state.yaml | 11 + .../card_templates/colors/purple_off.yaml | 14 + .../card_templates/colors/purple_on.yaml | 14 + .../card_templates/colors/red_no_state.yaml | 11 + .../card_templates/colors/red_off.yaml | 14 + .../card_templates/colors/red_on.yaml | 14 + .../card_templates/colors/yellow_no_card.yaml | 17 + .../colors/yellow_no_state.yaml | 11 + .../card_templates/colors/yellow_off.yaml | 17 + .../card_templates/colors/yellow_on.yaml | 20 + .../card_templates/colors/yellow_slider.yaml | 16 + .../internal_templates/chips.yaml | 35 + .../internal_templates/cover.yaml | 46 + .../internal_templates/edge.yaml | 6 + .../internal_templates/extended_card.yaml | 88 + .../internal_templates/icon.yaml | 50 + .../internal_templates/icon_alert.yaml | 74 + .../internal_templates/icon_info.yaml | 71 + .../internal_templates/icon_info_alert.yaml | 101 + .../internal_templates/icon_info_bg.yaml | 70 + .../internal_templates/icon_info_line.yaml | 29 + .../internal_templates/icon_more_info.yaml | 41 + .../icon_more_info_alert.yaml | 100 + .../icon_more_info_new.yaml | 110 + .../internal_templates/icon_only.yaml | 32 + .../internal_templates/list_2_items.yaml | 12 + .../internal_templates/list_3_items.yaml | 12 + .../internal_templates/list_4_items.yaml | 12 + .../internal_templates/list_items_line.yaml | 12 + .../list_one_third_items.yaml | 12 + .../list_two_third_items.yaml | 12 + .../internal_templates/widget_icon.yaml | 21 + .../card_media_player_art.yaml | 7 + .../card_media_player_controls.yaml | 7 + .../legacy_templates/cards.yaml | 144 + .../legacy_templates/chips.yaml | 15 + .../legacy_templates/list_items.yaml | 4 + .../legacy_templates/title.yaml | 4 + .../card_templates/title/card_title.yaml | 31 + .../vertical_buttons/vertical_buttons.yaml | 32 + .../vertical_buttons_custom_state.yaml | 34 + .../popup_buttons/popup_button.yaml | 32 + .../popup_button_airconditionner.yaml | 24 + .../popup_buttons/popup_button_app.yaml | 118 + .../popup_buttons/popup_button_back.yaml | 20 + .../popup_button_brightness.yaml | 4 + .../popup_buttons/popup_button_color.yaml | 4 + .../popup_button_color_temp.yaml | 4 + .../popup_button_cover_close.yaml | 4 + .../popup_button_cover_open.yaml | 4 + .../popup_button_cover_stop.yaml | 4 + .../popup_buttons/popup_button_forecast.yaml | 4 + .../popup_buttons/popup_button_history.yaml | 4 + .../popup_button_light_more_options.yaml | 49 + .../popup_buttons/popup_button_playing.yaml | 4 + .../popup_buttons/popup_button_power.yaml | 18 + .../popup_buttons/popup_button_radar.yaml | 4 + .../popup_buttons/popup_button_selected.yaml | 9 + .../popup_buttons/popup_button_source.yaml | 42 + .../popup_buttons/popup_button_stats.yaml | 4 + .../popup_buttons/popup_button_volume.yaml | 17 + .../popup_buttons/popup_card_volume.yaml | 15 + .../popup_buttons/popup_chip_controls.yaml | 88 + .../popup_buttons/popup_chip_volume.yaml | 23 + .../popup_buttons/popup_header.yaml | 22 + .../popup_buttons/popup_header_cover.yaml | 69 + .../popup_buttons/popup_header_light.yaml | 54 + .../popup_defaults/popup_default.yaml | 4 + .../popup_items/popup_item4_back_toggle.yaml | 13 + .../popup_layouts/popup_4_items.yaml | 20 + .../popup_layouts/popup_light_effect_row.yaml | 25 + .../popup_light_palette_row.yaml | 24 + .../popup_layouts/popup_list_items.yaml | 11 + .../popup_layouts/popup_media_player_row.yaml | 24 + .../popup_layouts/popup_row_layout.yaml | 25 + .../popup_layouts/popup_subtitle.yaml | 9 + .../popup_layouts/popup_weather_row.yaml | 111 + .../popup_templates/popups/popup_cover.yaml | 185 + .../popup_templates/popups/popup_light.yaml | 13 + .../popups/popup_light_brightness.yaml | 485 +++ .../popups/popup_light_color.yaml | 121 + .../popups/popup_light_color_temp.yaml | 122 + .../popups/popup_light_effect.yaml | 247 ++ .../popups/popup_light_palette.yaml | 124 + .../popups/popup_media_player.yaml | 13 + .../popups/popup_media_player_infos.yaml | 332 ++ .../popups/popup_media_player_source.yaml | 105 + .../popup_media_player_source_card.yaml | 18 + .../popups/popup_media_player_volume.yaml | 61 + .../popups/popup_power_outlet.yaml | 17 + .../popups/popup_power_outlet_history.yaml | 82 + .../popups/popup_power_outlet_stats.yaml | 192 + .../popups/popup_thermostat.yaml | 13 + .../popups/popup_thermostat_temperature.yaml | 618 +++ .../popup_templates/popups/popup_vacuum.yaml | 12 + .../popups/popup_vacuum_map.yaml | 68 + .../popup_templates/popups/popup_weather.yaml | 12 + .../popups/popup_weather_forecast.yaml | 238 ++ .../popups/popup_weather_radar.yaml | 77 + .../ui_lovelace_minimalist/manifest.json | 22 + .../ui_lovelace_minimalist/services.yaml | 6 + .../ui_lovelace_minimalist/strings.json | 70 + .../translations/ca.json | 70 + .../translations/cs.json | 70 + .../translations/da.json | 70 + .../translations/de.json | 71 + .../translations/en.json | 70 + .../translations/es.json | 70 + .../translations/fi.json | 70 + .../translations/fr.json | 70 + .../translations/he.json | 70 + .../translations/it.json | 70 + .../translations/ko-KR.json | 70 + .../translations/nl.json | 70 + .../translations/pl.json | 70 + .../translations/pt-BR.json | 70 + .../translations/ru.json | 70 + .../translations/sk.json | 70 + .../translations/sl.json | 70 + .../translations/sv.json | 70 + .../translations/uk.json | 70 + .../translations/zh-CN.json | 70 + .../ui_lovelace_minimalist/utils/decode.py | 8 + .../ui_lovelace_minimalist/utils/json.py | 10 + .../ui_lovelace_minimalist/utils/logger.py | 7 + .../yandex_pogoda/__init__.py | 77 + .../yandex_pogoda/config_flow.py | 153 + .../custom_components/yandex_pogoda/const.py | 261 ++ .../yandex_pogoda/device_trigger.py | 74 + .../yandex_pogoda/manifest.json | 11 + .../custom_components/yandex_pogoda/sensor.py | 250 ++ .../yandex_pogoda/translations/en.json | 93 + .../yandex_pogoda/translations/ru.json | 112 + .../yandex_pogoda/updater.py | 457 +++ .../yandex_pogoda/weather.py | 258 ++ .../yandex_smart_home/__init__.py | 263 ++ .../yandex_smart_home/backports.py | 26 + .../yandex_smart_home/capability.py | 233 ++ .../yandex_smart_home/capability_color.py | 446 +++ .../yandex_smart_home/capability_custom.py | 447 +++ .../yandex_smart_home/capability_mode.py | 971 +++++ .../yandex_smart_home/capability_onoff.py | 569 +++ .../yandex_smart_home/capability_range.py | 742 ++++ .../yandex_smart_home/capability_toggle.py | 265 ++ .../yandex_smart_home/capability_video.py | 107 + .../yandex_smart_home/cloud.py | 220 + .../yandex_smart_home/cloud_stream.py | 196 + .../yandex_smart_home/color.py | 315 ++ .../yandex_smart_home/config_flow.py | 761 ++++ .../yandex_smart_home/config_schema.py | 468 +++ .../yandex_smart_home/const.py | 153 + .../yandex_smart_home/device.py | 520 +++ .../yandex_smart_home/diagnostics.py | 35 + .../yandex_smart_home/entry_data.py | 494 +++ .../yandex_smart_home/handlers.py | 170 + .../yandex_smart_home/helpers.py | 172 + .../yandex_smart_home/http.py | 161 + .../yandex_smart_home/manifest.json | 14 + .../yandex_smart_home/notifier.py | 493 +++ .../yandex_smart_home/property.py | 140 + .../yandex_smart_home/property_custom.py | 489 +++ .../yandex_smart_home/property_event.py | 557 +++ .../yandex_smart_home/property_float.py | 914 +++++ .../yandex_smart_home/repairs.py | 123 + .../yandex_smart_home/schema/__init__.py | 16 + .../yandex_smart_home/schema/base.py | 22 + .../yandex_smart_home/schema/callback.py | 57 + .../yandex_smart_home/schema/capability.py | 160 + .../schema/capability_color.py | 111 + .../schema/capability_mode.py | 133 + .../schema/capability_onoff.py | 27 + .../schema/capability_range.py | 99 + .../schema/capability_toggle.py | 36 + .../schema/capability_video.py | 43 + .../yandex_smart_home/schema/device.py | 197 + .../yandex_smart_home/schema/property.py | 65 + .../schema/property_event.py | 192 + .../schema/property_float.py | 194 + .../yandex_smart_home/schema/response.py | 52 + .../yandex_smart_home/services.yaml | 2 + .../yandex_smart_home/translations/en.json | 350 ++ .../yandex_smart_home/unit_conversion.py | 128 + .../config/docker_containers_summary.py | 14 + homeassistant/config/scenes.yaml | 0 homeassistant/config/scripts.yaml | 0 .../config/themes/amoled/amoled.yaml | 26 + .../minimalist-desktop.yaml | 133 + .../minimalist-ios-tapbar.yaml | 200 + .../minimalist-mobile-tapbar.yaml | 196 + .../minimalist-mobile/minimalist-mobile.yaml | 140 + .../custom_actions/custom_actions.yaml | 12 + .../dashboard/adaptive-dash/adaptive-ui.yaml | 40 + .../dashboard/adaptive-dash/popup/popup.yaml | 149 + .../adaptive-dash/views/livingroom.yaml | 89 + .../dashboard/adaptive-dash/views/main.yaml | 105 + .../dashboard/ui-lovelace.yaml | 118 + homeassistant/docker-compose.yml | 36 + homeassistant/mosquitto/config/mosquitto.conf | 5 + immich/docker-compose.yml | 56 + immich/immich/db-init/01-extensions.sql | 4 + minio/docker-compose.yml | 41 + minio/policies/knowledge-base-policy.json | 25 + voice/docker-compose.yml | 23 + 458 files changed, 67215 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 _legacy/docker-compose-no-envs.yml create mode 100644 bitwarden/docker-compose.yml create mode 100644 gitlab/docker-compose.yml create mode 100644 homeassistant/config/.HA_VERSION create mode 100644 homeassistant/config/.ha_run.lock create mode 100644 homeassistant/config/automations.yaml create mode 100644 homeassistant/config/blueprints/automation/homeassistant/motion_light.yaml create mode 100644 homeassistant/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml create mode 100644 homeassistant/config/blueprints/script/homeassistant/confirmable_notification.yaml create mode 100644 homeassistant/config/blueprints/template/homeassistant/inverted_binary_sensor.yaml create mode 100644 homeassistant/config/configuration.yaml create mode 100644 homeassistant/config/configuration.yaml.bak.2026-01-31_233007 create mode 100644 homeassistant/config/configuration.yaml.bak.2026-02-01_022515 create mode 100644 homeassistant/config/configuration.yaml.bak_before_mqttfix.2026-02-01_022622 create mode 100644 homeassistant/config/custom_components/hacs/__init__.py create mode 100644 homeassistant/config/custom_components/hacs/base.py create mode 100644 homeassistant/config/custom_components/hacs/config_flow.py create mode 100644 homeassistant/config/custom_components/hacs/const.py create mode 100644 homeassistant/config/custom_components/hacs/coordinator.py create mode 100644 homeassistant/config/custom_components/hacs/data_client.py create mode 100644 homeassistant/config/custom_components/hacs/diagnostics.py create mode 100644 homeassistant/config/custom_components/hacs/entity.py create mode 100644 homeassistant/config/custom_components/hacs/enums.py create mode 100644 homeassistant/config/custom_components/hacs/exceptions.py create mode 100644 homeassistant/config/custom_components/hacs/frontend.py create mode 100644 homeassistant/config/custom_components/hacs/icons.json create mode 100644 homeassistant/config/custom_components/hacs/iconset.js create mode 100644 homeassistant/config/custom_components/hacs/manifest.json create mode 100644 homeassistant/config/custom_components/hacs/repairs.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/__init__.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/appdaemon.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/base.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/integration.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/plugin.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/python_script.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/template.py create mode 100644 homeassistant/config/custom_components/hacs/repositories/theme.py create mode 100644 homeassistant/config/custom_components/hacs/switch.py create mode 100644 homeassistant/config/custom_components/hacs/system_health.py create mode 100644 homeassistant/config/custom_components/hacs/translations/en.json create mode 100644 homeassistant/config/custom_components/hacs/types.py create mode 100644 homeassistant/config/custom_components/hacs/update.py create mode 100644 homeassistant/config/custom_components/hacs/utils/__init__.py create mode 100644 homeassistant/config/custom_components/hacs/utils/backup.py create mode 100644 homeassistant/config/custom_components/hacs/utils/configuration_schema.py create mode 100644 homeassistant/config/custom_components/hacs/utils/data.py create mode 100644 homeassistant/config/custom_components/hacs/utils/decode.py create mode 100644 homeassistant/config/custom_components/hacs/utils/decorator.py create mode 100644 homeassistant/config/custom_components/hacs/utils/file_system.py create mode 100644 homeassistant/config/custom_components/hacs/utils/filters.py create mode 100644 homeassistant/config/custom_components/hacs/utils/github_graphql_query.py create mode 100644 homeassistant/config/custom_components/hacs/utils/json.py create mode 100644 homeassistant/config/custom_components/hacs/utils/logger.py create mode 100644 homeassistant/config/custom_components/hacs/utils/path.py create mode 100644 homeassistant/config/custom_components/hacs/utils/queue_manager.py create mode 100644 homeassistant/config/custom_components/hacs/utils/regex.py create mode 100644 homeassistant/config/custom_components/hacs/utils/store.py create mode 100644 homeassistant/config/custom_components/hacs/utils/url.py create mode 100644 homeassistant/config/custom_components/hacs/utils/validate.py create mode 100644 homeassistant/config/custom_components/hacs/utils/version.py create mode 100644 homeassistant/config/custom_components/hacs/utils/workarounds.py create mode 100644 homeassistant/config/custom_components/hacs/validate/README.md create mode 100644 homeassistant/config/custom_components/hacs/validate/__init__.py create mode 100644 homeassistant/config/custom_components/hacs/validate/archived.py create mode 100644 homeassistant/config/custom_components/hacs/validate/base.py create mode 100644 homeassistant/config/custom_components/hacs/validate/brands.py create mode 100644 homeassistant/config/custom_components/hacs/validate/description.py create mode 100644 homeassistant/config/custom_components/hacs/validate/hacsjson.py create mode 100644 homeassistant/config/custom_components/hacs/validate/images.py create mode 100644 homeassistant/config/custom_components/hacs/validate/information.py create mode 100644 homeassistant/config/custom_components/hacs/validate/integration_manifest.py create mode 100644 homeassistant/config/custom_components/hacs/validate/issues.py create mode 100644 homeassistant/config/custom_components/hacs/validate/manager.py create mode 100644 homeassistant/config/custom_components/hacs/validate/topics.py create mode 100644 homeassistant/config/custom_components/hacs/websocket/__init__.py create mode 100644 homeassistant/config/custom_components/hacs/websocket/critical.py create mode 100644 homeassistant/config/custom_components/hacs/websocket/repositories.py create mode 100644 homeassistant/config/custom_components/hacs/websocket/repository.py create mode 100644 homeassistant/config/custom_components/localtuya/__init__.py create mode 100644 homeassistant/config/custom_components/localtuya/binary_sensor.py create mode 100644 homeassistant/config/custom_components/localtuya/climate.py create mode 100644 homeassistant/config/custom_components/localtuya/cloud_api.py create mode 100644 homeassistant/config/custom_components/localtuya/common.py create mode 100644 homeassistant/config/custom_components/localtuya/config_flow.py create mode 100644 homeassistant/config/custom_components/localtuya/const.py create mode 100644 homeassistant/config/custom_components/localtuya/cover.py create mode 100644 homeassistant/config/custom_components/localtuya/diagnostics.py create mode 100644 homeassistant/config/custom_components/localtuya/discovery.py create mode 100644 homeassistant/config/custom_components/localtuya/fan.py create mode 100644 homeassistant/config/custom_components/localtuya/light.py create mode 100644 homeassistant/config/custom_components/localtuya/manifest.json create mode 100644 homeassistant/config/custom_components/localtuya/number.py create mode 100644 homeassistant/config/custom_components/localtuya/pytuya/__init__.py create mode 100644 homeassistant/config/custom_components/localtuya/select.py create mode 100644 homeassistant/config/custom_components/localtuya/sensor.py create mode 100644 homeassistant/config/custom_components/localtuya/services.yaml create mode 100644 homeassistant/config/custom_components/localtuya/strings.json create mode 100644 homeassistant/config/custom_components/localtuya/switch.py create mode 100644 homeassistant/config/custom_components/localtuya/translations/en.json create mode 100644 homeassistant/config/custom_components/localtuya/translations/it.json create mode 100644 homeassistant/config/custom_components/localtuya/translations/pt-BR.json create mode 100644 homeassistant/config/custom_components/localtuya/vacuum.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/__init__.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_coordinator.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_discovery.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/binary_sensor.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/climate.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/config_flow.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/const.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/fan.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/hub.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/humidifier.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/manifest.json create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/sensor.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/switch.py create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/translations/en.json create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/translations/it.json create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/translations/pt.json create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/translations/sk.json create mode 100644 homeassistant/config/custom_components/midea_dehumidifier_lan/util.py create mode 100644 homeassistant/config/custom_components/polaris/__init__.py create mode 100644 homeassistant/config/custom_components/polaris/binary_sensor.py create mode 100644 homeassistant/config/custom_components/polaris/button.py create mode 100644 homeassistant/config/custom_components/polaris/climate.py create mode 100644 homeassistant/config/custom_components/polaris/common.py create mode 100644 homeassistant/config/custom_components/polaris/config_flow.py create mode 100644 homeassistant/config/custom_components/polaris/const.py create mode 100644 homeassistant/config/custom_components/polaris/humidifier.py create mode 100644 homeassistant/config/custom_components/polaris/icons.json create mode 100644 homeassistant/config/custom_components/polaris/image.py create mode 100644 homeassistant/config/custom_components/polaris/input_datetime.py create mode 100644 homeassistant/config/custom_components/polaris/light.py create mode 100644 homeassistant/config/custom_components/polaris/manifest.json create mode 100644 homeassistant/config/custom_components/polaris/number.py create mode 100644 homeassistant/config/custom_components/polaris/select.py create mode 100644 homeassistant/config/custom_components/polaris/sensor.py create mode 100644 homeassistant/config/custom_components/polaris/services.yaml create mode 100644 homeassistant/config/custom_components/polaris/switch.py create mode 100644 homeassistant/config/custom_components/polaris/time.py create mode 100644 homeassistant/config/custom_components/polaris/translations/en.json create mode 100644 homeassistant/config/custom_components/polaris/translations/ru.json create mode 100644 homeassistant/config/custom_components/polaris/vacuum.py create mode 100644 homeassistant/config/custom_components/polaris/water_heater.py create mode 100644 homeassistant/config/custom_components/prometheus_sensor/__init__.py create mode 100644 homeassistant/config/custom_components/prometheus_sensor/binary_sensor.py create mode 100644 homeassistant/config/custom_components/prometheus_sensor/const.py create mode 100644 homeassistant/config/custom_components/prometheus_sensor/manifest.json create mode 100644 homeassistant/config/custom_components/prometheus_sensor/sensor.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/.gitignore create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/__init__.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/base.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/blueprints/set_theme.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/button-card/button-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/light-entity-card/light-entity-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-auto-entities/lovelace-auto-entities.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-card-mod/lovelace-card-mod.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-layout-card/lovelace-layout-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-state-switch/lovelace-state-switch.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-graph-card/mini-graph-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-media-player/mini-media-player.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/my-cards/my-cards.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/simple-weather-card/simple-weather-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/cards/weather-radar-card/weather-radar-card.js create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/config_flow.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/const.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/enums.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/adaptive-dash/adaptive-ui.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/adaptive-dash/popup/popup.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/adaptive-dash/views/livingroom.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/adaptive-dash/views/main.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/custom_actions.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/themefiles/minimalist-desktop/minimalist-desktop.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/themefiles/minimalist-ios-tapbar/minimalist-ios-tapbar.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/themefiles/minimalist-mobile-tapbar/minimalist-mobile-tapbar.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/themefiles/minimalist-mobile/minimalist-mobile.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/cn.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/cs.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/da.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/de.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/default.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/en.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/es.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/fi.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/fr.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/he.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/it.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/ko-KR.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/nl.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/no.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/pl.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/pt-BR.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/pt.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/ru.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/sk.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/sv.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/tr.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/uk.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/translations/zh-CN.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ui-lovelace.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/actions/actions_card.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/actions/actions_card_overlay.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/actions/actions_icon.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/actions/actions_name.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/2-line_cards/card_graph.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_battery.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_binary_sensor.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_binary_sensor_alert.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_cover.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_fan.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_generic.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_generic_swap.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_input_boolean.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_light.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_media_player.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_navigate.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_person.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_power_outlet.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_room.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_scenes_welcome.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_script.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_thermostat.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_vacuum.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_vertical_button.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_weather.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_weather_ulm.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/cards/card_welcome_scenes.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_alarm.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_back.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_icon_double_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_icon_label.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_icon_only.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_icon_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_mdi_icon_only.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_mdi_icon_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_navigate.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_power_consumption.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_presence_detection.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_short_date_with_day.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_temperature.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/chips/chip_weather_date.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/blue_no_card.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/blue_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/blue_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/blue_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/green_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/green_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/green_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/grey_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/grey_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/grey_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/pink_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/pink_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/pink_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/purple_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/purple_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/purple_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/red_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/red_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/red_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/yellow_no_card.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/yellow_no_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/yellow_off.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/yellow_on.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/colors/yellow_slider.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/chips.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/cover.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/edge.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/extended_card.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_alert.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_info.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_info_alert.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_info_bg.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_info_line.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_more_info.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_more_info_alert.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_more_info_new.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/icon_only.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_2_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_3_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_4_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_items_line.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_one_third_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/list_two_third_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/internal_templates/widget_icon.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/card_media_player_art.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/card_media_player_controls.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/cards.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/chips.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/list_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/legacy_templates/title.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/title/card_title.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/vertical_buttons/vertical_buttons.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/card_templates/vertical_buttons/vertical_buttons_custom_state.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_airconditionner.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_app.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_back.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_brightness.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_color.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_color_temp.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_cover_close.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_cover_open.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_cover_stop.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_forecast.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_history.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_light_more_options.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_playing.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_power.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_radar.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_selected.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_source.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_stats.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_button_volume.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_card_volume.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_chip_controls.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_chip_volume.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_header.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_header_cover.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_buttons/popup_header_light.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_defaults/popup_default.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_items/popup_item4_back_toggle.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_4_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_light_effect_row.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_light_palette_row.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_list_items.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_media_player_row.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_row_layout.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_subtitle.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popup_layouts/popup_weather_row.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_cover.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light_brightness.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light_color.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light_color_temp.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light_effect.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_light_palette.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_media_player.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_media_player_infos.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_media_player_source.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_media_player_source_card.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_media_player_volume.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_power_outlet.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_power_outlet_history.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_power_outlet_stats.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_thermostat.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_thermostat_temperature.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_vacuum.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_vacuum_map.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_weather.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_weather_forecast.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/lovelace/ulm_templates/popup_templates/popups/popup_weather_radar.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/manifest.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/services.yaml create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/strings.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/ca.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/cs.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/da.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/de.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/en.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/es.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/fi.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/fr.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/he.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/it.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/ko-KR.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/nl.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/pl.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/pt-BR.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/ru.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/sk.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/sl.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/sv.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/uk.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/translations/zh-CN.json create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/utils/decode.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/utils/json.py create mode 100644 homeassistant/config/custom_components/ui_lovelace_minimalist/utils/logger.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/__init__.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/config_flow.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/const.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/device_trigger.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/manifest.json create mode 100644 homeassistant/config/custom_components/yandex_pogoda/sensor.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/translations/en.json create mode 100644 homeassistant/config/custom_components/yandex_pogoda/translations/ru.json create mode 100644 homeassistant/config/custom_components/yandex_pogoda/updater.py create mode 100644 homeassistant/config/custom_components/yandex_pogoda/weather.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/__init__.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/backports.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_color.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_custom.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_mode.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_onoff.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_range.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_toggle.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/capability_video.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/cloud.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/cloud_stream.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/color.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/config_flow.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/config_schema.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/const.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/device.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/diagnostics.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/entry_data.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/handlers.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/helpers.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/http.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/manifest.json create mode 100644 homeassistant/config/custom_components/yandex_smart_home/notifier.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/property.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/property_custom.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/property_event.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/property_float.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/repairs.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/__init__.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/base.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/callback.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_color.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_mode.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_onoff.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_range.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_toggle.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/capability_video.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/device.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/property.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/property_event.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/property_float.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/schema/response.py create mode 100644 homeassistant/config/custom_components/yandex_smart_home/services.yaml create mode 100644 homeassistant/config/custom_components/yandex_smart_home/translations/en.json create mode 100644 homeassistant/config/custom_components/yandex_smart_home/unit_conversion.py create mode 100755 homeassistant/config/docker_containers_summary.py create mode 100644 homeassistant/config/scenes.yaml create mode 100644 homeassistant/config/scripts.yaml create mode 100644 homeassistant/config/themes/amoled/amoled.yaml create mode 100644 homeassistant/config/themes/minimalist-desktop/minimalist-desktop.yaml create mode 100644 homeassistant/config/themes/minimalist-ios-tapbar/minimalist-ios-tapbar.yaml create mode 100644 homeassistant/config/themes/minimalist-mobile-tapbar/minimalist-mobile-tapbar.yaml create mode 100644 homeassistant/config/themes/minimalist-mobile/minimalist-mobile.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/custom_actions/custom_actions.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/dashboard/adaptive-dash/adaptive-ui.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/dashboard/adaptive-dash/popup/popup.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/dashboard/adaptive-dash/views/livingroom.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/dashboard/adaptive-dash/views/main.yaml create mode 100644 homeassistant/config/ui_lovelace_minimalist/dashboard/ui-lovelace.yaml create mode 100644 homeassistant/docker-compose.yml create mode 100644 homeassistant/mosquitto/config/mosquitto.conf create mode 100644 immich/docker-compose.yml create mode 100644 immich/immich/db-init/01-extensions.sql create mode 100644 minio/docker-compose.yml create mode 100644 minio/policies/knowledge-base-policy.json create mode 100644 voice/docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c2233c --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# secrets +**/.env +**/secrets.yaml +**/*secret* +**/*.pem +**/*.key +**/*.crt + +# runtime data +**/data/ +**/logs/ +**/log/ +**/cache/ +**/tmp/ +**/backups/ +**/.storage/ +**/*.db +**/*.db-shm +**/*.db-wal +**/*.log +**/*.log.* +**/tts/ +**/__pycache__/ +**/*.pyc + +# service-specific caches +homeassistant/config/custom_components/hacs/hacs_frontend/ +homeassistant/config/home-assistant*.log* +homeassistant/config/home-assistant_v2.db* +homeassistant/config/tts/ +immich/immich/db/ +immich/immich/model-cache/ +immich/immich/redis/ +voice/piper-data/ +voice/whisper-data/ + +# archives +_archive/ + +# gitlab specific +**/gitlab/config/ +**/gitlab/config/gitlab-secrets.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1febc2 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# /srv/docker + +Единый репозиторий docker‑стеков сервера. + +## Содержимое +- `bitwarden/` — vaultwarden +- `gitlab/` — GitLab CE (self‑hosted) +- `homeassistant/` — HA +- `immich/` — Immich +- `minio/` — MinIO +- `voice/` — Wyoming (Piper/Whisper) +- `_legacy/` — старые/временные compose +- `_archive/` — архивы (в git не попадают) + +## Примечания +- Файлы `.env`, `data/`, `logs/`, `backups/` и прочие runtime‑данные исключены из git. +- Для добавления новых сервисов — создаём папку и `docker-compose.yml`. diff --git a/_legacy/docker-compose-no-envs.yml b/_legacy/docker-compose-no-envs.yml new file mode 100644 index 0000000..81deaaa --- /dev/null +++ b/_legacy/docker-compose-no-envs.yml @@ -0,0 +1,241 @@ +version: "3.8" + +services: + minio: + image: quay.io/minio/minio:latest + command: server /data1 /data2 --console-address ":9001" + environment: + MINIO_ROOT_USER: * + MINIO_ROOT_PASSWORD: * + MINIO_PROMETHEUS_AUTH_TYPE: public + MINIO_REGION: ru-1 + ports: + - "9000:9000" + - "9110:9001" + volumes: + - /mnt/disk2/minio1:/data1 + - /mnt/disk1/minio2:/data2 + restart: unless-stopped + + minio-init: + image: quay.io/minio/mc:latest + depends_on: [minio] + environment: + MC_HOST_minio: "http://minio-root:minio-root-pass@minio:9000" + entrypoint: > + /bin/sh -c " + mc alias set minio http://minio:9000 minio-root minio-root-pass && + mc mb -p minio/nextcloud || true && + mc admin user add minio nextcloud nextcloud-secret && + mc admin policy attach minio readwrite --user nextcloud || true && + exit 0 + " + restart: "no" + + postgres: + image: postgres:16 + environment: + POSTGRES_DB: nextcloud + POSTGRES_USER: * + POSTGRES_PASSWORD: * + volumes: + - /data/postgres:/var/lib/postgresql/data + restart: unless-stopped + + redis: + image: redis:7 + command: ["redis-server", "--save", "", "--appendonly", "no"] + restart: unless-stopped + + nextcloud: + build: ./nextcloud + image: nextcloud:30-apache-ffmpeg + depends_on: [postgres, redis, minio, minio-init] + ports: + - "80:80" + dns: + - 1.1.1.1 + - 8.8.8.8 + dns_opt: + - use-vc + environment: + POSTGRES_HOST: postgres + POSTGRES_DB: nextcloud + POSTGRES_USER: * + POSTGRES_PASSWORD: * + NEXTCLOUD_TRUSTED_DOMAINS: * + NEXTCLOUD_ADMIN_USER: * + NEXTCLOUD_ADMIN_PASSWORD: * + OVERWRITEHOST: * + OVERWRITEPROTOCOL: * + TRUSTED_PROXIES: * + MAGICK_THREAD_LIMIT: * + OMP_NUM_THREADS: * + volumes: + - /data/nextcloud/html:/var/www/html + + nextcloud-cron: + image: nextcloud:30-apache + restart: unless-stopped + volumes: + - /data/nextcloud/html:/var/www/html + entrypoint: /cron.sh + depends_on: + - nextcloud + + # --- IMMICH AND REQ-S --- + + immich-server: + image: ghcr.io/immich-app/immich-server:release + container_name: immich-server + environment: + # URL, по которому клиенты будут подключаться + PUBLIC_URL: https://photos.g4st3r.ru + UPLOAD_LOCATION: /usr/src/app/upload + + # база данных + DB_HOSTNAME: immich-db + DB_PORT: 5432 + DB_USERNAME: * + DB_PASSWORD: * + DB_DATABASE_NAME: * + + # Redis + REDIS_HOSTNAME: immich-redis + REDIS_PORT: 6379 + + # ML сервис + MACHINE_LEARNING_URL: http://immich-ml:3003 + volumes: + - /mnt/disk3/immich/uploads:/usr/src/app/upload + depends_on: + - immich-db + - immich-redis + - immich-ml + ports: + - 2283:2283 + restart: unless-stopped + + immich-ml: + image: ghcr.io/immich-app/immich-machine-learning:release + container_name: immich-ml + ports: + - "3003:3003" + environment: + - TRANSFORMERS_CACHE=/cache + volumes: + - /mnt/disk2/immich/ml-cache:/cache + restart: unless-stopped + + immich-db: + image: pgvector/pgvector:pg16 + container_name: immich-db + environment: + - POSTGRES_USER=${DB_USERNAME:-immich} + - POSTGRES_PASSWORD=${DB_PASSWORD:-immich_strong_pass} + - POSTGRES_DB=${DB_DATABASE_NAME:-immich} + volumes: + - /mnt/disk2/immich/db:/var/lib/postgresql/data + restart: unless-stopped + + immich-redis: + image: redis:7-alpine + container_name: immich-redis + command: ["redis-server", "--save", "60", "1"] + volumes: + - /mnt/disk2/immich/redis:/data + restart: unless-stopped + + + # --- EXPORTERS & MONITORING --- + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + restart: unless-stopped + ports: + - "9100:9100" + command: + - '--path.rootfs=/host' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)' + volumes: + - '/:/host:ro,rslave' + + postgres-exporter: + image: quay.io/prometheuscommunity/postgres-exporter:latest + container_name: postgres-exporter + restart: unless-stopped + environment: + # у тебя Postgres с юзером nc и БД nextcloud + DATA_SOURCE_NAME: "postgresql://nc:*@postgres:5432/nextcloud?sslmode=disable" + ports: + - "9187:9187" + depends_on: + - postgres + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + command: + - '--config.file=/etc/prometheus/prometheus.yml' + volumes: + - /srv/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + ports: + - "9090:9090" + depends_on: + - node-exporter + - cadvisor + - postgres-exporter + - minio + + smartctl-exporter: + image: prometheuscommunity/smartctl-exporter:v0.9.1 + container_name: smartctl-exporter + privileged: true # нужно, чтобы читать SMART с устройств + volumes: + - /dev:/dev:ro + - /run/udev:/run/udev:ro + environment: + - SMARTCTL_INTERVAL=300 # каждые 5 минут + ports: + - "9633:9633" + restart: unless-stopped + + grafana: + image: grafana/grafana:latest + container_name: grafana + restart: unless-stopped + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=* + - GF_SERVER_DOMAIN=* + - GF_SERVER_ROOT_URL=* + volumes: + - /srv/monitoring/grafana:/var/lib/grafana + - /srv/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + ports: + - "3000:3000" + depends_on: + - prometheus + + alertmanager: + image: prom/alertmanager:latest + container_name: alertmanager + command: + - --config.file=/etc/alertmanager/alertmanager.yml + - --web.external-url=https://alert.g4st3r.ru + volumes: + - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + - ./alertmanager/templates:/etc/alertmanager/templates:ro + ports: + - "9093:9093" + restart: unless-stopped + environment: + - TELEGRAM_BOT_TOKEN=* + - TELEGRAM_CHAT_ID=* + + +volumes: + prometheus-data: diff --git a/bitwarden/docker-compose.yml b/bitwarden/docker-compose.yml new file mode 100644 index 0000000..1833f6e --- /dev/null +++ b/bitwarden/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + vaultwarden: + image: vaultwarden/server:latest + container_name: vaultwarden + restart: unless-stopped + environment: + DOMAIN: "https://bw.g4st3r.ru" + SIGNUPS_ALLOWED: "false" + INVITATIONS_ALLOWED: "false" + WEBSOCKET_ENABLED: "true" + ADMIN_TOKEN: "${VW_ADMIN_TOKEN}" + volumes: + - ./data:/data + ports: + - "8087:80" diff --git a/gitlab/docker-compose.yml b/gitlab/docker-compose.yml new file mode 100644 index 0000000..b274850 --- /dev/null +++ b/gitlab/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' +services: + gitlab: + image: gitlab/gitlab-ce:latest + container_name: gitlab + restart: unless-stopped + hostname: gitlab + ports: + - "2224:22" + - "9080:80" + - "9443:443" + volumes: + - ./config:/etc/gitlab + - ./logs:/var/log/gitlab + - ./data:/var/opt/gitlab + environment: + GITLAB_OMNIBUS_CONFIG: | + external_url 'https://gitlab.g4st3r.ru' + gitlab_rails['gitlab_shell_ssh_port'] = 2224 diff --git a/homeassistant/config/.HA_VERSION b/homeassistant/config/.HA_VERSION new file mode 100644 index 0000000..e3f033b --- /dev/null +++ b/homeassistant/config/.HA_VERSION @@ -0,0 +1 @@ +2025.11.1 \ No newline at end of file diff --git a/homeassistant/config/.ha_run.lock b/homeassistant/config/.ha_run.lock new file mode 100644 index 0000000..2db9ae1 --- /dev/null +++ b/homeassistant/config/.ha_run.lock @@ -0,0 +1 @@ +{"pid": 67, "version": 1, "ha_version": "2025.11.1", "start_ts": 1771675562.0109854} \ No newline at end of file diff --git a/homeassistant/config/automations.yaml b/homeassistant/config/automations.yaml new file mode 100644 index 0000000..ba0ad65 --- /dev/null +++ b/homeassistant/config/automations.yaml @@ -0,0 +1,395 @@ +- id: '1762991834560' + alias: Утро - свет + description: '' + triggers: + - trigger: time + at: '9:30:00' + weekday: + - mon + - tue + - wed + - thu + - fri + conditions: + - condition: device + device_id: 51bac6c99c3cc7a5c2640ce4cd55cca8 + domain: device_tracker + entity_id: 842d4a75a61896ca8d3930b322c57764 + type: is_home + actions: + - action: light.turn_on + metadata: {} + data: + color_temp_kelvin: 6500 + brightness_pct: 100 + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + mode: single +- id: '1762992032227' + alias: Утро - свет моргание + description: '' + triggers: + - trigger: time + at: 09:10:00 + weekday: + - mon + - wed + - tue + - thu + - fri + - sat + - sun + conditions: + - condition: device + device_id: 51bac6c99c3cc7a5c2640ce4cd55cca8 + domain: device_tracker + entity_id: 842d4a75a61896ca8d3930b322c57764 + type: is_home + actions: + - action: light.turn_off + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_on + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_off + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_on + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_off + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_on + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_off + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - action: light.turn_on + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + mode: single +- id: '1762992378204' + alias: Ночь - свет + description: '' + triggers: + - trigger: time + at: 00:00:00 + weekday: + - mon + - wed + - tue + - thu + - fri + - sat + - sun + conditions: [] + actions: + - action: light.turn_off + metadata: {} + data: {} + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + mode: single +- id: '1762992684588' + alias: Вечер - свет + description: '' + triggers: + - trigger: sun + event: sunset + offset: 0 + conditions: + - condition: device + device_id: 51bac6c99c3cc7a5c2640ce4cd55cca8 + domain: device_tracker + entity_id: 842d4a75a61896ca8d3930b322c57764 + type: is_home + actions: + - action: light.turn_on + metadata: {} + data: + color_temp_kelvin: 2000 + brightness_pct: 50 + target: + entity_id: + - light.iot_led_classic + - light.iot_a61_rgb + mode: single +- id: '1762994506315' + alias: Утренний дашборд в Telegram + description: Подробный утренний отчёт о доме и системе + triggers: + - at: 09:30:00 + trigger: time + conditions: [] + actions: + - action: telegram_bot.send_message + data: + message: 'Доброе утро! + + + Это тестовое сообщение + + + Свет уже должен гореть в спальне' + mode: single +- id: "1762999000001" + alias: "Алерты - ресурсы системы" + description: "Диск/память/CPU: уведомления в Telegram" + triggers: + - trigger: numeric_state + entity_id: sensor.localhost_disk_free + below: 20 + for: "00:10:00" + - trigger: numeric_state + entity_id: sensor.localhost_mnt_disk2_disk_usage + above: 80 + for: "00:10:00" + - trigger: numeric_state + entity_id: sensor.localhost_mnt_disk3_disk_usage + above: 80 + for: "00:10:00" + - trigger: numeric_state + entity_id: sensor.localhost_memory_usage + above: 90 + for: "00:10:00" + - trigger: numeric_state + entity_id: sensor.localhost_swap_usage + above: 80 + for: "00:10:00" + - trigger: numeric_state + entity_id: sensor.localhost_cpu_usage + above: 80 + for: "00:05:00" + actions: + - choose: + - conditions: "{{ trigger.entity_id == sensor.localhost_disk_free and (trigger.to_state.state|float(0)) < 10 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🔴 CRIT: Мало места на root — {{ trigger.to_state.state }} {{ trigger.to_state.attributes.unit_of_measurement }} свободно" + - conditions: "{{ trigger.entity_id == sensor.localhost_disk_free and (trigger.to_state.state|float(0)) < 20 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🟡 WARN: Мало места на root — {{ trigger.to_state.state }} {{ trigger.to_state.attributes.unit_of_measurement }} свободно" + - conditions: "{{ trigger.entity_id in [sensor.localhost_mnt_disk2_disk_usage,sensor.localhost_mnt_disk3_disk_usage] and (trigger.to_state.state|float(0)) >= 90 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🔴 CRIT: Диск {{ trigger.entity_id }} заполнен на {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id in [sensor.localhost_mnt_disk2_disk_usage,sensor.localhost_mnt_disk3_disk_usage] and (trigger.to_state.state|float(0)) >= 80 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🟡 WARN: Диск {{ trigger.entity_id }} заполнен на {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id == sensor.localhost_memory_usage and (trigger.to_state.state|float(0)) >= 95 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🔴 CRIT: RAM {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id == sensor.localhost_memory_usage and (trigger.to_state.state|float(0)) >= 90 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🟡 WARN: RAM {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id == sensor.localhost_swap_usage and (trigger.to_state.state|float(0)) >= 90 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🔴 CRIT: Swap {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id == sensor.localhost_swap_usage and (trigger.to_state.state|float(0)) >= 80 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🟡 WARN: Swap {{ trigger.to_state.state }}%" + - conditions: "{{ trigger.entity_id == sensor.localhost_cpu_usage and (trigger.to_state.state|float(0)) >= 90 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🔴 CRIT: CPU {{ trigger.to_state.state }}% (5+ мин)" + - conditions: "{{ trigger.entity_id == sensor.localhost_cpu_usage and (trigger.to_state.state|float(0)) >= 80 }}" + sequence: + - action: telegram_bot.send_message + data: + message: "🟡 WARN: CPU {{ trigger.to_state.state }}% (5+ мин)" + mode: single +- id: "1763004000001" + alias: "Контекст: выход из дома — лампочки" + description: "Напоминание о покупке лампочек при выходе из дома" + triggers: + - trigger: state + entity_id: person.aleksandr + to: "not_home" + for: "00:02:00" + conditions: + - condition: template + value_template: "{{ this.attributes.last_triggered is none or (as_timestamp(now()) - as_timestamp(this.attributes.last_triggered)) > 21600 }}" + actions: + - action: telegram_bot.send_message + data: + message: "🛒 Не забудь: купить лампочки на кухню и коридор." + mode: single + +- id: "1763004000002" + alias: "Контекст: возвращение домой — барабаны" + description: "Вечернее напоминание обновить заметку про барабаны" + triggers: + - trigger: state + entity_id: person.aleksandr + to: "home" + for: "00:02:00" + conditions: + - condition: time + after: "18:00:00" + before: "23:30:00" + - condition: template + value_template: "{{ this.attributes.last_triggered is none or (as_timestamp(now()) - as_timestamp(this.attributes.last_triggered)) > 21600 }}" + actions: + - action: telegram_bot.send_message + data: + message: "🥁 Если есть силы: обнови заметку по барабанам (цели/план/что практиковать)." + mode: single +- id: "1763004000003" + alias: "Контекст: утро дома — 3 приоритета" + description: "Утреннее напоминание выбрать 3 приоритета дня" + triggers: + - trigger: state + entity_id: person.aleksandr + to: "home" + for: "00:05:00" + conditions: + - condition: time + after: "08:30:00" + before: "12:00:00" + - condition: template + value_template: "{{ this.attributes.last_triggered is none or (as_timestamp(now()) - as_timestamp(this.attributes.last_triggered)) > 21600 }}" + actions: + - action: telegram_bot.send_message + data: + message: "☀️ Выбери 3 главных приоритета на сегодня (1 важное, 1 среднее, 1 мелочь)." + mode: single + +- id: "1763004000004" + alias: "Контекст: ночь — проверить дом" + description: "Ночное напоминание проверить свет и утренние дела" + triggers: + - trigger: time + at: "23:30:00" + conditions: + - condition: state + entity_id: person.aleksandr + state: "home" + actions: + - action: telegram_bot.send_message + data: + message: "🌙 На ночь: проверь, что свет/приборы выключены, и отметь 1 утреннюю задачу." + mode: single +- id: "1763004000005" + alias: "Контекст: выезд с работы — Ozon" + description: "Напоминание зайти в пункт выдачи Ozon по дороге" + triggers: + - trigger: state + entity_id: person.aleksandr + to: "not_home" + for: "00:02:00" + conditions: + - condition: time + after: "17:00:00" + before: "21:30:00" + - condition: template + value_template: "{{ this.attributes.last_triggered is none or (as_timestamp(now()) - as_timestamp(this.attributes.last_triggered)) > 21600 }}" + actions: + - action: telegram_bot.send_message + data: + message: "📦 По дороге: зайди в пункт выдачи Ozon." + mode: single +- id: "1763004000006" + alias: "Напоминание: отпуск к 25 числу" + description: "Раз в пару дней после 1 марта напоминать про отпуск на 25 число" + triggers: + - trigger: time + at: "10:00:00" + conditions: + - condition: template + value_template: "{{ now().date() >= strptime(2026-03-01,%Y-%m-%d).date() }}" + - condition: template + value_template: "{{ (now().timetuple().tm_yday % 2) == 1 }}" + actions: + - action: telegram_bot.send_message + data: + message: "✈️ Напоминание: подумай об отпуске на 25‑е (и соседние даты)." + mode: single diff --git a/homeassistant/config/blueprints/automation/homeassistant/motion_light.yaml b/homeassistant/config/blueprints/automation/homeassistant/motion_light.yaml new file mode 100644 index 0000000..1190070 --- /dev/null +++ b/homeassistant/config/blueprints/automation/homeassistant/motion_light.yaml @@ -0,0 +1,58 @@ +blueprint: + name: Motion-activated Light + description: Turn on a light when motion is detected. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + author: Home Assistant + input: + motion_entity: + name: Motion Sensor + selector: + entity: + filter: + - device_class: occupancy + domain: binary_sensor + - device_class: motion + domain: binary_sensor + light_target: + name: Light + selector: + target: + entity: + domain: light + no_motion_wait: + name: Wait time + description: Time to leave the light on after last motion is detected. + default: 120 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds + +# If motion is detected within the delay, +# we restart the script. +mode: restart +max_exceeded: silent + +triggers: + trigger: state + entity_id: !input motion_entity + from: "off" + to: "on" + +actions: + - alias: "Turn on the light" + action: light.turn_on + target: !input light_target + - alias: "Wait until there is no motion from device" + wait_for_trigger: + trigger: state + entity_id: !input motion_entity + from: "on" + to: "off" + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + action: light.turn_off + target: !input light_target diff --git a/homeassistant/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml b/homeassistant/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml new file mode 100644 index 0000000..e072aad --- /dev/null +++ b/homeassistant/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml @@ -0,0 +1,50 @@ +blueprint: + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + author: Home Assistant + input: + person_entity: + name: Person + selector: + entity: + filter: + domain: person + zone_entity: + name: Zone + selector: + entity: + filter: + domain: zone + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + filter: + integration: mobile_app + +triggers: + trigger: state + entity_id: !input person_entity + +variables: + zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. + zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" + +conditions: + condition: template + # The first case handles leaving the Home zone which has a special state when zoning called 'home'. + # The second case handles leaving all other zones. + value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +actions: + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/homeassistant/config/blueprints/script/homeassistant/confirmable_notification.yaml b/homeassistant/config/blueprints/script/homeassistant/confirmable_notification.yaml new file mode 100644 index 0000000..0106a4e --- /dev/null +++ b/homeassistant/config/blueprints/script/homeassistant/confirmable_notification.yaml @@ -0,0 +1,86 @@ +blueprint: + name: Confirmable Notification + description: >- + A script that sends an actionable notification with a confirmation before + running the specified action. + domain: script + source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml + author: Home Assistant + input: + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + filter: + integration: mobile_app + title: + name: "Title" + description: "The title of the button shown in the notification." + default: "" + selector: + text: + message: + name: "Message" + description: "The message body" + selector: + text: + confirm_text: + name: "Confirmation Text" + description: "Text to show on the confirmation button" + default: "Confirm" + selector: + text: + confirm_action: + name: "Confirmation Action" + description: "Action to run when notification is confirmed" + default: [] + selector: + action: + dismiss_text: + name: "Dismiss Text" + description: "Text to show on the dismiss button" + default: "Dismiss" + selector: + text: + dismiss_action: + name: "Dismiss Action" + description: "Action to run when notification is dismissed" + default: [] + selector: + action: + +mode: restart + +sequence: + - alias: "Set up variables" + variables: + action_confirm: "{{ 'CONFIRM_' ~ context.id }}" + action_dismiss: "{{ 'DISMISS_' ~ context.id }}" + - alias: "Send notification" + domain: mobile_app + type: notify + device_id: !input notify_device + title: !input title + message: !input message + data: + actions: + - action: "{{ action_confirm }}" + title: !input confirm_text + - action: "{{ action_dismiss }}" + title: !input dismiss_text + - alias: "Awaiting response" + wait_for_trigger: + - trigger: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_confirm }}" + - trigger: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_dismiss }}" + - choose: + - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" + sequence: !input confirm_action + - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" + sequence: !input dismiss_action diff --git a/homeassistant/config/blueprints/template/homeassistant/inverted_binary_sensor.yaml b/homeassistant/config/blueprints/template/homeassistant/inverted_binary_sensor.yaml new file mode 100644 index 0000000..5be1840 --- /dev/null +++ b/homeassistant/config/blueprints/template/homeassistant/inverted_binary_sensor.yaml @@ -0,0 +1,27 @@ +blueprint: + name: Invert a binary sensor + description: Creates a binary_sensor which holds the inverted value of a reference binary_sensor + domain: template + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml + input: + reference_entity: + name: Binary sensor to be inverted + description: The binary_sensor which needs to have its value inverted + selector: + entity: + domain: binary_sensor +variables: + reference_entity: !input reference_entity +binary_sensor: + state: > + {% if states(reference_entity) == 'on' %} + off + {% elif states(reference_entity) == 'off' %} + on + {% else %} + {{ states(reference_entity) }} + {% endif %} + # delay_on: not_used in this example + # delay_off: not_used in this example + # auto_off: not_used in this example + availability: "{{ states(reference_entity) not in ('unknown', 'unavailable') }}" diff --git a/homeassistant/config/configuration.yaml b/homeassistant/config/configuration.yaml new file mode 100644 index 0000000..34c9621 --- /dev/null +++ b/homeassistant/config/configuration.yaml @@ -0,0 +1,63 @@ +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + - ::1 + - 10.8.0.0/24 + +template: + - sensor: + - name: "Network RX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states(sensor.localhost_wlp2s0_rx) | float * 8 / 1_000_000 ) | round(2) }} + + - name: "Network TX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states(sensor.localhost_wlp2s0_tx) | float * 8 / 1_000_000 ) | round(2) }} + +command_line: + - sensor: + name: "WireGuard wg0 status" + command: >- + ip link show wg0 | grep -q "LOWER_UP" && echo UP || echo DOWN + scan_interval: 30 + - sensor: + name: "Docker containers summary" + command: "/config/docker_containers_summary.py" + scan_interval: 60 + value_template: "{{ value_json.count }}" + json_attributes: + - list + +rest: + - resource: http://127.0.0.1:8099/status + scan_interval: 60 + sensor: + - name: "OpenClaw Gateway Reachable" + value_template: "{{ value_json.gateway.reachable }}" + - name: "OpenClaw Gateway Latency" + unit_of_measurement: "ms" + value_template: "{{ value_json.gateway.connectLatencyMs }}" + - name: "OpenClaw Sessions" + value_template: "{{ value_json.agents.totalSessions }}" + - name: "OpenClaw Bootstrap Pending" + value_template: "{{ value_json.agents.bootstrapPendingCount }}" + - name: "OpenClaw Last Active Age" + unit_of_measurement: "ms" + value_template: "{{ value_json.agents.agents[0].lastActiveAgeMs }}" + +shell_command: + docker_restart: "curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/{{ container }}/restart" diff --git a/homeassistant/config/configuration.yaml.bak.2026-01-31_233007 b/homeassistant/config/configuration.yaml.bak.2026-01-31_233007 new file mode 100644 index 0000000..3b43370 --- /dev/null +++ b/homeassistant/config/configuration.yaml.bak.2026-01-31_233007 @@ -0,0 +1,38 @@ + +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + - ::1 + - 10.8.0.0/24 + +template: + - sensor: + - name: "Network RX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_rx') | float * 8 / 1_000_000 ) | round(2) }} + + - name: "Network TX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_tx') | float * 8 / 1_000_000 ) | round(2) }} + +sensor: + - platform: command_line + name: "WireGuard wg0 status" + command: >- + ip link show wg0 | grep -q "LOWER_UP" && echo UP || echo DOWN + scan_interval: 30 + diff --git a/homeassistant/config/configuration.yaml.bak.2026-02-01_022515 b/homeassistant/config/configuration.yaml.bak.2026-02-01_022515 new file mode 100644 index 0000000..e433230 --- /dev/null +++ b/homeassistant/config/configuration.yaml.bak.2026-02-01_022515 @@ -0,0 +1,37 @@ + +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + - ::1 + - 10.8.0.0/24 + +template: + - sensor: + - name: "Network RX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_rx') | float * 8 / 1_000_000 ) | round(2) }} + + - name: "Network TX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_tx') | float * 8 / 1_000_000 ) | round(2) }} + +sensor: + - platform: command_line + name: "WireGuard wg0 status" + command: >- + ip link show wg0 | grep -q "LOWER_UP" && echo UP || echo DOWN + scan_interval: 30 diff --git a/homeassistant/config/configuration.yaml.bak_before_mqttfix.2026-02-01_022622 b/homeassistant/config/configuration.yaml.bak_before_mqttfix.2026-02-01_022622 new file mode 100644 index 0000000..c84b841 --- /dev/null +++ b/homeassistant/config/configuration.yaml.bak_before_mqttfix.2026-02-01_022622 @@ -0,0 +1,41 @@ + +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 + - ::1 + - 10.8.0.0/24 + +template: + - sensor: + - name: "Network RX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_rx') | float * 8 / 1_000_000 ) | round(2) }} + + - name: "Network TX Mbit (wlp2s0)" + unit_of_measurement: "Mbit/s" + state: > + {{ (states('sensor.localhost_wlp2s0_tx') | float * 8 / 1_000_000 ) | round(2) }} + +sensor: + - platform: command_line + name: "WireGuard wg0 status" + command: >- + ip link show wg0 | grep -q "LOWER_UP" && echo UP || echo DOWN + scan_interval: 30 + +mqtt: + broker: 127.0.0.1 + port: 1883 diff --git a/homeassistant/config/custom_components/hacs/__init__.py b/homeassistant/config/custom_components/hacs/__init__.py new file mode 100644 index 0000000..6af3f8f --- /dev/null +++ b/homeassistant/config/custom_components/hacs/__init__.py @@ -0,0 +1,229 @@ +"""HACS gives you a powerful UI to handle downloads of all your custom needs. + +For more details about this integration, please refer to the documentation at +https://hacs.xyz/ +""" + +from __future__ import annotations + +from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI +from aiogithubapi.const import ACCEPT_HEADERS +from awesomeversion import AwesomeVersion +from homeassistant.components.frontend import async_remove_panel +from homeassistant.components.lovelace.system_health import system_health_info +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import Platform, __version__ as HAVERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start +from homeassistant.loader import async_get_integration + +from .base import HacsBase +from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP +from .data_client import HacsDataClient +from .enums import HacsDisabledReason, HacsStage, LovelaceMode +from .frontend import async_register_frontend +from .utils.data import HacsData +from .utils.queue_manager import QueueManager +from .utils.version import version_left_higher_or_equal_then_right +from .websocket import async_register_websocket_commands + +PLATFORMS = [Platform.SWITCH, Platform.UPDATE] + + +async def _async_initialize_integration( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> bool: + """Initialize the integration""" + hass.data[DOMAIN] = hacs = HacsBase() + hacs.enable_hacs() + + if config_entry.source == SOURCE_IMPORT: + # Import is not supported + hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) + return False + + hacs.configuration.update_from_dict( + { + "config_entry": config_entry, + **config_entry.data, + **config_entry.options, + }, + ) + + integration = await async_get_integration(hass, DOMAIN) + + hacs.set_stage(None) + + hacs.log.info(STARTUP, integration.version) + + clientsession = async_get_clientsession(hass) + + hacs.integration = integration + hacs.version = integration.version + hacs.configuration.dev = integration.version == "0.0.0" + hacs.hass = hass + hacs.queue = QueueManager(hass=hass) + hacs.data = HacsData(hacs=hacs) + hacs.data_client = HacsDataClient( + session=clientsession, + client_name=f"HACS/{integration.version}", + ) + hacs.system.running = True + hacs.session = clientsession + + hacs.core.lovelace_mode = LovelaceMode.YAML + try: + lovelace_info = await system_health_info(hacs.hass) + hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml")) + except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except + # If this happens, the users YAML is not valid, we assume YAML mode + pass + hacs.core.config_path = hacs.hass.config.path() + + if hacs.core.ha_version is None: + hacs.core.ha_version = AwesomeVersion(HAVERSION) + + ## Legacy GitHub client + hacs.github = GitHub( + hacs.configuration.token, + clientsession, + headers={ + "User-Agent": f"HACS/{hacs.version}", + "Accept": ACCEPT_HEADERS["preview"], + }, + ) + + ## New GitHub client + hacs.githubapi = GitHubAPI( + token=hacs.configuration.token, + session=clientsession, + **{"client_name": f"HACS/{hacs.version}"}, + ) + + async def async_startup(): + """HACS startup tasks.""" + hacs.enable_hacs() + + try: + import custom_components.custom_updater + except ImportError: + pass + else: + hacs.log.critical( + "HACS cannot be used with custom_updater. " + "To use HACS you need to remove custom_updater from `custom_components`", + ) + + hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) + return False + + if not version_left_higher_or_equal_then_right( + hacs.core.ha_version.string, + MINIMUM_HA_VERSION, + ): + hacs.log.critical( + "You need HA version %s or newer to use this integration.", + MINIMUM_HA_VERSION, + ) + hacs.disable_hacs(HacsDisabledReason.CONSTRAINS) + return False + + if not await hacs.data.restore(): + hacs.disable_hacs(HacsDisabledReason.RESTORE) + return False + + hacs.set_active_categories() + + async_register_websocket_commands(hass) + await async_register_frontend(hass, hacs) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + hacs.set_stage(HacsStage.SETUP) + if hacs.system.disabled: + return False + + hacs.set_stage(HacsStage.WAITING) + hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts") + + # Schedule startup tasks + async_at_start(hass=hass, at_start_cb=hacs.startup_tasks) + + return not hacs.system.disabled + + async def async_try_startup(_=None): + """Startup wrapper for yaml config.""" + try: + startup_result = await async_startup() + except AIOGitHubAPIException: + startup_result = False + if not startup_result: + if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN: + hacs.log.info("Could not setup HACS, trying again in 15 min") + async_call_later(hass, 900, async_try_startup) + return + hacs.enable_hacs() + + await async_try_startup() + + # Remove old (v0-v1) sensor if it exists, can be removed in v3 + er = async_get_entity_registry(hass) + if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID): + er.async_remove(old_sensor) + + # Mischief managed! + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry) + hacs: HacsBase = hass.data[DOMAIN] + return setup_result and not hacs.system.disabled + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" + hacs: HacsBase = hass.data[DOMAIN] + + if hacs.queue.has_pending_tasks: + hacs.log.warning("Pending tasks, can not unload, try again later.") + return False + + # Clear out pending queue + hacs.queue.clear() + + for task in hacs.recurring_tasks: + # Cancel all pending tasks + task() + + # Store data + await hacs.data.async_write(force=True) + + try: + if hass.data.get("frontend_panels", {}).get("hacs"): + hacs.log.info("Removing sidepanel") + async_remove_panel(hass, "hacs") + except AttributeError: + pass + + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + hacs.set_stage(None) + hacs.disable_hacs(HacsDisabledReason.REMOVED) + + hass.data.pop(DOMAIN, None) + + return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Reload the HACS config entry.""" + if not await async_unload_entry(hass, config_entry): + return + await async_setup_entry(hass, config_entry) diff --git a/homeassistant/config/custom_components/hacs/base.py b/homeassistant/config/custom_components/hacs/base.py new file mode 100644 index 0000000..a29e43d --- /dev/null +++ b/homeassistant/config/custom_components/hacs/base.py @@ -0,0 +1,1110 @@ +"""Base HACS class.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from dataclasses import asdict, dataclass, field +from datetime import timedelta +import gzip +import math +import os +import pathlib +import shutil +from typing import TYPE_CHECKING, Any + +from aiogithubapi import ( + AIOGitHubAPIException, + GitHub, + GitHubAPI, + GitHubAuthenticationException, + GitHubException, + GitHubNotModifiedException, + GitHubRatelimitException, +) +from aiogithubapi.objects.repository import AIOGitHubAPIRepository +from aiohttp.client import ClientSession, ClientTimeout +from awesomeversion import AwesomeVersion +from homeassistant.components.persistent_notification import ( + async_create as async_create_persistent_notification, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.loader import Integration +from homeassistant.util import dt + +from .const import DOMAIN, TV, URL_BASE +from .coordinator import HacsUpdateCoordinator +from .data_client import HacsDataClient +from .enums import ( + HacsCategory, + HacsDisabledReason, + HacsDispatchEvent, + HacsGitHubRepo, + HacsStage, + LovelaceMode, +) +from .exceptions import ( + AddonRepositoryException, + HacsException, + HacsExecutionStillInProgress, + HacsExpectedException, + HacsNotModifiedException, + HacsRepositoryArchivedException, + HacsRepositoryExistException, + HomeAssistantCoreRepositoryException, +) +from .repositories import REPOSITORY_CLASSES +from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT +from .utils.file_system import async_exists +from .utils.json import json_loads +from .utils.logger import LOGGER +from .utils.queue_manager import QueueManager +from .utils.store import async_load_from_store, async_save_to_store +from .utils.workarounds import async_register_static_path + +if TYPE_CHECKING: + from .repositories.base import HacsRepository + from .utils.data import HacsData + from .validate.manager import ValidationManager + + +@dataclass +class RemovedRepository: + """Removed repository.""" + + repository: str | None = None + reason: str | None = None + link: str | None = None + removal_type: str = None # archived, not_compliant, critical, dev, broken + acknowledged: bool = False + + def update_data(self, data: dict): + """Update data of the repository.""" + for key in data: + if data[key] is None: + continue + if key in ( + "reason", + "link", + "removal_type", + "acknowledged", + ): + self.__setattr__(key, data[key]) + + def to_json(self): + """Return a JSON representation of the data.""" + return { + "repository": self.repository, + "reason": self.reason, + "link": self.link, + "removal_type": self.removal_type, + "acknowledged": self.acknowledged, + } + + +@dataclass +class HacsConfiguration: + """HacsConfiguration class.""" + + appdaemon_path: str = "appdaemon/apps/" + appdaemon: bool = False + config: dict[str, Any] = field(default_factory=dict) + config_entry: ConfigEntry | None = None + country: str = "ALL" + debug: bool = False + dev: bool = False + frontend_repo_url: str = "" + frontend_repo: str = "" + plugin_path: str = "www/community/" + python_script_path: str = "python_scripts/" + python_script: bool = False + release_limit: int = 5 + sidepanel_icon: str = "hacs:hacs" + sidepanel_title: str = "HACS" + theme_path: str = "themes/" + theme: bool = False + token: str = None + + def to_json(self) -> str: + """Return a json string.""" + return asdict(self) + + def update_from_dict(self, data: dict) -> None: + """Set attributes from dicts.""" + if not isinstance(data, dict): + raise HacsException("Configuration is not valid.") + + for key in data: + if key in {"experimental", "netdaemon", "release_limit", "debug"}: + continue + self.__setattr__(key, data[key]) + + +@dataclass +class HacsCore: + """HACS Core info.""" + + config_path: pathlib.Path | None = None + ha_version: AwesomeVersion | None = None + lovelace_mode = LovelaceMode("yaml") + + +@dataclass +class HacsCommon: + """Common for HACS.""" + + categories: set[str] = field(default_factory=set) + renamed_repositories: dict[str, str] = field(default_factory=dict) + archived_repositories: set[str] = field(default_factory=set) + ignored_repositories: set[str] = field(default_factory=set) + skip: set[str] = field(default_factory=set) + + +@dataclass +class HacsStatus: + """HacsStatus.""" + + startup: bool = True + new: bool = False + active_frontend_endpoint_plugin: bool = False + active_frontend_endpoint_theme: bool = False + inital_fetch_done: bool = False + + +@dataclass +class HacsSystem: + """HACS System info.""" + + disabled_reason: HacsDisabledReason | None = None + running: bool = False + stage = HacsStage.SETUP + action: bool = False + generator: bool = False + + @property + def disabled(self) -> bool: + """Return if HACS is disabled.""" + return self.disabled_reason is not None + + +@dataclass +class HacsRepositories: + """HACS Repositories.""" + + _default_repositories: set[str] = field(default_factory=set) + _repositories: set[HacsRepository] = field(default_factory=set) + _repositories_by_full_name: dict[str, HacsRepository] = field(default_factory=dict) + _repositories_by_id: dict[str, HacsRepository] = field(default_factory=dict) + _removed_repositories_by_full_name: dict[str, RemovedRepository] = field(default_factory=dict) + + @property + def list_all(self) -> list[HacsRepository]: + """Return a list of repositories.""" + return list(self._repositories) + + @property + def list_removed(self) -> list[RemovedRepository]: + """Return a list of removed repositories.""" + return list(self._removed_repositories_by_full_name.values()) + + @property + def list_downloaded(self) -> list[HacsRepository]: + """Return a list of downloaded repositories.""" + return [repo for repo in self._repositories if repo.data.installed] + + def category_downloaded(self, category: HacsCategory) -> bool: + """Check if a given category has been downloaded.""" + for repository in self.list_downloaded: + if repository.data.category == category: + return True + return False + + def register(self, repository: HacsRepository, default: bool = False) -> None: + """Register a repository.""" + repo_id = str(repository.data.id) + + if repo_id == "0": + return + + if registered_repo := self._repositories_by_id.get(repo_id): + if registered_repo.data.full_name == repository.data.full_name: + return + + self.unregister(registered_repo) + + registered_repo.data.full_name = repository.data.full_name + registered_repo.data.new = False + repository = registered_repo + + if repository not in self._repositories: + self._repositories.add(repository) + + self._repositories_by_id[repo_id] = repository + self._repositories_by_full_name[repository.data.full_name_lower] = repository + + if default: + self.mark_default(repository) + + def unregister(self, repository: HacsRepository) -> None: + """Unregister a repository.""" + repo_id = str(repository.data.id) + + if repo_id == "0": + return + + if not self.is_registered(repository_id=repo_id): + return + + if self.is_default(repo_id): + self._default_repositories.remove(repo_id) + + if repository in self._repositories: + self._repositories.remove(repository) + + self._repositories_by_id.pop(repo_id, None) + self._repositories_by_full_name.pop(repository.data.full_name_lower, None) + + def mark_default(self, repository: HacsRepository) -> None: + """Mark a repository as default.""" + repo_id = str(repository.data.id) + + if repo_id == "0": + return + + if not self.is_registered(repository_id=repo_id): + return + + self._default_repositories.add(repo_id) + + def set_repository_id(self, repository: HacsRepository, repo_id: str): + """Update a repository id.""" + existing_repo_id = str(repository.data.id) + if existing_repo_id == repo_id: + return + if existing_repo_id != "0": + raise ValueError( + f"The repo id for {repository.data.full_name_lower} " + f"is already set to {existing_repo_id}" + ) + repository.data.id = repo_id + self.register(repository) + + def is_default(self, repository_id: str | None = None) -> bool: + """Check if a repository is default.""" + if not repository_id: + return False + return repository_id in self._default_repositories + + def is_registered( + self, + repository_id: str | None = None, + repository_full_name: str | None = None, + ) -> bool: + """Check if a repository is registered.""" + if repository_id is not None: + return repository_id in self._repositories_by_id + if repository_full_name is not None: + return repository_full_name in self._repositories_by_full_name + return False + + def is_downloaded( + self, + repository_id: str | None = None, + repository_full_name: str | None = None, + ) -> bool: + """Check if a repository is registered.""" + if repository_id is not None: + repo = self.get_by_id(repository_id) + if repository_full_name is not None: + repo = self.get_by_full_name(repository_full_name) + if repo is None: + return False + return repo.data.installed + + def get_by_id(self, repository_id: str | None) -> HacsRepository | None: + """Get repository by id.""" + if not repository_id: + return None + return self._repositories_by_id.get(str(repository_id)) + + def get_by_full_name(self, repository_full_name: str | None) -> HacsRepository | None: + """Get repository by full name.""" + if not repository_full_name: + return None + return self._repositories_by_full_name.get(repository_full_name.lower()) + + def is_removed(self, repository_full_name: str) -> bool: + """Check if a repository is removed.""" + return repository_full_name in self._removed_repositories_by_full_name + + def removed_repository(self, repository_full_name: str) -> RemovedRepository: + """Get repository by full name.""" + if removed := self._removed_repositories_by_full_name.get(repository_full_name): + return removed + + removed = RemovedRepository(repository=repository_full_name) + self._removed_repositories_by_full_name[repository_full_name] = removed + return removed + + +class HacsBase: + """Base HACS class.""" + + data: HacsData | None = None + data_client: HacsDataClient | None = None + frontend_version: str | None = None + github: GitHub | None = None + githubapi: GitHubAPI | None = None + hass: HomeAssistant | None = None + integration: Integration | None = None + queue: QueueManager | None = None + repository: AIOGitHubAPIRepository | None = None + session: ClientSession | None = None + stage: HacsStage | None = None + validation: ValidationManager | None = None + version: AwesomeVersion | None = None + + def __init__(self) -> None: + """Initialize.""" + self.common = HacsCommon() + self.configuration = HacsConfiguration() + self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {} + self.core = HacsCore() + self.log = LOGGER + self.recurring_tasks: list[Callable[[], None]] = [] + self.repositories = HacsRepositories() + self.status = HacsStatus() + self.system = HacsSystem() + + @property + def integration_dir(self) -> pathlib.Path: + """Return the HACS integration dir.""" + return self.integration.file_path + + def set_stage(self, stage: HacsStage | None) -> None: + """Set HACS stage.""" + if stage and self.stage == stage: + return + + self.stage = stage + if stage is not None: + self.log.info("Stage changed: %s", self.stage) + self.async_dispatch(HacsDispatchEvent.STAGE, {"stage": self.stage}) + + def disable_hacs(self, reason: HacsDisabledReason) -> None: + """Disable HACS.""" + if self.system.disabled_reason == reason: + return + + self.system.disabled_reason = reason + if reason != HacsDisabledReason.REMOVED: + self.log.error("HACS is disabled - %s", reason) + + if reason == HacsDisabledReason.INVALID_TOKEN: + self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass) + + def enable_hacs(self) -> None: + """Enable HACS.""" + if self.system.disabled_reason is not None: + self.system.disabled_reason = None + self.log.info("HACS is enabled") + + def enable_hacs_category(self, category: HacsCategory) -> None: + """Enable HACS category.""" + if category not in self.common.categories: + self.log.info("Enable category: %s", category) + self.common.categories.add(category) + self.coordinators[category] = HacsUpdateCoordinator() + + def disable_hacs_category(self, category: HacsCategory) -> None: + """Disable HACS category.""" + if category in self.common.categories: + self.log.info("Disabling category: %s", category) + self.common.categories.pop(category) + self.coordinators.pop(category) + + async def async_save_file(self, file_path: str, content: Any) -> bool: + """Save a file.""" + + def _write_file(): + with open( + file_path, + mode="w" if isinstance(content, str) else "wb", + encoding="utf-8" if isinstance(content, str) else None, + errors="ignore" if isinstance(content, str) else None, + ) as file_handler: + file_handler.write(content) + + # Create gz for .js files + if os.path.isfile(file_path): + if file_path.endswith(".js"): + with open(file_path, "rb") as f_in: + with gzip.open(file_path + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + # LEGACY! Remove with 2.0 + if "themes" in file_path and file_path.endswith(".yaml"): + filename = file_path.split("/")[-1] + base = file_path.split("/themes/")[0] + combined = f"{base}/themes/{filename}" + if os.path.exists(combined): + self.log.info("Removing old theme file %s", combined) + os.remove(combined) + + try: + await self.hass.async_add_executor_job(_write_file) + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as error: + self.log.error("Could not write data to %s - %s", file_path, error) + return False + + return await async_exists(self.hass, file_path) + + async def async_can_update(self) -> int: + """Helper to calculate the number of repositories we can fetch data for.""" + try: + response = await self.async_github_api_method(self.githubapi.rate_limit) + if ((limit := response.data.resources.core.remaining or 0) - 1000) >= 10: + return math.floor((limit - 1000) / 10) + reset = dt.as_local(dt.utc_from_timestamp(response.data.resources.core.reset)) + self.log.info( + "GitHub API ratelimited - %s remaining (%s)", + response.data.resources.core.remaining, + f"{reset.hour}:{reset.minute}:{reset.second}", + ) + self.disable_hacs(HacsDisabledReason.RATE_LIMIT) + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + self.log.exception(exception) + + return 0 + + async def async_github_api_method( + self, + method: Callable[[], Awaitable[TV]], + *args, + raise_exception: bool = True, + **kwargs, + ) -> TV | None: + """Call a GitHub API method""" + _exception = None + + try: + return await method(*args, **kwargs) + except GitHubAuthenticationException as exception: + self.disable_hacs(HacsDisabledReason.INVALID_TOKEN) + _exception = exception + except GitHubRatelimitException as exception: + self.disable_hacs(HacsDisabledReason.RATE_LIMIT) + _exception = exception + except GitHubNotModifiedException as exception: + raise exception + except GitHubException as exception: + _exception = exception + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + self.log.exception(exception) + _exception = exception + + if raise_exception and _exception is not None: + raise HacsException(_exception) + return None + + async def async_register_repository( + self, + repository_full_name: str, + category: HacsCategory, + *, + check: bool = True, + ref: str | None = None, + repository_id: str | None = None, + default: bool = False, + ) -> None: + """Register a repository.""" + if repository_full_name in self.common.skip: + if repository_full_name != HacsGitHubRepo.INTEGRATION: + raise HacsExpectedException(f"Skipping {repository_full_name}") + + if repository_full_name == "home-assistant/core": + raise HomeAssistantCoreRepositoryException() + + if repository_full_name == "home-assistant/addons" or repository_full_name.startswith( + "hassio-addons/" + ): + raise AddonRepositoryException() + + if category not in REPOSITORY_CLASSES: + self.log.warning( + "%s is not a valid repository category, %s will not be registered.", + category, + repository_full_name, + ) + return + + if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None: + repository_full_name = renamed + + repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name) + if check: + try: + await repository.async_registration(ref) + if repository.validate.errors: + self.common.skip.add(repository.data.full_name) + if not self.status.startup: + self.log.error("Validation for %s failed.", repository_full_name) + if self.system.action: + raise HacsException( + f"::error:: Validation for { + repository_full_name} failed." + ) + return repository.validate.errors + if self.system.action: + repository.logger.info("%s Validation completed", repository.string) + else: + repository.logger.info("%s Registration completed", repository.string) + except (HacsRepositoryExistException, HacsRepositoryArchivedException) as exception: + if self.system.generator: + repository.logger.error( + "%s Registration Failed - %s", repository.string, exception + ) + return + except AIOGitHubAPIException as exception: + self.common.skip.add(repository.data.full_name) + raise HacsException( + f"Validation for { + repository_full_name} failed with {exception}." + ) from exception + + if self.status.new: + repository.data.new = False + + if repository_id is not None: + repository.data.id = repository_id + + else: + if self.hass is not None and check and repository.data.new: + self.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "action": "registration", + "repository": repository.data.full_name, + "repository_id": repository.data.id, + }, + ) + + self.repositories.register(repository, default) + + async def startup_tasks(self, _=None) -> None: + """Tasks that are started after setup.""" + self.set_stage(HacsStage.STARTUP) + await self.async_load_hacs_from_github() + + if critical := await async_load_from_store(self.hass, "critical"): + for repo in critical: + if not repo["acknowledged"]: + self.log.critical("URGENT!: Check the HACS panel!") + async_create_persistent_notification( + self.hass, title="URGENT!", message="**Check the HACS panel!**" + ) + break + + self.recurring_tasks.append( + async_track_time_interval( + self.hass, + self.async_load_hacs_from_github, + timedelta(hours=48), + ) + ) + + self.recurring_tasks.append( + async_track_time_interval( + self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48) + ) + ) + + self.recurring_tasks.append( + async_track_time_interval( + self.hass, self.async_get_all_category_repositories, timedelta(hours=6) + ) + ) + + self.recurring_tasks.append( + async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5)) + ) + self.recurring_tasks.append( + async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10)) + ) + + self.recurring_tasks.append( + async_track_time_interval( + self.hass, self.async_handle_critical_repositories, timedelta(hours=6) + ) + ) + + unsub = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write + ) + if config_entry := self.configuration.config_entry: + config_entry.async_on_unload(unsub) + + self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks)) + + self.status.startup = False + self.async_dispatch(HacsDispatchEvent.STATUS, {}) + + await self.async_handle_removed_repositories() + await self.async_get_all_category_repositories() + + self.set_stage(HacsStage.RUNNING) + + self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True}) + + await self.async_handle_critical_repositories() + await self.async_process_queue() + + self.async_dispatch(HacsDispatchEvent.STATUS, {}) + + async def async_download_file( + self, + url: str, + *, + headers: dict | None = None, + keep_url: bool = False, + nolog: bool = False, + **_, + ) -> bytes | None: + """Download files, and return the content.""" + if url is None: + return None + + if not keep_url and "tags/" in url: + url = url.replace("tags/", "") + + self.log.debug("Trying to download %s", url) + timeouts = 0 + + while timeouts < 5: + try: + request = await self.session.get( + url=url, + timeout=ClientTimeout(total=60), + headers=headers, + ) + + # Make sure that we got a valid result + if request.status == 200: + return await request.read() + + raise HacsException( + f"Got status code { + request.status} when trying to download {url}" + ) + except TimeoutError: + self.log.warning( + "A timeout of 60! seconds was encountered while downloading %s, " + "using over 60 seconds to download a single file is not normal. " + "This is not a problem with HACS but how your host communicates with GitHub. " + "Retrying up to 5 times to mask/hide your host/network problems to " + "stop the flow of issues opened about it. " + "Tries left %s", + url, + (4 - timeouts), + ) + timeouts += 1 + await asyncio.sleep(1) + continue + + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + if not nolog: + self.log.exception("Download failed - %s", exception) + + return None + + async def async_recreate_entities(self) -> None: + """Recreate entities.""" + platforms = [Platform.UPDATE] + + # Workaround for core versions without https://github.com/home-assistant/core/pull/117084 + if self.core.ha_version < AwesomeVersion("2024.6.0"): + unload_platforms_lock = asyncio.Lock() + async with unload_platforms_lock: + on_unload = self.configuration.config_entry._on_unload + self.configuration.config_entry._on_unload = [] + await self.hass.config_entries.async_unload_platforms( + entry=self.configuration.config_entry, + platforms=platforms, + ) + self.configuration.config_entry._on_unload = on_unload + else: + await self.hass.config_entries.async_unload_platforms( + entry=self.configuration.config_entry, + platforms=platforms, + ) + await self.hass.config_entries.async_forward_entry_setups( + self.configuration.config_entry, platforms + ) + + @callback + def async_dispatch(self, signal: HacsDispatchEvent, data: dict | None = None) -> None: + """Dispatch a signal with data.""" + async_dispatcher_send(self.hass, signal, data) + + def set_active_categories(self) -> None: + """Set the active categories.""" + self.common.categories = set() + for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE): + self.enable_hacs_category(HacsCategory(category)) + + if ( + HacsCategory.PYTHON_SCRIPT in self.hass.config.components + or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT) + ): + self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT) + + if self.hass.services.has_service( + "frontend", "reload_themes" + ) or self.repositories.category_downloaded(HacsCategory.THEME): + self.enable_hacs_category(HacsCategory.THEME) + + if self.configuration.appdaemon: + self.enable_hacs_category(HacsCategory.APPDAEMON) + + async def async_load_hacs_from_github(self, _=None) -> None: + """Load HACS from GitHub.""" + if self.status.inital_fetch_done: + return + + try: + repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) + should_recreate_entities = False + if repository is None: + should_recreate_entities = True + await self.async_register_repository( + repository_full_name=HacsGitHubRepo.INTEGRATION, + category=HacsCategory.INTEGRATION, + default=True, + ) + repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION) + elif not self.status.startup: + self.log.error("Scheduling update of hacs/integration") + self.queue.add(repository.common_update()) + if repository is None: + raise HacsException("Unknown error") + + repository.data.installed = True + repository.data.installed_version = self.integration.version.string + repository.data.new = False + repository.data.releases = True + + if should_recreate_entities: + await self.async_recreate_entities() + + self.repository = repository.repository_object + self.repositories.mark_default(repository) + except HacsException as exception: + if "403" in str(exception): + self.log.critical( + "GitHub API is ratelimited, or the token is wrong.", + ) + else: + self.log.critical("Could not load HACS! - %s", exception) + self.disable_hacs(HacsDisabledReason.LOAD_HACS) + + async def async_get_all_category_repositories(self, _=None) -> None: + """Get all category repositories.""" + if self.system.disabled: + return + self.log.info("Loading known repositories") + await asyncio.gather( + *[ + self.async_get_category_repositories_experimental(category) + for category in self.common.categories or [] + ] + ) + + async def async_get_category_repositories_experimental(self, category: str) -> None: + """Update all category repositories.""" + self.log.debug("Fetching updated content for %s", category) + try: + category_data = await self.data_client.get_data(category, validate=True) + except HacsNotModifiedException: + self.log.debug("No updates for %s", category) + return + except HacsException as exception: + self.log.error("Could not update %s - %s", category, exception) + return + + await self.data.register_unknown_repositories(category_data, category) + + for repo_id, repo_data in category_data.items(): + repo_name = repo_data["full_name"] + if self.common.renamed_repositories.get(repo_name): + repo_name = self.common.renamed_repositories[repo_name] + if self.repositories.is_removed(repo_name): + continue + if repo_name in self.common.archived_repositories: + continue + if repository := self.repositories.get_by_full_name(repo_name): + self.repositories.set_repository_id(repository, repo_id) + self.repositories.mark_default(repository) + if repository.data.last_fetched is None or ( + repository.data.last_fetched.timestamp() < repo_data["last_fetched"] + ): + repository.data.update_data({**dict(REPOSITORY_KEYS_TO_EXPORT), **repo_data}) + if (manifest := repo_data.get("manifest")) is not None: + repository.repository_manifest.update_data( + {**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest} + ) + + if category == "integration": + self.status.inital_fetch_done = True + + if self.stage == HacsStage.STARTUP: + for repository in self.repositories.list_all: + if ( + repository.data.category == category + and not repository.data.installed + and not self.repositories.is_default(repository.data.id) + ): + repository.logger.debug( + "%s Unregister stale custom repository", repository.string + ) + self.repositories.unregister(repository) + + self.async_dispatch(HacsDispatchEvent.REPOSITORY, {}) + self.coordinators[category].async_update_listeners() + + async def async_check_rate_limit(self, _=None) -> None: + """Check rate limit.""" + if not self.system.disabled or self.system.disabled_reason != HacsDisabledReason.RATE_LIMIT: + return + + self.log.debug("Checking if ratelimit has lifted") + can_update = await self.async_can_update() + self.log.debug("Ratelimit indicate we can update %s", can_update) + if can_update > 0: + self.enable_hacs() + await self.async_process_queue() + + async def async_process_queue(self, _=None) -> None: + """Process the queue.""" + if self.system.disabled: + self.log.debug("HACS is disabled") + return + if not self.queue.has_pending_tasks: + self.log.debug("Nothing in the queue") + return + if self.queue.running: + self.log.debug("Queue is already running") + return + + async def _handle_queue(): + if not self.queue.has_pending_tasks: + await self.data.async_write() + return + can_update = await self.async_can_update() + self.log.debug( + "Can update %s repositories, items in queue %s", + can_update, + self.queue.pending_tasks, + ) + if can_update != 0: + try: + await self.queue.execute(can_update) + except HacsExecutionStillInProgress: + return + + await _handle_queue() + + await _handle_queue() + + async def async_handle_removed_repositories(self, _=None) -> None: + """Handle removed repositories.""" + if self.system.disabled: + return + need_to_save = False + self.log.info("Loading removed repositories") + + try: + removed_repositories = await self.data_client.get_data("removed", validate=True) + except HacsException: + return + + for item in removed_repositories: + removed = self.repositories.removed_repository(item["repository"]) + removed.update_data(item) + + for removed in self.repositories.list_removed: + if (repository := self.repositories.get_by_full_name(removed.repository)) is None: + continue + if repository.data.full_name in self.common.ignored_repositories: + continue + if repository.data.installed: + if removed.removal_type != "critical": + async_create_issue( + hass=self.hass, + domain=DOMAIN, + issue_id=f"removed_{repository.data.id}", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="removed", + translation_placeholders={ + "name": repository.data.full_name, + "reason": removed.reason, + "repositry_id": repository.data.id, + }, + ) + self.log.warning( + "You have '%s' installed with HACS " + "this repository has been removed from HACS, please consider removing it. " + "Removal reason (%s)", + repository.data.full_name, + removed.reason, + ) + else: + need_to_save = True + repository.remove() + + if need_to_save: + await self.data.async_write() + + async def async_update_downloaded_custom_repositories(self, _=None) -> None: + """Execute the task.""" + if self.system.disabled: + return + self.log.info("Starting recurring background task for downloaded custom repositories") + + repositories_to_update = 0 + repositories_updated = asyncio.Event() + + async def update_repository(repository: HacsRepository) -> None: + """Update a repository""" + nonlocal repositories_to_update + await repository.update_repository(ignore_issues=True) + repositories_to_update -= 1 + if not repositories_to_update: + repositories_updated.set() + + for repository in self.repositories.list_downloaded: + if ( + repository.data.category in self.common.categories + and not self.repositories.is_default(repository.data.id) + ): + repositories_to_update += 1 + self.queue.add(update_repository(repository)) + + async def update_coordinators() -> None: + """Update all coordinators.""" + await repositories_updated.wait() + for coordinator in self.coordinators.values(): + coordinator.async_update_listeners() + + if config_entry := self.configuration.config_entry: + config_entry.async_create_background_task( + self.hass, update_coordinators(), "update_coordinators" + ) + else: + self.hass.async_create_background_task(update_coordinators(), "update_coordinators") + + self.log.debug("Recurring background task for downloaded custom repositories done") + + async def async_handle_critical_repositories(self, _=None) -> None: + """Handle critical repositories.""" + critical_queue = QueueManager(hass=self.hass) + instored = [] + critical = [] + was_installed = False + + try: + critical = await self.data_client.get_data("critical", validate=True) + except (GitHubNotModifiedException, HacsNotModifiedException): + return + except HacsException: + pass + + if not critical: + self.log.debug("No critical repositories") + return + + stored_critical = await async_load_from_store(self.hass, "critical") + + for stored in stored_critical or []: + instored.append(stored["repository"]) + + stored_critical = [] + + for repository in critical: + removed_repo = self.repositories.removed_repository(repository["repository"]) + removed_repo.removal_type = "critical" + repo = self.repositories.get_by_full_name(repository["repository"]) + + stored = { + "repository": repository["repository"], + "reason": repository["reason"], + "link": repository["link"], + "acknowledged": True, + } + if repository["repository"] not in instored: + if repo is not None and repo.data.installed: + self.log.critical( + "Removing repository %s, it is marked as critical", + repository["repository"], + ) + was_installed = True + stored["acknowledged"] = False + # Remove from HACS + critical_queue.add(repo.uninstall()) + repo.remove() + + stored_critical.append(stored) + removed_repo.update_data(stored) + + # Uninstall + await critical_queue.execute() + + # Save to FS + await async_save_to_store(self.hass, "critical", stored_critical) + + # Restart HASS + if was_installed: + self.log.critical("Restarting Home Assistant") + self.hass.async_create_task(self.hass.async_stop(100)) + + async def async_setup_frontend_endpoint_plugin(self) -> None: + """Setup the http endpoints for plugins if its not already handled.""" + if self.status.active_frontend_endpoint_plugin or not await async_exists( + self.hass, self.hass.config.path("www/community") + ): + return + + self.log.info("Setting up plugin endpoint") + use_cache = self.core.lovelace_mode == "storage" + self.log.info( + " %s mode, cache for /hacsfiles/: %s", + self.core.lovelace_mode, + use_cache, + ) + + await async_register_static_path( + self.hass, + URL_BASE, + self.hass.config.path("www/community"), + cache_headers=use_cache, + ) + + self.status.active_frontend_endpoint_plugin = True diff --git a/homeassistant/config/custom_components/hacs/config_flow.py b/homeassistant/config/custom_components/hacs/config_flow.py new file mode 100644 index 0000000..ada9d11 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/config_flow.py @@ -0,0 +1,225 @@ +"""Adds config flow for HACS.""" + +from __future__ import annotations + +import asyncio +from contextlib import suppress +from typing import TYPE_CHECKING + +from aiogithubapi import ( + GitHubDeviceAPI, + GitHubException, + GitHubLoginDeviceModel, + GitHubLoginOauthModel, +) +from aiogithubapi.common.const import OAUTH_USER_LOGIN +from awesomeversion import AwesomeVersion +from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.const import __version__ as HAVERSION +from homeassistant.core import callback +from homeassistant.data_entry_flow import UnknownFlow +from homeassistant.helpers import aiohttp_client +from homeassistant.loader import async_get_integration +import voluptuous as vol + +from .base import HacsBase +from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION +from .utils.configuration_schema import ( + APPDAEMON, + COUNTRY, + SIDEPANEL_ICON, + SIDEPANEL_TITLE, +) +from .utils.logger import LOGGER + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + +class HacsFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for HACS.""" + + VERSION = 1 + + hass: HomeAssistant + activation_task: asyncio.Task | None = None + device: GitHubDeviceAPI | None = None + + _registration: GitHubLoginDeviceModel | None = None + _activation: GitHubLoginOauthModel | None = None + _reauth: bool = False + + def __init__(self) -> None: + """Initialize.""" + self._errors = {} + self._user_input = {} + + async def async_step_user(self, user_input): + """Handle a flow initialized by the user.""" + self._errors = {} + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if self.hass.data.get(DOMAIN): + return self.async_abort(reason="single_instance_allowed") + + if user_input: + if [x for x in user_input if x.startswith("acc_") and not user_input[x]]: + self._errors["base"] = "acc" + return await self._show_config_form(user_input) + + self._user_input = user_input + + return await self.async_step_device(user_input) + + # Initial form + return await self._show_config_form(user_input) + + async def async_step_device(self, _user_input): + """Handle device steps.""" + + async def _wait_for_activation() -> None: + try: + response = await self.device.activation(device_code=self._registration.device_code) + self._activation = response.data + finally: + + async def _progress(): + with suppress(UnknownFlow): + await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + + if not self.device: + integration = await async_get_integration(self.hass, DOMAIN) + self.device = GitHubDeviceAPI( + client_id=CLIENT_ID, + session=aiohttp_client.async_get_clientsession(self.hass), + **{"client_name": f"HACS/{integration.version}"}, + ) + try: + response = await self.device.register() + self._registration = response.data + except GitHubException as exception: + LOGGER.exception(exception) + return self.async_abort(reason="could_not_register") + + if self.activation_task is None: + self.activation_task = self.hass.async_create_task(_wait_for_activation()) + + if self.activation_task.done(): + if (exception := self.activation_task.exception()) is not None: + LOGGER.exception(exception) + return self.async_show_progress_done(next_step_id="could_not_register") + return self.async_show_progress_done(next_step_id="device_done") + + show_progress_kwargs = { + "step_id": "device", + "progress_action": "wait_for_device", + "description_placeholders": { + "url": OAUTH_USER_LOGIN, + "code": self._registration.user_code, + }, + "progress_task": self.activation_task, + } + return self.async_show_progress(**show_progress_kwargs) + + async def _show_config_form(self, user_input): + """Show the configuration form to edit location data.""" + + if not user_input: + user_input = {} + + if AwesomeVersion(HAVERSION) < MINIMUM_HA_VERSION: + return self.async_abort( + reason="min_ha_version", + description_placeholders={"version": MINIMUM_HA_VERSION}, + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("acc_logs", default=user_input.get("acc_logs", False)): bool, + vol.Required("acc_addons", default=user_input.get("acc_addons", False)): bool, + vol.Required( + "acc_untested", default=user_input.get("acc_untested", False) + ): bool, + vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool, + } + ), + errors=self._errors, + ) + + async def async_step_device_done(self, user_input: dict[str, bool] | None = None): + """Handle device steps""" + if self._reauth: + existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self.hass.config_entries.async_update_entry( + existing_entry, data={**existing_entry.data, "token": self._activation.access_token} + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title="", + data={ + "token": self._activation.access_token, + }, + options={ + "experimental": True, + }, + ) + + async def async_step_could_not_register(self, _user_input=None): + """Handle issues that need transition await from progress step.""" + return self.async_abort(reason="could_not_register") + + async def async_step_reauth(self, _user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + self._reauth = True + return await self.async_step_device(None) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + return HacsOptionsFlowHandler(config_entry) + + +class HacsOptionsFlowHandler(OptionsFlow): + """HACS config flow options handler.""" + + def __init__(self, config_entry): + """Initialize HACS options flow.""" + if AwesomeVersion(HAVERSION) < "2024.11.99": + self.config_entry = config_entry + + async def async_step_init(self, _user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + hacs: HacsBase = self.hass.data.get(DOMAIN) + if user_input is not None: + return self.async_create_entry(title="", data={**user_input, "experimental": True}) + + if hacs is None or hacs.configuration is None: + return self.async_abort(reason="not_setup") + + if hacs.queue.has_pending_tasks: + return self.async_abort(reason="pending_tasks") + + schema = { + vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str, + vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str, + vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE), + vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool, + } + + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) diff --git a/homeassistant/config/custom_components/hacs/const.py b/homeassistant/config/custom_components/hacs/const.py new file mode 100644 index 0000000..271484b --- /dev/null +++ b/homeassistant/config/custom_components/hacs/const.py @@ -0,0 +1,294 @@ +"""Constants for HACS""" + +from typing import TypeVar + +from aiogithubapi.common.const import ACCEPT_HEADERS + +NAME_SHORT = "HACS" +DOMAIN = "hacs" +CLIENT_ID = "395a8e669c5de9f7c6e8" +MINIMUM_HA_VERSION = "2024.4.1" + +URL_BASE = "/hacsfiles" + +TV = TypeVar("TV") + +PACKAGE_NAME = "custom_components.hacs" + +DEFAULT_CONCURRENT_TASKS = 15 +DEFAULT_CONCURRENT_BACKOFF_TIME = 1 + +HACS_REPOSITORY_ID = "172733314" + +HACS_ACTION_GITHUB_API_HEADERS = { + "User-Agent": "HACS/action", + "Accept": ACCEPT_HEADERS["preview"], +} + +VERSION_STORAGE = "6" +STORENAME = "hacs" + +HACS_SYSTEM_ID = "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd" + +STARTUP = """ +------------------------------------------------------------------- +HACS (Home Assistant Community Store) + +Version: %s +This is a custom integration +If you have any issues with this you need to open an issue here: +https://github.com/hacs/integration/issues +------------------------------------------------------------------- +""" + +LOCALE = [ + "ALL", + "AF", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "HR", + "CU", + "CW", + "CY", + "CZ", + "CI", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KP", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "MX", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RO", + "RU", + "RW", + "RE", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SX", + "SK", + "SI", + "SB", + "SO", + "ZA", + "GS", + "SS", + "ES", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "UG", + "UA", + "AE", + "GB", + "US", + "UM", + "UY", + "UZ", + "VU", + "VE", + "VN", + "VG", + "VI", + "WF", + "EH", + "YE", + "ZM", + "ZW", +] diff --git a/homeassistant/config/custom_components/hacs/coordinator.py b/homeassistant/config/custom_components/hacs/coordinator.py new file mode 100644 index 0000000..03cf891 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/coordinator.py @@ -0,0 +1,38 @@ +"""Coordinator to trigger entity updates.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol + + +class HacsUpdateCoordinator(BaseDataUpdateCoordinatorProtocol): + """Dispatch updates to update entities.""" + + def __init__(self) -> None: + """Initialize.""" + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.pop(remove_listener) + + self._listeners[remove_listener] = (update_callback, context) + + return remove_listener + + @callback + def async_update_listeners(self) -> None: + """Update all registered listeners.""" + for update_callback, _ in list(self._listeners.values()): + update_callback() diff --git a/homeassistant/config/custom_components/hacs/data_client.py b/homeassistant/config/custom_components/hacs/data_client.py new file mode 100644 index 0000000..bcd7ff3 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/data_client.py @@ -0,0 +1,98 @@ +"""HACS Data client.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp import ClientSession, ClientTimeout +import voluptuous as vol + +from .exceptions import HacsException, HacsNotModifiedException +from .utils.logger import LOGGER +from .utils.validate import ( + VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA, + VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA, + VALIDATE_FETCHED_V2_REPO_DATA, +) + +CRITICAL_REMOVED_VALIDATORS = { + "critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA, + "removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA, +} + + +class HacsDataClient: + """HACS Data client.""" + + def __init__(self, session: ClientSession, client_name: str) -> None: + """Initialize.""" + self._client_name = client_name + self._etags = {} + self._session = session + + async def _do_request( + self, + filename: str, + section: str | None = None, + ) -> dict[str, dict[str, Any]] | list[str]: + """Do request.""" + endpoint = "/".join([v for v in [section, filename] if v is not None]) + try: + response = await self._session.get( + f"https://data-v2.hacs.xyz/{endpoint}", + timeout=ClientTimeout(total=60), + headers={ + "User-Agent": self._client_name, + "If-None-Match": self._etags.get(endpoint, ""), + }, + ) + if response.status == 304: + raise HacsNotModifiedException() from None + response.raise_for_status() + except HacsNotModifiedException: + raise + except TimeoutError: + raise HacsException("Timeout of 60s reached") from None + except Exception as exception: + raise HacsException(f"Error fetching data from HACS: {exception}") from exception + + self._etags[endpoint] = response.headers.get("etag") + + return await response.json() + + async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]: + """Get data.""" + data = await self._do_request(filename="data.json", section=section) + if not validate: + return data + + if section in VALIDATE_FETCHED_V2_REPO_DATA: + validated = {} + for key, repo_data in data.items(): + try: + validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data) + except vol.Invalid as exception: + LOGGER.info( + "Got invalid data for %s (%s)", repo_data.get("full_name", key), exception + ) + continue + + return validated + + if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)): + raise ValueError(f"Do not know how to validate {section}") + + validated = [] + for repo_data in data: + try: + validated.append(validator(repo_data)) + except vol.Invalid as exception: + LOGGER.info("Got invalid data for %s (%s)", section, exception) + continue + + return validated + + async def get_repositories(self, section: str) -> list[str]: + """Get repositories.""" + return await self._do_request(filename="repositories.json", section=section) diff --git a/homeassistant/config/custom_components/hacs/diagnostics.py b/homeassistant/config/custom_components/hacs/diagnostics.py new file mode 100644 index 0000000..fb88fa2 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/diagnostics.py @@ -0,0 +1,80 @@ +"""Diagnostics support for HACS.""" + +from __future__ import annotations + +from typing import Any + +from aiogithubapi import GitHubException +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .base import HacsBase +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + hacs: HacsBase = hass.data[DOMAIN] + + data = { + "entry": entry.as_dict(), + "hacs": { + "stage": hacs.stage, + "version": hacs.version, + "disabled_reason": hacs.system.disabled_reason, + "new": hacs.status.new, + "startup": hacs.status.startup, + "categories": hacs.common.categories, + "renamed_repositories": hacs.common.renamed_repositories, + "archived_repositories": hacs.common.archived_repositories, + "ignored_repositories": hacs.common.ignored_repositories, + "lovelace_mode": hacs.core.lovelace_mode, + "configuration": {}, + }, + "custom_repositories": [ + repo.data.full_name + for repo in hacs.repositories.list_all + if not hacs.repositories.is_default(str(repo.data.id)) + ], + "repositories": [], + } + + for key in ( + "appdaemon", + "country", + "debug", + "dev", + "python_script", + "release_limit", + "theme", + ): + data["hacs"]["configuration"][key] = getattr(hacs.configuration, key, None) + + for repository in hacs.repositories.list_downloaded: + data["repositories"].append( + { + "data": repository.data.to_json(), + "integration_manifest": repository.integration_manifest, + "repository_manifest": repository.repository_manifest.to_dict(), + "ref": repository.ref, + "paths": { + "localpath": repository.localpath.replace(hacs.core.config_path, "/config"), + "local": repository.content.path.local.replace( + hacs.core.config_path, "/config" + ), + "remote": repository.content.path.remote, + }, + } + ) + + try: + rate_limit_response = await hacs.githubapi.rate_limit() + data["rate_limit"] = rate_limit_response.data.as_dict + except GitHubException as exception: + data["rate_limit"] = str(exception) + + return async_redact_data(data, ("token",)) diff --git a/homeassistant/config/custom_components/hacs/entity.py b/homeassistant/config/custom_components/hacs/entity.py new file mode 100644 index 0000000..6d5d2b8 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/entity.py @@ -0,0 +1,143 @@ +"""HACS Base entities.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity + +from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT +from .coordinator import HacsUpdateCoordinator +from .enums import HacsDispatchEvent, HacsGitHubRepo + +if TYPE_CHECKING: + from .base import HacsBase + from .repositories.base import HacsRepository + + +def system_info(hacs: HacsBase) -> dict: + """Return system info.""" + return { + "identifiers": {(DOMAIN, HACS_SYSTEM_ID)}, + "name": NAME_SHORT, + "manufacturer": "hacs.xyz", + "model": "", + "sw_version": str(hacs.version), + "configuration_url": "homeassistant://hacs", + "entry_type": DeviceEntryType.SERVICE, + } + + +class HacsBaseEntity(Entity): + """Base HACS entity.""" + + repository: HacsRepository | None = None + _attr_should_poll = False + + def __init__(self, hacs: HacsBase) -> None: + """Initialize.""" + self.hacs = hacs + + +class HacsDispatcherEntity(HacsBaseEntity): + """Base HACS entity listening to dispatcher signals.""" + + async def async_added_to_hass(self) -> None: + """Register for status events.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + HacsDispatchEvent.REPOSITORY, + self._update_and_write_state, + ) + ) + + @callback + def _update(self) -> None: + """Update the sensor.""" + + async def async_update(self) -> None: + """Manual updates of the sensor.""" + self._update() + + @callback + def _update_and_write_state(self, _: Any) -> None: + """Update the entity and write state.""" + self._update() + self.async_write_ha_state() + + +class HacsSystemEntity(HacsDispatcherEntity): + """Base system entity.""" + + _attr_icon = "hacs:hacs" + _attr_unique_id = HACS_SYSTEM_ID + + @property + def device_info(self) -> dict[str, any]: + """Return device information about HACS.""" + return system_info(self.hacs) + + +class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity): + """Base repository entity.""" + + def __init__( + self, + hacs: HacsBase, + repository: HacsRepository, + ) -> None: + """Initialize.""" + BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category]) + HacsBaseEntity.__init__(self, hacs=hacs) + self.repository = repository + self._attr_unique_id = str(repository.data.id) + self._repo_last_fetched = repository.data.last_fetched + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.hacs.repositories.is_downloaded(repository_id=str(self.repository.data.id)) + + @property + def device_info(self) -> dict[str, any]: + """Return device information about HACS.""" + if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION: + return system_info(self.hacs) + + def _manufacturer(): + if authors := self.repository.data.authors: + return ", ".join(author.replace("@", "") for author in authors) + return self.repository.data.full_name.split("/")[0] + + return { + "identifiers": {(DOMAIN, str(self.repository.data.id))}, + "name": self.repository.display_name, + "model": self.repository.data.category, + "manufacturer": _manufacturer(), + "configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}", + "entry_type": DeviceEntryType.SERVICE, + } + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if ( + self._repo_last_fetched is not None + and self.repository.data.last_fetched is not None + and self._repo_last_fetched >= self.repository.data.last_fetched + ): + return + + self._repo_last_fetched = self.repository.data.last_fetched + self.async_write_ha_state() + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ diff --git a/homeassistant/config/custom_components/hacs/enums.py b/homeassistant/config/custom_components/hacs/enums.py new file mode 100644 index 0000000..dd947e1 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/enums.py @@ -0,0 +1,71 @@ +"""Helper constants.""" + +# pylint: disable=missing-class-docstring +from enum import StrEnum + + +class HacsGitHubRepo(StrEnum): + """HacsGitHubRepo.""" + + DEFAULT = "hacs/default" + INTEGRATION = "hacs/integration" + + +class HacsCategory(StrEnum): + APPDAEMON = "appdaemon" + INTEGRATION = "integration" + LOVELACE = "lovelace" + PLUGIN = "plugin" # Kept for legacy purposes + PYTHON_SCRIPT = "python_script" + TEMPLATE = "template" + THEME = "theme" + REMOVED = "removed" + + def __str__(self): + return str(self.value) + + +class HacsDispatchEvent(StrEnum): + """HacsDispatchEvent.""" + + CONFIG = "hacs_dispatch_config" + ERROR = "hacs_dispatch_error" + RELOAD = "hacs_dispatch_reload" + REPOSITORY = "hacs_dispatch_repository" + REPOSITORY_DOWNLOAD_PROGRESS = "hacs_dispatch_repository_download_progress" + STAGE = "hacs_dispatch_stage" + STARTUP = "hacs_dispatch_startup" + STATUS = "hacs_dispatch_status" + + +class RepositoryFile(StrEnum): + """Repository file names.""" + + HACS_JSON = "hacs.json" + MAINIFEST_JSON = "manifest.json" + + +class LovelaceMode(StrEnum): + """Lovelace Modes.""" + + STORAGE = "storage" + AUTO = "auto" + AUTO_GEN = "auto-gen" + YAML = "yaml" + + +class HacsStage(StrEnum): + SETUP = "setup" + STARTUP = "startup" + WAITING = "waiting" + RUNNING = "running" + BACKGROUND = "background" + + +class HacsDisabledReason(StrEnum): + RATE_LIMIT = "rate_limit" + REMOVED = "removed" + INVALID_TOKEN = "invalid_token" + CONSTRAINS = "constrains" + LOAD_HACS = "load_hacs" + RESTORE = "restore" diff --git a/homeassistant/config/custom_components/hacs/exceptions.py b/homeassistant/config/custom_components/hacs/exceptions.py new file mode 100644 index 0000000..f852b3c --- /dev/null +++ b/homeassistant/config/custom_components/hacs/exceptions.py @@ -0,0 +1,49 @@ +"""Custom Exceptions for HACS.""" + + +class HacsException(Exception): + """Super basic.""" + + +class HacsRepositoryArchivedException(HacsException): + """For repositories that are archived.""" + + +class HacsNotModifiedException(HacsException): + """For responses that are not modified.""" + + +class HacsExpectedException(HacsException): + """For stuff that are expected.""" + + +class HacsRepositoryExistException(HacsException): + """For repositories that are already exist.""" + + +class HacsExecutionStillInProgress(HacsException): + """Exception to raise if execution is still in progress.""" + + +class AddonRepositoryException(HacsException): + """Exception to raise when user tries to add add-on repository.""" + + exception_message = ( + "The repository does not seem to be a integration, " + "but an add-on repository. HACS does not manage add-ons." + ) + + def __init__(self) -> None: + super().__init__(self.exception_message) + + +class HomeAssistantCoreRepositoryException(HacsException): + """Exception to raise when user tries to add the home-assistant/core repository.""" + + exception_message = ( + "You can not add homeassistant/core, to use core integrations " + "check the Home Assistant documentation for how to add them." + ) + + def __init__(self) -> None: + super().__init__(self.exception_message) diff --git a/homeassistant/config/custom_components/hacs/frontend.py b/homeassistant/config/custom_components/hacs/frontend.py new file mode 100644 index 0000000..7038f3c --- /dev/null +++ b/homeassistant/config/custom_components/hacs/frontend.py @@ -0,0 +1,67 @@ +"""Starting setup task: Frontend.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from homeassistant.components.frontend import ( + add_extra_js_url, + async_register_built_in_panel, +) + +from .const import DOMAIN, URL_BASE +from .hacs_frontend import VERSION as FE_VERSION, locate_dir +from .utils.workarounds import async_register_static_path + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from .base import HacsBase + + +async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None: + """Register the frontend.""" + + # Register frontend + if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")): + hacs.log.warning( + " Frontend development mode enabled. Do not run in production!" + ) + await async_register_static_path( + hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False + ) + hacs.frontend_version = "dev" + else: + await async_register_static_path( + hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False + ) + hacs.frontend_version = FE_VERSION + + # Custom iconset + await async_register_static_path( + hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js") + ) + add_extra_js_url(hass, f"{URL_BASE}/iconset.js") + + # Add to sidepanel if needed + if DOMAIN not in hass.data.get("frontend_panels", {}): + async_register_built_in_panel( + hass, + component_name="custom", + sidebar_title=hacs.configuration.sidepanel_title, + sidebar_icon=hacs.configuration.sidepanel_icon, + frontend_url_path=DOMAIN, + config={ + "_panel_custom": { + "name": "hacs-frontend", + "embed_iframe": True, + "trust_external": False, + "js_url": f"/hacsfiles/frontend/entrypoint.js?hacstag={hacs.frontend_version}", + } + }, + require_admin=True, + ) + + # Setup plugin endpoint if needed + await hacs.async_setup_frontend_endpoint_plugin() diff --git a/homeassistant/config/custom_components/hacs/icons.json b/homeassistant/config/custom_components/hacs/icons.json new file mode 100644 index 0000000..a1ae544 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "switch": { + "pre-release": { + "state": { + "on": "mdi:test-tube", + "off": "mdi:test-tube-off" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/hacs/iconset.js b/homeassistant/config/custom_components/hacs/iconset.js new file mode 100644 index 0000000..02fe919 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/iconset.js @@ -0,0 +1,21 @@ +const hacsIcons = { + hacs: { + path: "m 20.064849,22.306912 c -0.0319,0.369835 -0.280561,0.707789 -0.656773,0.918212 -0.280572,0.153036 -0.605773,0.229553 -0.950094,0.229553 -0.0765,0 -0.146661,-0.0064 -0.216801,-0.01275 -0.605774,-0.05739 -1.135016,-0.344329 -1.402827,-0.7588 l 0.784304,-0.516495 c 0.0893,0.146659 0.344331,0.312448 0.707793,0.34433 0.235931,0.02551 0.471852,-0.01913 0.637643,-0.108401 0.101998,-0.05101 0.172171,-0.127529 0.17854,-0.191295 0.0065,-0.08289 -0.0255,-0.369835 -0.733293,-0.439975 -1.013854,-0.09565 -1.645127,-0.688661 -1.568606,-1.460214 0.0319,-0.382589 0.280561,-0.714165 0.663153,-0.930965 0.331571,-0.172165 0.752423,-0.25506 1.166895,-0.210424 0.599382,0.05739 1.128635,0.344329 1.402816,0.7588 l -0.784304,0.510118 c -0.0893,-0.140282 -0.344331,-0.299694 -0.707782,-0.331576 -0.235932,-0.02551 -0.471863,0.01913 -0.637654,0.10202 -0.0956,0.05739 -0.165791,0.133906 -0.17216,0.191295 -0.0255,0.293317 0.465482,0.420847 0.726913,0.439976 v 0.0064 c 1.020234,0.09565 1.638757,0.66953 1.562237,1.460213 z m -7.466854,-0.988354 c 0,-1.192401 0.962855,-2.155249 2.15525,-2.155249 0.599393,0 1.179645,0.25506 1.594117,0.707789 l -0.695033,0.624895 c -0.235931,-0.25506 -0.561133,-0.401718 -0.899084,-0.401718 -0.675903,0 -1.217906,0.542 -1.217906,1.217906 0,0.66953 0.542003,1.217908 1.217906,1.217908 0.337951,0 0.663153,-0.140283 0.899084,-0.401718 l 0.695033,0.631271 c -0.414472,0.452729 -0.988355,0.707788 -1.594117,0.707788 -1.192395,0 -2.15525,-0.969224 -2.15525,-2.148872 z M 8.6573365,23.461054 10.353474,19.14418 h 0.624893 l 1.568618,4.316874 H 11.52037 L 11.265308,22.734136 H 9.964513 l -0.274192,0.726918 z m 1.6833885,-1.68339 h 0.580263 L 10.646796,21.012487 Z M 8.1089536,19.156932 v 4.297745 H 7.1461095 v -1.645131 h -1.606867 v 1.645131 H 4.5763876 v -4.297745 h 0.9628549 v 1.696143 h 1.606867 V 19.156932 Z M 20.115859,4.2997436 C 20.090359,4.159461 19.969198,4.0574375 19.822548,4.0574375 H 14.141102 10.506516 4.8250686 c -0.14665,0 -0.2678112,0.1020202 -0.2933108,0.2423061 L 3.690064,8.8461703 c -0.00651,0.01913 -0.00651,0.03826 -0.00651,0.057391 v 1.5239797 c 0,0.165789 0.133911,0.299694 0.2996911,0.299694 H 4.5762579 20.0711 20.664112 c 0.165781,0 0.299691,-0.133905 0.299691,-0.299694 V 8.8971848 c 0,-0.01913 0,-0.03826 -0.0065,-0.05739 z M 4.5763876,17.358767 c 0,0.184917 0.1466608,0.331577 0.3315819,0.331577 h 5.5985465 3.634586 0.924594 c 0.184911,0 0.331571,-0.14666 0.331571,-0.331577 v -4.744098 c 0,-0.184918 0.146661,-0.331577 0.331582,-0.331577 h 2.894913 c 0.184921,0 0.331582,0.146659 0.331582,0.331577 v 4.744098 c 0,0.184917 0.146661,0.331577 0.331571,0.331577 h 0.446363 c 0.18491,0 0.331571,-0.14666 0.331571,-0.331577 v -5.636804 c 0,-0.184918 -0.146661,-0.331577 -0.331571,-0.331577 H 4.9079695 c -0.1849211,0 -0.3315819,0.146659 -0.3315819,0.331577 z m 1.6578879,-4.852498 h 5.6495565 c 0.15303,0 0.280561,0.12753 0.280561,0.280564 v 3.513438 c 0,0.153036 -0.127531,0.280566 -0.280561,0.280566 H 6.2342755 c -0.1530412,0 -0.2805719,-0.12753 -0.2805719,-0.280566 v -3.513438 c 0,-0.159411 0.1275307,-0.280564 0.2805719,-0.280564 z M 19.790657,3.3879075 H 4.8569594 c -0.1530412,0 -0.2805718,-0.1275296 -0.2805718,-0.2805642 V 1.3665653 C 4.5763876,1.2135296 4.7039182,1.086 4.8569594,1.086 H 19.790657 c 0.153041,0 0.280572,0.1275296 0.280572,0.2805653 v 1.740778 c 0,0.1530346 -0.127531,0.2805642 -0.280572,0.2805642 z", + keywords: ["hacs", "home assistant community store"], + }, +}; + +window.customIcons = window.customIcons || {}; +window.customIconsets = window.customIconsets || {}; + +window.customIcons["hacs"] = { + getIcon: async (iconName) => ( + { path: hacsIcons[iconName]?.path } + ), + getIconList: async () => + Object.entries(hacsIcons).map(([icon, content]) => ({ + name: icon, + keywords: content.keywords, + }) + ) +}; diff --git a/homeassistant/config/custom_components/hacs/manifest.json b/homeassistant/config/custom_components/hacs/manifest.json new file mode 100644 index 0000000..1ca2f24 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/manifest.json @@ -0,0 +1,26 @@ +{ + "domain": "hacs", + "name": "HACS", + "after_dependencies": [ + "python_script" + ], + "codeowners": [ + "@ludeeus" + ], + "config_flow": true, + "dependencies": [ + "http", + "websocket_api", + "frontend", + "persistent_notification", + "lovelace", + "repairs" + ], + "documentation": "https://hacs.xyz/docs/use/", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/hacs/integration/issues", + "requirements": [ + "aiogithubapi>=22.10.1" + ], + "version": "2.0.5" +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/hacs/repairs.py b/homeassistant/config/custom_components/hacs/repairs.py new file mode 100644 index 0000000..0b9ecb8 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repairs.py @@ -0,0 +1,58 @@ +"""Repairs platform for HACS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +import voluptuous as vol + +from custom_components.hacs.base import HacsBase + +from .const import DOMAIN + + +class RestartRequiredFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, issue_id: str) -> None: + self.issue_id = issue_id + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await self.async_step_confirm_restart() + + async def async_step_confirm_restart( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + await self.hass.services.async_call("homeassistant", "restart") + return self.async_create_entry(title="", data={}) + + hacs: HacsBase = self.hass.data[DOMAIN] + integration = hacs.repositories.get_by_id(self.issue_id.split("_")[2]) + + return self.async_show_form( + step_id="confirm_restart", + data_schema=vol.Schema({}), + description_placeholders={"name": integration.display_name}, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None = None, + *args: Any, + **kwargs: Any, +) -> RepairsFlow | None: + """Create flow.""" + if issue_id.startswith("restart_required"): + return RestartRequiredFixFlow(issue_id) + return None diff --git a/homeassistant/config/custom_components/hacs/repositories/__init__.py b/homeassistant/config/custom_components/hacs/repositories/__init__.py new file mode 100644 index 0000000..e19ca33 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/__init__.py @@ -0,0 +1,21 @@ +"""Initialize repositories.""" + +from __future__ import annotations + +from ..enums import HacsCategory +from .appdaemon import HacsAppdaemonRepository +from .base import HacsRepository +from .integration import HacsIntegrationRepository +from .plugin import HacsPluginRepository +from .python_script import HacsPythonScriptRepository +from .template import HacsTemplateRepository +from .theme import HacsThemeRepository + +REPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = { + HacsCategory.THEME: HacsThemeRepository, + HacsCategory.INTEGRATION: HacsIntegrationRepository, + HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository, + HacsCategory.APPDAEMON: HacsAppdaemonRepository, + HacsCategory.PLUGIN: HacsPluginRepository, + HacsCategory.TEMPLATE: HacsTemplateRepository, +} diff --git a/homeassistant/config/custom_components/hacs/repositories/appdaemon.py b/homeassistant/config/custom_components/hacs/repositories/appdaemon.py new file mode 100644 index 0000000..f25aa1d --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/appdaemon.py @@ -0,0 +1,93 @@ +"""Class for appdaemon apps in HACS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aiogithubapi import AIOGitHubAPIException + +from ..enums import HacsCategory, HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.decorator import concurrent +from .base import HacsRepository + +if TYPE_CHECKING: + from ..base import HacsBase + + +class HacsAppdaemonRepository(HacsRepository): + """Appdaemon apps in HACS.""" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.APPDAEMON + self.content.path.local = self.localpath + self.content.path.remote = "apps" + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.name}" + + async def validate_repository(self): + """Validate.""" + await self.common_validate() + + # Custom step 1: Validate content. + try: + addir = await self.repository_object.get_contents("apps", self.ref) + except AIOGitHubAPIException: + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) from None + + if not isinstance(addir, list): + self.validate.errors.append(f"{self.string} Repository structure not compliant") + + self.content.path.remote = addir[0].path + self.content.objects = await self.repository_object.get_contents( + self.content.path.remote, self.ref + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + # Get appdaemon objects. + if self.repository_manifest: + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "apps": + addir = await self.repository_object.get_contents(self.content.path.remote, self.ref) + self.content.path.remote = addir[0].path + self.content.objects = await self.repository_object.get_contents( + self.content.path.remote, self.ref + ) + + # Set local path + self.content.path.local = self.localpath + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) diff --git a/homeassistant/config/custom_components/hacs/repositories/base.py b/homeassistant/config/custom_components/hacs/repositories/base.py new file mode 100644 index 0000000..c5470d7 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/base.py @@ -0,0 +1,1454 @@ +"""Repository.""" + +from __future__ import annotations + +from asyncio import sleep +from datetime import UTC, datetime +import os +import pathlib +import shutil +import tempfile +from typing import TYPE_CHECKING, Any +import zipfile + +from aiogithubapi import ( + AIOGitHubAPIException, + AIOGitHubAPINotModifiedException, + GitHubReleaseModel, +) +from aiogithubapi.objects.repository import AIOGitHubAPIRepository +import attr +from homeassistant.helpers import device_registry as dr, issue_registry as ir + +from ..const import DOMAIN +from ..enums import HacsDispatchEvent, RepositoryFile +from ..exceptions import ( + HacsException, + HacsNotModifiedException, + HacsRepositoryArchivedException, + HacsRepositoryExistException, +) +from ..types import DownloadableContent +from ..utils.backup import Backup +from ..utils.decode import decode_content +from ..utils.decorator import concurrent +from ..utils.file_system import async_exists, async_remove, async_remove_directory +from ..utils.filters import filter_content_return_one_of_type +from ..utils.github_graphql_query import GET_REPOSITORY_RELEASES +from ..utils.json import json_loads +from ..utils.logger import LOGGER +from ..utils.path import is_safe +from ..utils.queue_manager import QueueManager +from ..utils.store import async_remove_store +from ..utils.url import github_archive, github_release_asset +from ..utils.validate import Validate +from ..utils.version import ( + version_left_higher_or_equal_then_right, + version_left_higher_then_right, +) +from ..utils.workarounds import DOMAIN_OVERRIDES + +if TYPE_CHECKING: + from ..base import HacsBase + + +TOPIC_FILTER = ( + "add-on", + "addon", + "app", + "appdaemon-apps", + "appdaemon", + "custom-card", + "custom-cards", + "custom-component", + "custom-components", + "customcomponents", + "hacktoberfest", + "hacs-default", + "hacs-integration", + "hacs-repository", + "hacs", + "hass", + "hassio", + "home-assistant-custom", + "home-assistant-frontend", + "home-assistant-hacs", + "home-assistant-sensor", + "home-assistant", + "home-automation", + "homeassistant-components", + "homeassistant-integration", + "homeassistant-sensor", + "homeassistant", + "homeautomation", + "integration", + "lovelace-ui", + "lovelace", + "media-player", + "mediaplayer", + "plugin", + "python_script", + "python-script", + "python", + "sensor", + "smart-home", + "smarthome", + "template", + "templates", + "theme", + "themes", +) + + +REPOSITORY_KEYS_TO_EXPORT = ( + # Keys can not be removed from this list until v3 + # If keys are added, the action need to be re-run with force + ("description", ""), + ("downloads", 0), + ("domain", None), + ("etag_releases", None), + ("etag_repository", None), + ("full_name", ""), + ("last_commit", None), + ("last_updated", 0), + ("last_version", None), + ("manifest_name", None), + ("open_issues", 0), + ("prerelease", None), + ("stargazers_count", 0), + ("topics", []), +) + +HACS_MANIFEST_KEYS_TO_EXPORT = ( + # Keys can not be removed from this list until v3 + # If keys are added, the action need to be re-run with force + ("country", []), + ("name", None), +) + + +class FileInformation: + """FileInformation.""" + + def __init__(self, url, path, name): + self.download_url = url + self.path = path + self.name = name + + +@attr.s(auto_attribs=True) +class RepositoryData: + """RepositoryData class.""" + + archived: bool = False + authors: list[str] = [] + category: str = "" + config_flow: bool = False + default_branch: str = None + description: str = "" + domain: str = None + downloads: int = 0 + etag_repository: str = None + etag_releases: str = None + file_name: str = "" + first_install: bool = False + full_name: str = "" + hide: bool = False + has_issues: bool = True + id: int = 0 + installed_commit: str = None + installed_version: str = None + installed: bool = False + last_commit: str = None + last_fetched: datetime = None + last_updated: str = 0 + last_version: str = None + manifest_name: str = None + new: bool = True + open_issues: int = 0 + prerelease: str = None + published_tags: list[str] = [] + releases: bool = False + selected_tag: str = None + show_beta: bool = False + stargazers_count: int = 0 + topics: list[str] = [] + + @property + def name(self): + """Return the name.""" + if self.category == "integration": + return self.domain + return self.full_name.split("/")[-1] + + def to_json(self): + """Export to json.""" + return attr.asdict(self, filter=lambda attr, value: attr.name != "last_fetched") + + @staticmethod + def create_from_dict(source: dict, action: bool = False) -> RepositoryData: + """Set attributes from dicts.""" + data = RepositoryData() + data.update_data(source, action) + return data + + def update_data(self, data: dict, action: bool = False) -> None: + """Update data of the repository.""" + for key, value in data.items(): + if key not in self.__dict__: + continue + + if key == "last_fetched" and isinstance(value, float): + setattr(self, key, datetime.fromtimestamp(value, UTC)) + elif key == "id": + setattr(self, key, str(value)) + elif key == "country": + if isinstance(value, str): + setattr(self, key, [value]) + else: + setattr(self, key, value) + elif key == "topics" and not action: + setattr(self, key, [topic for topic in value if topic not in TOPIC_FILTER]) + + else: + setattr(self, key, value) + + +@attr.s(auto_attribs=True) +class HacsManifest: + """HacsManifest class.""" + + content_in_root: bool = False + country: list[str] = [] + filename: str = None + hacs: str = None # Minimum HACS version + hide_default_branch: bool = False + homeassistant: str = None # Minimum Home Assistant version + manifest: dict = {} + name: str = None + persistent_directory: str = None + render_readme: bool = False + zip_release: bool = False + + def to_dict(self): + """Export to json.""" + return attr.asdict(self) + + @staticmethod + def from_dict(manifest: dict): + """Set attributes from dicts.""" + if manifest is None: + raise HacsException("Missing manifest data") + + manifest_data = HacsManifest() + manifest_data.manifest = { + k: v + for k, v in manifest.items() + if k in manifest_data.__dict__ and v != manifest_data.__getattribute__(k) + } + + for key, value in manifest_data.manifest.items(): + if key == "country" and isinstance(value, str): + setattr(manifest_data, key, [value]) + elif key in manifest_data.__dict__: + setattr(manifest_data, key, value) + return manifest_data + + def update_data(self, data: dict) -> None: + """Update the manifest data.""" + for key, value in data.items(): + if key not in self.__dict__: + continue + + if key == "country": + if isinstance(value, str): + setattr(self, key, [value]) + else: + setattr(self, key, value) + else: + setattr(self, key, value) + + +class RepositoryReleases: + """RepositoyReleases.""" + + last_release = None + last_release_object = None + published_tags = [] + objects: list[GitHubReleaseModel] = [] + releases = False + downloads = None + + +class RepositoryPath: + """RepositoryPath.""" + + local: str | None = None + remote: str | None = None + + +class RepositoryContent: + """RepositoryContent.""" + + path: RepositoryPath | None = None + files = [] + objects = [] + single = False + + +class HacsRepository: + """HacsRepository.""" + + def __init__(self, hacs: HacsBase) -> None: + """Set up HacsRepository.""" + self.hacs = hacs + self.additional_info = "" + self.data = RepositoryData() + self.content = RepositoryContent() + self.content.path = RepositoryPath() + self.repository_object: AIOGitHubAPIRepository | None = None + self.updated_info = False + self.state = None + self.force_branch = False + self.integration_manifest = {} + self.repository_manifest = HacsManifest.from_dict({}) + self.validate = Validate() + self.releases = RepositoryReleases() + self.pending_restart = False + self.tree = [] + self.treefiles = [] + self.ref = None + self.logger = LOGGER + + def __str__(self) -> str: + """Return a string representation of the repository.""" + return self.string + + @property + def string(self) -> str: + """Return a string representation of the repository.""" + return f"<{self.data.category.title()} {self.data.full_name}>" + + @property + def display_name(self) -> str: + """Return display name.""" + if self.repository_manifest.name is not None: + return self.repository_manifest.name + + if self.data.category == "integration": + if self.data.manifest_name is not None: + return self.data.manifest_name + if "name" in self.integration_manifest: + return self.integration_manifest["name"] + + return self.data.full_name.split("/")[-1].replace("-", " ").replace("_", " ").title() + + @property + def ignored_by_country_configuration(self) -> bool: + """Return True if hidden by country.""" + if self.data.installed: + return False + configuration = self.hacs.configuration.country.lower() + if configuration == "all": + return False + + manifest = [entry.lower() for entry in self.repository_manifest.country or []] + if not manifest: + return False + return configuration not in manifest + + @property + def display_status(self) -> str: + """Return display_status.""" + if self.data.new: + status = "new" + elif self.pending_restart: + status = "pending-restart" + elif self.pending_update: + status = "pending-upgrade" + elif self.data.installed: + status = "installed" + else: + status = "default" + return status + + @property + def display_installed_version(self) -> str: + """Return display_authors""" + if self.data.installed_version is not None: + installed = self.data.installed_version + else: + if self.data.installed_commit is not None: + installed = self.data.installed_commit + else: + installed = "" + return str(installed) + + @property + def display_available_version(self) -> str: + """Return display_authors""" + if self.data.show_beta and self.data.prerelease is not None: + available = self.data.prerelease + elif self.data.last_version is not None: + available = self.data.last_version + else: + if self.data.last_commit is not None: + available = self.data.last_commit + else: + available = "" + return str(available) + + @property + def display_version_or_commit(self) -> str: + """Does the repositoriy use releases or commits?""" + if self.data.releases: + version_or_commit = "version" + else: + version_or_commit = "commit" + return version_or_commit + + @property + def pending_update(self) -> bool: + """Return True if pending update.""" + if self.data.installed: + if self.data.selected_tag is not None: + if self.data.selected_tag == self.data.default_branch: + if self.data.installed_commit != self.data.last_commit: + return True + return False + if self.display_version_or_commit == "version": + if ( + result := version_left_higher_then_right( + self.display_available_version, + self.display_installed_version, + ) + ) is not None: + return result + if self.display_installed_version != self.display_available_version: + return True + + return False + + @property + def can_download(self) -> bool: + """Return True if we can download.""" + if self.repository_manifest.homeassistant is not None: + if self.data.releases: + if not version_left_higher_or_equal_then_right( + self.hacs.core.ha_version.string, + self.repository_manifest.homeassistant, + ): + return False + return True + + @property + def localpath(self) -> str | None: + """Return localpath.""" + return None + + @property + def should_try_releases(self) -> bool: + """Return a boolean indicating whether to download releases or not.""" + if self.repository_manifest.zip_release: + if self.repository_manifest.filename.endswith(".zip"): + if self.ref != self.data.default_branch: + return True + if self.ref == self.data.default_branch: + return False + if self.data.category not in ["plugin", "theme"]: + return False + if not self.data.releases: + return False + return True + + async def validate_repository(self) -> None: + """Validate.""" + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False) -> None: + """Update the repository""" + + async def common_validate(self, ignore_issues: bool = False) -> None: + """Common validation steps of the repository.""" + self.validate.errors.clear() + + # Make sure the repository exist. + self.logger.debug("%s Checking repository.", self.string) + await self.common_update_data(ignore_issues=ignore_issues) + + # Get the content of hacs.json + if RepositoryFile.HACS_JSON in [x.filename for x in self.tree]: + if manifest := await self.async_get_hacs_json(): + self.repository_manifest = HacsManifest.from_dict(manifest) + self.data.update_data( + self.repository_manifest.to_dict(), + action=self.hacs.system.action, + ) + + async def common_registration(self) -> None: + """Common registration steps of the repository.""" + # Attach repository + if self.repository_object is None: + try: + self.repository_object, etag = await self.async_get_legacy_repository_object( + etag=None if self.data.installed else self.data.etag_repository, + ) + self.data.update_data( + self.repository_object.attributes, + action=self.hacs.system.action, + ) + self.data.etag_repository = etag + except HacsNotModifiedException: + self.logger.debug("%s Did not update, content was not modified", self.string) + return + + if self.repository_object: + self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0) + self.data.last_fetched = datetime.now(UTC) + + @concurrent(concurrenttasks=10, backoff_time=5) + async def common_update(self, ignore_issues=False, force=False, skip_releases=False) -> bool: + """Common information update steps of the repository.""" + self.logger.debug("%s Getting repository information", self.string) + + # Attach repository + current_etag = self.data.etag_repository + try: + await self.common_update_data( + ignore_issues=ignore_issues, + force=force, + skip_releases=skip_releases, + ) + except HacsRepositoryExistException: + self.data.full_name = self.hacs.common.renamed_repositories[self.data.full_name] + await self.common_update_data(ignore_issues=ignore_issues, force=force) + + except HacsException: + if not ignore_issues and not force: + return False + + if not self.data.installed and (current_etag == self.data.etag_repository) and not force: + self.logger.debug("%s Did not update, content was not modified", self.string) + return False + + # Update last updated + if self.repository_object: + self.data.last_updated = self.repository_object.attributes.get("pushed_at", 0) + + # Update last available commit + await self.repository_object.set_last_commit() + self.data.last_commit = self.repository_object.last_commit + + # Get the content of hacs.json + if RepositoryFile.HACS_JSON in [x.filename for x in self.tree]: + if manifest := await self.async_get_hacs_json(): + self.repository_manifest = HacsManifest.from_dict(manifest) + self.data.update_data( + self.repository_manifest.to_dict(), + action=self.hacs.system.action, + ) + + # Update "info.md" + self.additional_info = await self.async_get_info_file_contents() + + # Set last fetch attribute + self.data.last_fetched = datetime.now(UTC) + + return True + + async def download_zip_files(self, validate: Validate) -> None: + """Download ZIP archive from repository release.""" + + try: + await self.async_download_zip_file( + DownloadableContent( + name=self.repository_manifest.filename, + url=github_release_asset( + repository=self.data.full_name, + version=self.ref, + filename=self.repository_manifest.filename, + ), + ), + validate, + ) + # lgtm [py/catch-base-exception] pylint: disable=broad-except + except BaseException: + validate.errors.append( + f"Download of { + self.repository_manifest.filename} was not completed" + ) + + async def async_download_zip_file( + self, + content: DownloadableContent, + validate: Validate, + ) -> None: + """Download ZIP archive from repository release.""" + try: + filecontent = await self.hacs.async_download_file(content["url"]) + + if filecontent is None: + validate.errors.append(f"Failed to download {content['url']}") + return + + temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp) + temp_file = f"{temp_dir}/{self.repository_manifest.filename}" + + result = await self.hacs.async_save_file(temp_file, filecontent) + + def _extract_zip_file(): + with zipfile.ZipFile(temp_file, "r") as zip_file: + zip_file.extractall(self.content.path.local) + + await self.hacs.hass.async_add_executor_job(_extract_zip_file) + + def cleanup_temp_dir(): + """Cleanup temp_dir.""" + if os.path.exists(temp_dir): + self.logger.debug("%s Cleaning up %s", self.string, temp_dir) + shutil.rmtree(temp_dir) + + if result: + self.logger.info("%s Download of %s completed", self.string, content["name"]) + await self.hacs.hass.async_add_executor_job(cleanup_temp_dir) + return + + validate.errors.append(f"[{content['name']}] was not downloaded") + # lgtm [py/catch-base-exception] pylint: disable=broad-except + except BaseException: + validate.errors.append("Download was not completed") + + async def download_content(self, version: string | None = None) -> None: + """Download the content of a directory.""" + contents: list[FileInformation] | None = None + if ( + not self.repository_manifest.zip_release + and not self.data.file_name + and self.content.path.remote is not None + ): + self.logger.info("%s Downloading repository archive", self.string) + try: + await self.download_repository_zip() + return + except HacsException as exception: + self.logger.exception(exception) + + if self.repository_manifest.filename: + self.logger.debug("%s %s", self.string, self.repository_manifest.filename) + + if self.content.path.remote == "release" and version is not None: + contents = await self.release_contents(version) + + if not contents: + contents = self.gather_files_to_download() + + if not contents: + raise HacsException("No content to download") + + download_queue = QueueManager(hass=self.hacs.hass) + + for content in contents: + if self.repository_manifest.content_in_root and self.repository_manifest.filename: + if content.name != self.repository_manifest.filename: + continue + download_queue.add(self.dowload_repository_content(content)) + + await download_queue.execute() + + async def download_repository_zip(self): + """Download the zip archive of the repository.""" + ref = f"{self.ref}".replace("tags/", "") + + if not ref: + raise HacsException("Missing required elements.") + + filecontent = await self.hacs.async_download_file( + github_archive(repository=self.data.full_name, version=ref, variant="tags"), + keep_url=True, + nolog=True, + ) + + if filecontent is None: + filecontent = await self.hacs.async_download_file( + github_archive(repository=self.data.full_name, version=ref, variant="heads"), + keep_url=True, + ) + if filecontent is None: + raise HacsException(f"[{self}] Failed to download zipball") + + temp_dir = await self.hacs.hass.async_add_executor_job(tempfile.mkdtemp) + temp_file = f"{temp_dir}/{self.repository_manifest.filename}" + result = await self.hacs.async_save_file(temp_file, filecontent) + if not result: + raise HacsException("Could not save ZIP file") + + def _extract_zip_file(): + with zipfile.ZipFile(temp_file, "r") as zip_file: + extractable = [] + for path in zip_file.filelist: + filename = "/".join(path.filename.split("/")[1:]) + if ( + filename.startswith(self.content.path.remote) + and filename != self.content.path.remote + ): + path.filename = filename.replace(self.content.path.remote, "") + if path.filename == "/": + # Blank files is not valid, and will start to throw in Python 3.12 + continue + extractable.append(path) + + if len(extractable) == 0: + raise HacsException("No content to extract") + zip_file.extractall(self.content.path.local, extractable) + + await self.hacs.hass.async_add_executor_job(_extract_zip_file) + + def cleanup_temp_dir(): + """Cleanup temp_dir.""" + if os.path.exists(temp_dir): + self.logger.debug("%s Cleaning up %s", self.string, temp_dir) + shutil.rmtree(temp_dir) + + await self.hacs.hass.async_add_executor_job(cleanup_temp_dir) + self.logger.info("%s Content was extracted to %s", self.string, self.content.path.local) + + async def async_get_hacs_json(self, ref: str = None) -> dict[str, Any] | None: + """Get the content of the hacs.json file.""" + try: + response = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.repos.contents.get, + raise_exception=False, + repository=self.data.full_name, + path=RepositoryFile.HACS_JSON, + **{"params": {"ref": ref or self.version_to_download()}}, + ) + if response: + return json_loads(decode_content(response.data.content)) + # lgtm [py/catch-base-exception] pylint: disable=broad-except + except BaseException: + pass + + async def async_get_info_file_contents(self, *, version: str | None = None, **kwargs) -> str: + """Get the content of the info.md file.""" + + def _info_file_variants() -> tuple[str, ...]: + name: str = "readme" + return ( + f"{name.upper()}.md", + f"{name}.md", + f"{name}.MD", + f"{name.upper()}.MD", + name.upper(), + name, + ) + + info_files = [filename for filename in _info_file_variants() if filename in self.treefiles] + + if not info_files: + return "" + + return await self.get_documentation(filename=info_files[0], version=version) or "" + + def remove(self) -> None: + """Run remove tasks.""" + if self.hacs.repositories.is_registered(repository_id=str(self.data.id)): + self.logger.info("%s Starting removal", self.string) + self.hacs.repositories.unregister(self) + + async def uninstall(self) -> None: + """Run uninstall tasks.""" + self.logger.info("%s Removing", self.string) + if not await self.remove_local_directory(): + raise HacsException("Could not uninstall") + self.data.installed = False + await self._async_post_uninstall() + await async_remove_store(self.hacs.hass, f"hacs/{self.data.id}.hacs") + + self.data.installed_version = None + self.data.installed_commit = None + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "uninstall", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + + await self.async_remove_entity_device() + ir.async_delete_issue(self.hacs.hass, DOMAIN, f"removed_{self.data.id}") + + async def remove_local_directory(self) -> None: + """Check the local directory.""" + + try: + if self.data.category == "python_script": + local_path = f"{self.content.path.local}/{self.data.file_name}" + elif self.data.category == "template": + local_path = f"{self.content.path.local}/{self.data.file_name}" + elif self.data.category == "theme": + path = ( + f"{self.hacs.core.config_path}/" + f"{self.hacs.configuration.theme_path}/" + f"{self.data.name}.yaml" + ) + await async_remove(self.hacs.hass, path, missing_ok=True) + local_path = self.content.path.local + elif self.data.category == "integration": + if not self.data.domain: + if domain := DOMAIN_OVERRIDES.get(self.data.full_name): + self.data.domain = domain + self.content.path.local = self.localpath + else: + self.logger.error("%s Missing domain", self.string) + return False + local_path = self.content.path.local + else: + local_path = self.content.path.local + + if await async_exists(self.hacs.hass, local_path): + if not is_safe(self.hacs, local_path): + self.logger.error("%s Path %s is blocked from removal", self.string, local_path) + return False + self.logger.debug("%s Removing %s", self.string, local_path) + + if self.data.category in ["python_script", "template"]: + await async_remove(self.hacs.hass, local_path) + else: + await async_remove_directory(self.hacs.hass, local_path) + + while await async_exists(self.hacs.hass, local_path): + await sleep(1) + else: + self.logger.debug( + "%s Presumed local content path %s does not exist", self.string, local_path + ) + + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + self.logger.debug("%s Removing %s failed with %s", self.string, local_path, exception) + return False + return True + + async def async_pre_registration(self) -> None: + """Run pre registration steps.""" + + @concurrent(concurrenttasks=10) + async def async_registration(self, ref=None) -> None: + """Run registration steps.""" + await self.async_pre_registration() + + if ref is not None: + self.data.selected_tag = ref + self.ref = ref + self.force_branch = True + + if not await self.validate_repository(): + return False + + # Run common registration steps. + await self.common_registration() + + # Set correct local path + self.content.path.local = self.localpath + + # Run local post registration steps. + await self.async_post_registration() + + async def async_post_registration(self) -> None: + """Run post registration steps.""" + if not self.hacs.system.action: + return + await self.hacs.validation.async_run_repository_checks(self) + + async def async_pre_install(self) -> None: + """Run pre install steps.""" + + async def _async_pre_install(self) -> None: + """Run pre install steps.""" + self.logger.info("%s Running pre installation steps", self.string) + await self.async_pre_install() + self.logger.info("%s Pre installation steps completed", self.string) + + async def async_install(self, *, version: str | None = None, **_) -> None: + """Run install steps.""" + await self._async_pre_install() + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 30}, + ) + self.logger.info("%s Running installation steps", self.string) + await self.async_install_repository(version=version) + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 90}, + ) + self.logger.info("%s Installation steps completed", self.string) + await self._async_post_install() + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": False}, + ) + + async def async_post_installation(self) -> None: + """Run post install steps.""" + + async def async_post_uninstall(self): + """Run post uninstall steps.""" + + async def _async_post_uninstall(self): + """Run post uninstall steps.""" + await self.async_post_uninstall() + + async def _async_post_install(self) -> None: + """Run post install steps.""" + self.logger.info("%s Running post installation steps", self.string) + await self.async_post_installation() + self.data.new = False + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "install", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + self.logger.info("%s Post installation steps completed", self.string) + + async def async_install_repository(self, *, version: str | None = None, **_) -> None: + """Common installation steps of the repository.""" + persistent_directory = None + await self.update_repository(force=version is None) + if self.content.path.local is None: + raise HacsException("repository.content.path.local is None") + self.validate.errors.clear() + + version_to_install = version or self.version_to_download() + if version_to_install == self.data.default_branch: + self.ref = version_to_install + else: + self.ref = f"tags/{version_to_install}" + + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 40}, + ) + + if self.repository_manifest.persistent_directory: + if await async_exists( + self.hacs.hass, + f"{self.content.path.local}/{self.repository_manifest.persistent_directory}", + ): + persistent_directory = Backup( + hacs=self.hacs, + local_path=f"{ + self.content.path.local}/{self.repository_manifest.persistent_directory}", + backup_path=tempfile.gettempdir() + "/hacs_persistent_directory/", + ) + await self.hacs.hass.async_add_executor_job(persistent_directory.create) + + if self.data.installed and not self.content.single: + backup = Backup(hacs=self.hacs, local_path=self.content.path.local) + await self.hacs.hass.async_add_executor_job(backup.create) + + self.hacs.log.debug("%s Local path is set to %s", self.string, self.content.path.local) + self.hacs.log.debug("%s Remote path is set to %s", self.string, self.content.path.remote) + self.hacs.log.debug("%s Version to install: %s", self.string, version_to_install) + + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 50}, + ) + + if self.repository_manifest.zip_release and self.repository_manifest.filename: + await self.download_zip_files(self.validate) + else: + await self.download_content(version_to_install) + + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 70}, + ) + + if self.validate.errors: + for error in self.validate.errors: + self.logger.error("%s %s", self.string, error) + if self.data.installed and not self.content.single: + await self.hacs.hass.async_add_executor_job(backup.restore) + await self.hacs.hass.async_add_executor_job(backup.cleanup) + raise HacsException("Could not download, see log for details") + + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 80}, + ) + + if self.data.installed and not self.content.single: + await self.hacs.hass.async_add_executor_job(backup.cleanup) + + if persistent_directory is not None: + await self.hacs.hass.async_add_executor_job(persistent_directory.restore) + await self.hacs.hass.async_add_executor_job(persistent_directory.cleanup) + + if self.validate.success: + self.data.installed = True + self.data.installed_commit = self.data.last_commit + + if version_to_install == self.data.default_branch: + self.data.installed_version = None + else: + self.data.installed_version = version_to_install + + async def async_get_legacy_repository_object( + self, + etag: str | None = None, + ) -> tuple[AIOGitHubAPIRepository, Any | None]: + """Return a repository object.""" + try: + repository = await self.hacs.github.get_repo(self.data.full_name, etag) + return repository, self.hacs.github.client.last_response.etag + except AIOGitHubAPINotModifiedException as exception: + raise HacsNotModifiedException(exception) from exception + except (ValueError, AIOGitHubAPIException, Exception) as exception: + raise HacsException(exception) from exception + + def update_filenames(self) -> None: + """Get the filename to target.""" + + async def get_tree(self, ref: str): + """Return the repository tree.""" + if self.repository_object is None: + raise HacsException("No repository_object") + try: + tree = await self.repository_object.get_tree(ref) + return tree + except (ValueError, AIOGitHubAPIException) as exception: + raise HacsException(exception) from exception + + async def get_releases(self, prerelease=False, returnlimit=5) -> list[GitHubReleaseModel]: + """Return the repository releases.""" + response = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.repos.releases.list, + repository=self.data.full_name, + ) + releases = [] + for release in response.data or []: + if len(releases) == returnlimit: + break + if release.draft or (release.prerelease and not prerelease): + continue + releases.append(release) + return releases + + async def common_update_data( + self, + ignore_issues: bool = False, + force: bool = False, + retry=False, + skip_releases=False, + ) -> None: + """Common update data.""" + releases = [] + try: + repository_object, etag = await self.async_get_legacy_repository_object( + etag=None if force or self.data.installed else self.data.etag_repository, + ) + self.repository_object = repository_object + if self.data.full_name.lower() != repository_object.full_name.lower(): + self.hacs.common.renamed_repositories[self.data.full_name] = ( + repository_object.full_name + ) + if not self.hacs.system.generator: + raise HacsRepositoryExistException + self.logger.error( + "%s Repository has been renamed - %s", self.string, repository_object.full_name + ) + self.data.update_data( + repository_object.attributes, + action=self.hacs.system.action, + ) + self.data.etag_repository = etag + except HacsNotModifiedException: + return + except HacsRepositoryExistException: + raise HacsRepositoryExistException from None + except (AIOGitHubAPIException, HacsException) as exception: + if not self.hacs.status.startup or self.hacs.system.generator: + self.logger.error("%s %s", self.string, exception) + if not ignore_issues: + self.validate.errors.append("Repository does not exist.") + raise HacsException(exception) from exception + + # Make sure the repository is not archived. + if self.data.archived and not ignore_issues: + self.validate.errors.append("Repository is archived.") + if self.data.full_name not in self.hacs.common.archived_repositories: + self.hacs.common.archived_repositories.add(self.data.full_name) + raise HacsRepositoryArchivedException(f"{self} Repository is archived.") + + # Make sure the repository is not in the blacklist. + if self.hacs.repositories.is_removed(self.data.full_name): + removed = self.hacs.repositories.removed_repository(self.data.full_name) + if removed.removal_type != "remove" and not ignore_issues: + self.validate.errors.append("Repository has been requested to be removed.") + raise HacsException(f"{self} Repository has been requested to be removed.") + + # Get releases. + if not skip_releases: + try: + releases = await self.get_releases(prerelease=True, returnlimit=30) + if releases: + self.data.prerelease = None + for release in releases: + if release.draft: + continue + elif release.prerelease: + if self.data.prerelease is None: + self.data.prerelease = release.tag_name + else: + self.data.last_version = release.tag_name + break + + self.data.releases = True + + filtered_releases = [ + release + for release in releases + if not release.draft and (self.data.show_beta or not release.prerelease) + ] + self.releases.objects = filtered_releases + self.data.published_tags = [x.tag_name for x in filtered_releases] + + except HacsException: + self.data.releases = False + + if not self.force_branch: + self.ref = self.version_to_download() + if self.data.releases: + for release in self.releases.objects or []: + if release.tag_name == self.ref: + if assets := release.assets: + downloads = next(iter(assets)).download_count + self.data.downloads = downloads + elif self.hacs.system.generator and self.repository_object: + await self.repository_object.set_last_commit() + self.data.last_commit = self.repository_object.last_commit + + self.hacs.log.debug( + "%s Running checks against %s", self.string, self.ref.replace("tags/", "") + ) + + try: + self.tree = await self.get_tree(self.ref) + if not self.tree: + raise HacsException("No files in tree") + self.treefiles = [] + for treefile in self.tree: + self.treefiles.append(treefile.full_path) + except (AIOGitHubAPIException, HacsException) as exception: + if ( + not retry + and self.ref is not None + and str(exception).startswith("GitHub returned 404") + ): + # Handle tags/branches being deleted. + self.data.selected_tag = None + self.ref = self.version_to_download() + self.logger.warning( + "%s Selected version/branch %s has been removed, falling back to default", + self.string, + self.ref, + ) + return await self.common_update_data(ignore_issues, force, True) + if not self.hacs.status.startup and not ignore_issues: + self.logger.error("%s %s", self.string, exception) + if not ignore_issues: + raise HacsException(exception) from None + + def gather_files_to_download(self) -> list[FileInformation]: + """Return a list of file objects to be downloaded.""" + files = [] + tree = self.tree + ref = f"{self.ref}".replace("tags/", "") + releaseobjects = self.releases.objects + category = self.data.category + remotelocation = self.content.path.remote + + if self.should_try_releases: + for release in releaseobjects or []: + if ref == release.tag_name: + for asset in release.assets or []: + files.append( + FileInformation(asset.browser_download_url, asset.name, asset.name) + ) + if files: + return files + + if self.content.single: + for treefile in tree: + if treefile.filename == self.data.file_name: + files.append( + FileInformation( + treefile.download_url, treefile.full_path, treefile.filename + ) + ) + return files + + if category == "plugin": + for treefile in tree: + if treefile.path in ["", "dist"]: + if remotelocation == "dist" and not treefile.filename.startswith("dist"): + continue + if not remotelocation: + if not treefile.filename.endswith(".js"): + continue + if treefile.path != "": + continue + if not treefile.is_directory: + files.append( + FileInformation( + treefile.download_url, treefile.full_path, treefile.filename + ) + ) + if files: + return files + + if self.repository_manifest.content_in_root: + if not self.repository_manifest.filename: + if category == "theme": + tree = filter_content_return_one_of_type(self.tree, "", "yaml", "full_path") + + for path in tree: + if path.is_directory: + continue + if path.full_path.startswith(self.content.path.remote): + files.append(FileInformation(path.download_url, path.full_path, path.filename)) + return files + + async def release_contents(self, version: str | None = None) -> list[FileInformation] | None: + """Gather the contents of a release.""" + release = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.generic, + endpoint=f"/repos/{self.data.full_name}/releases/tags/{version}", + raise_exception=False, + ) + if release is None: + return None + + return [ + FileInformation( + url=asset.get("browser_download_url"), + path=asset.get("name"), + name=asset.get("name"), + ) + for asset in release.data.get("assets", []) + ] + + @concurrent(concurrenttasks=10) + async def dowload_repository_content(self, content: FileInformation) -> None: + """Download content.""" + try: + self.logger.debug("%s Downloading %s", self.string, content.name) + + filecontent = await self.hacs.async_download_file(content.download_url) + + if filecontent is None: + self.validate.errors.append(f"[{content.name}] was not downloaded.") + return + + # Save the content of the file. + if self.content.single or content.path is None: + local_directory = self.content.path.local + + else: + _content_path = content.path + if not self.repository_manifest.content_in_root: + _content_path = _content_path.replace(f"{self.content.path.remote}", "") + + local_directory = f"{self.content.path.local}/{_content_path}" + local_directory = local_directory.split("/") + del local_directory[-1] + local_directory = "/".join(local_directory) + + # Check local directory + pathlib.Path(local_directory).mkdir(parents=True, exist_ok=True) + + local_file_path = (f"{local_directory}/{content.name}").replace("//", "/") + + result = await self.hacs.async_save_file(local_file_path, filecontent) + if result: + self.logger.info("%s Download of %s completed", self.string, content.name) + return + self.validate.errors.append(f"[{content.name}] was not downloaded.") + + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + self.validate.errors.append(f"Download was not completed [{exception}]") + + async def async_remove_entity_device(self) -> None: + """Remove the entity device.""" + device_registry: dr.DeviceRegistry = dr.async_get(hass=self.hacs.hass) + device = device_registry.async_get_device(identifiers={(DOMAIN, str(self.data.id))}) + + if device is None: + return + + device_registry.async_remove_device(device_id=device.id) + + def version_to_download(self) -> str: + """Determine which version to download.""" + if self.data.last_version is not None: + if self.data.selected_tag is not None: + if self.data.selected_tag == self.data.last_version: + self.data.selected_tag = None + return self.data.last_version + return self.data.selected_tag + return self.data.last_version + + if self.data.selected_tag is not None: + if self.data.selected_tag == self.data.default_branch: + return self.data.default_branch + if self.data.selected_tag in self.data.published_tags: + return self.data.selected_tag + + return self.data.default_branch or "main" + + async def get_documentation( + self, + *, + filename: str | None = None, + version: str | None = None, + **kwargs, + ) -> str | None: + """Get the documentation of the repository.""" + if filename is None: + return None + + if version is not None: + target_version = version + elif self.data.installed: + target_version = self.data.installed_version or self.data.installed_commit + else: + target_version = self.data.last_version or self.data.last_commit or self.ref + + self.logger.debug( + "%s Getting documentation for version=%s,filename=%s", + self.string, + target_version, + filename, + ) + if target_version is None: + return None + + result = await self.hacs.async_download_file( + f"https://raw.githubusercontent.com/{ + self.data.full_name}/{target_version}/{filename}", + nolog=True, + ) + + return ( + result.decode(encoding="utf-8") + .replace(" HacsManifest | None: + """Get the hacs.json file of the repository.""" + self.logger.debug("%s Getting hacs.json for version=%s", self.string, version) + try: + result = await self.hacs.async_download_file( + f"https://raw.githubusercontent.com/{ + self.data.full_name}/{version}/hacs.json", + nolog=True, + ) + if result is None: + return None + return HacsManifest.from_dict(json_loads(result)) + except Exception: # pylint: disable=broad-except + return None + + async def _ensure_download_capabilities(self, ref: str | None, **kwargs: Any) -> None: + """Ensure that the download can be handled.""" + target_manifest: HacsManifest | None = None + if ref is None: + if not self.can_download: + raise HacsException( + f"This { + self.data.category.value} is not available for download." + ) + return + + if ref == self.data.last_version: + target_manifest = self.repository_manifest + else: + target_manifest = await self.get_hacs_json(version=ref) + + if target_manifest is None: + raise HacsException( + f"The version {ref} for this { + self.data.category.value} can not be used with HACS." + ) + + if ( + target_manifest.homeassistant is not None + and self.hacs.core.ha_version < target_manifest.homeassistant + ): + raise HacsException( + f"This version requires Home Assistant { + target_manifest.homeassistant} or newer." + ) + if target_manifest.hacs is not None and self.hacs.version < target_manifest.hacs: + raise HacsException(f"This version requires HACS { + target_manifest.hacs} or newer.") + + async def async_download_repository(self, *, ref: str | None = None, **_) -> None: + """Download the content of a repository.""" + await self._ensure_download_capabilities(ref) + self.logger.info("Starting download, %s", ref) + if self.display_version_or_commit == "version": + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 10}, + ) + if not ref: + await self.update_repository(force=True) + else: + self.ref = ref + self.data.selected_tag = ref + self.force_branch = ref is not None + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": 20}, + ) + + try: + await self.async_install(version=ref) + except HacsException as exception: + raise HacsException( + f"Downloading {self.data.full_name} with version { + ref or self.data.last_version or self.data.last_commit} failed with ({exception})" + ) from exception + finally: + self.data.selected_tag = None + self.force_branch = False + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + {"repository": self.data.full_name, "progress": False}, + ) + + async def async_get_releases(self, *, first: int = 30) -> list[GitHubReleaseModel]: + """Get the last x releases of a repository.""" + response = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.repos.releases.list, + repository=self.data.full_name, + kwargs={"per_page": 30}, + ) + return response.data diff --git a/homeassistant/config/custom_components/hacs/repositories/integration.py b/homeassistant/config/custom_components/hacs/repositories/integration.py new file mode 100644 index 0000000..8456ce6 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/integration.py @@ -0,0 +1,217 @@ +"""Class for integrations in HACS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.loader import async_get_custom_components + +from ..const import DOMAIN +from ..enums import HacsCategory, HacsDispatchEvent, HacsGitHubRepo, RepositoryFile +from ..exceptions import AddonRepositoryException, HacsException +from ..utils.decode import decode_content +from ..utils.decorator import concurrent +from ..utils.filters import get_first_directory_in_directory +from ..utils.json import json_loads +from .base import HacsRepository + +if TYPE_CHECKING: + from ..base import HacsBase + + +class HacsIntegrationRepository(HacsRepository): + """Integrations in HACS.""" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.INTEGRATION + self.content.path.remote = "custom_components" + self.content.path.local = self.localpath + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/custom_components/{self.data.domain}" + + async def async_post_installation(self): + """Run post installation steps.""" + self.pending_restart = True + if self.data.config_flow: + if self.data.full_name != HacsGitHubRepo.INTEGRATION: + await self.reload_custom_components() + if self.data.first_install: + self.pending_restart = False + + if self.pending_restart: + self.logger.debug("%s Creating restart_required issue", self.string) + async_create_issue( + hass=self.hacs.hass, + domain=DOMAIN, + issue_id=f"restart_required_{self.data.id}_{self.ref}", + is_fixable=True, + issue_domain=self.data.domain or DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="restart_required", + translation_placeholders={ + "name": self.display_name, + }, + ) + + async def async_post_uninstall(self) -> None: + """Run post uninstall steps.""" + if self.data.config_flow: + await self.reload_custom_components() + else: + self.pending_restart = True + + async def validate_repository(self): + """Validate.""" + await self.common_validate() + + # Custom step 1: Validate content. + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "custom_components": + name = get_first_directory_in_directory(self.tree, "custom_components") + if name is None: + if ( + "repository.json" in self.treefiles + or "repository.yaml" in self.treefiles + or "repository.yml" in self.treefiles + ): + raise AddonRepositoryException() + raise HacsException( + f"{self.string} Repository structure for { + self.ref.replace('tags/', '')} is not compliant" + ) + self.content.path.remote = f"custom_components/{name}" + + # Get the content of manifest.json + if manifest := await self.async_get_integration_manifest(): + try: + self.integration_manifest = manifest + self.data.authors = manifest.get("codeowners", []) + self.data.domain = manifest["domain"] + self.data.manifest_name = manifest.get("name") + self.data.config_flow = manifest.get("config_flow", False) + + except KeyError as exception: + self.validate.errors.append( + f"Missing expected key '{exception}' in { + RepositoryFile.MAINIFEST_JSON}" + ) + self.hacs.log.error( + "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON + ) + + # Set local path + self.content.path.local = self.localpath + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + if self.content.path.remote == "custom_components": + name = get_first_directory_in_directory(self.tree, "custom_components") + self.content.path.remote = f"custom_components/{name}" + + # Get the content of manifest.json + if manifest := await self.async_get_integration_manifest(): + try: + self.integration_manifest = manifest + self.data.authors = manifest.get("codeowners", []) + self.data.domain = manifest["domain"] + self.data.manifest_name = manifest.get("name") + self.data.config_flow = manifest.get("config_flow", False) + + except KeyError as exception: + self.validate.errors.append( + f"Missing expected key '{exception}' in { + RepositoryFile.MAINIFEST_JSON}" + ) + self.hacs.log.error( + "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON + ) + + # Set local path + self.content.path.local = self.localpath + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + + async def reload_custom_components(self): + """Reload custom_components (and config flows)in HA.""" + self.logger.info("Reloading custom_component cache") + del self.hacs.hass.data["custom_components"] + await async_get_custom_components(self.hacs.hass) + self.logger.info("Custom_component cache reloaded") + + async def async_get_integration_manifest(self, ref: str = None) -> dict[str, Any] | None: + """Get the content of the manifest.json file.""" + manifest_path = ( + "manifest.json" + if self.repository_manifest.content_in_root + else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}" + ) + + if not manifest_path in (x.full_path for x in self.tree): + raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'") + + response = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.repos.contents.get, + repository=self.data.full_name, + path=manifest_path, + **{"params": {"ref": ref or self.version_to_download()}}, + ) + if response: + return json_loads(decode_content(response.data.content)) + + async def get_integration_manifest(self, *, version: str, **kwargs) -> dict[str, Any] | None: + """Get the content of the manifest.json file.""" + manifest_path = ( + "manifest.json" + if self.repository_manifest.content_in_root + else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}" + ) + + if manifest_path not in (x.full_path for x in self.tree): + raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'") + + self.logger.debug("%s Getting manifest.json for version=%s", self.string, version) + try: + result = await self.hacs.async_download_file( + f"https://raw.githubusercontent.com/{ + self.data.full_name}/{version}/{manifest_path}", + nolog=True, + ) + if result is None: + return None + return json_loads(result) + except Exception: # pylint: disable=broad-except + return None diff --git a/homeassistant/config/custom_components/hacs/repositories/plugin.py b/homeassistant/config/custom_components/hacs/repositories/plugin.py new file mode 100644 index 0000000..190abc5 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/plugin.py @@ -0,0 +1,246 @@ +"""Class for plugins in HACS.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from ..enums import HacsCategory, HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.decorator import concurrent +from ..utils.json import json_loads +from .base import HacsRepository + +HACSTAG_REPLACER = re.compile(r"\D+") + +if TYPE_CHECKING: + from homeassistant.components.lovelace.resources import ResourceStorageCollection + + from ..base import HacsBase + + +class HacsPluginRepository(HacsRepository): + """Plugins in HACS.""" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.file_name = None + self.data.category = HacsCategory.PLUGIN + self.content.path.local = self.localpath + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/www/community/{self.data.full_name.split('/')[-1]}" + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + self.update_filenames() + + if self.content.path.remote is None: + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.content.path.remote == "release": + self.content.single = True + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + async def async_post_installation(self): + """Run post installation steps.""" + await self.hacs.async_setup_frontend_endpoint_plugin() + await self.update_dashboard_resources() + + async def async_post_uninstall(self): + """Run post uninstall steps.""" + await self.remove_dashboard_resources() + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + # Get plugin objects. + self.update_filenames() + + if self.content.path.remote is None: + self.validate.errors.append( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.content.path.remote == "release": + self.content.single = True + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + + async def get_package_content(self): + """Get package content.""" + try: + package = await self.repository_object.get_contents("package.json", self.ref) + package = json_loads(package.content) + + if package: + self.data.authors = package["author"] + except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except + pass + + def update_filenames(self) -> None: + """Get the filename to target.""" + content_in_root = self.repository_manifest.content_in_root + if specific_filename := self.repository_manifest.filename: + valid_filenames = (specific_filename,) + else: + valid_filenames = ( + f"{self.data.name.replace('lovelace-', '')}.js", + f"{self.data.name}.js", + f"{self.data.name}.umd.js", + f"{self.data.name}-bundle.js", + ) + + if not content_in_root: + if self.releases.objects: + release = self.releases.objects[0] + if release.assets: + if assetnames := [ + filename + for filename in valid_filenames + for asset in release.assets + if filename == asset.name + ]: + self.data.file_name = assetnames[0] + self.content.path.remote = "release" + return + + all_paths = {x.full_path for x in self.tree} + for filename in valid_filenames: + if filename in all_paths: + self.data.file_name = filename + self.content.path.remote = "" + return + if not content_in_root and f"dist/{filename}" in all_paths: + self.data.file_name = filename.split("/")[-1] + self.content.path.remote = "dist" + return + + def generate_dashboard_resource_hacstag(self) -> str: + """Get the HACS tag used by dashboard resources.""" + version = ( + self.display_installed_version + or self.data.selected_tag + or self.display_available_version + ) + return f"{self.data.id}{HACSTAG_REPLACER.sub('', version)}" + + def generate_dashboard_resource_namespace(self) -> str: + """Get the dashboard resource namespace.""" + return f"/hacsfiles/{self.data.full_name.split("/")[1]}" + + def generate_dashboard_resource_url(self) -> str: + """Get the dashboard resource namespace.""" + filename = self.data.file_name + if "/" in filename: + self.logger.warning("%s have defined an invalid file name %s", self.string, filename) + filename = filename.split("/")[-1] + return ( + f"{self.generate_dashboard_resource_namespace()}/{filename}" + f"?hacstag={self.generate_dashboard_resource_hacstag()}" + ) + + def _get_resource_handler(self) -> ResourceStorageCollection | None: + """Get the resource handler.""" + resources: ResourceStorageCollection | None + if not (hass_data := self.hacs.hass.data): + self.logger.error("%s Can not access the hass data", self.string) + return + + if (lovelace_data := hass_data.get("lovelace")) is None: + self.logger.warning("%s Can not access the lovelace integration data", self.string) + return + + if self.hacs.core.ha_version > "2025.1.99": + # Changed to 2025.2.0 + # Changed in https://github.com/home-assistant/core/pull/136313 + resources = lovelace_data.resources + else: + resources = lovelace_data.get("resources") + + if resources is None: + self.logger.warning("%s Can not access the dashboard resources", self.string) + return + + if not hasattr(resources, "store") or resources.store is None: + self.logger.info("%s YAML mode detected, can not update resources", self.string) + return + + if resources.store.key != "lovelace_resources" or resources.store.version != 1: + self.logger.warning("%s Can not use the dashboard resources", self.string) + return + + return resources + + async def update_dashboard_resources(self) -> None: + """Update dashboard resources.""" + if not (resources := self._get_resource_handler()): + return + + if not resources.loaded: + await resources.async_load() + + namespace = self.generate_dashboard_resource_namespace() + url = self.generate_dashboard_resource_url() + + for entry in resources.async_items(): + if (entry_url := entry["url"]).startswith(namespace): + if entry_url != url: + self.logger.info( + "%s Updating existing dashboard resource from %s to %s", + self.string, + entry_url, + url, + ) + await resources.async_update_item(entry["id"], {"url": url}) + return + + # Nothing was updated, add the resource + self.logger.info("%s Adding dashboard resource %s", self.string, url) + await resources.async_create_item({"res_type": "module", "url": url}) + + async def remove_dashboard_resources(self) -> None: + """Remove dashboard resources.""" + if not (resources := self._get_resource_handler()): + return + + if not resources.loaded: + await resources.async_load() + + namespace = self.generate_dashboard_resource_namespace() + + for entry in resources.async_items(): + if entry["url"].startswith(namespace): + self.logger.info("%s Removing dashboard resource %s", self.string, entry["url"]) + await resources.async_delete_item(entry["id"]) + return diff --git a/homeassistant/config/custom_components/hacs/repositories/python_script.py b/homeassistant/config/custom_components/hacs/repositories/python_script.py new file mode 100644 index 0000000..abbb6a1 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/python_script.py @@ -0,0 +1,111 @@ +"""Class for python_scripts in HACS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..enums import HacsCategory, HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.decorator import concurrent +from .base import HacsRepository + +if TYPE_CHECKING: + from ..base import HacsBase + + +class HacsPythonScriptRepository(HacsRepository): + """python_scripts in HACS.""" + + category = "python_script" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.PYTHON_SCRIPT + self.content.path.remote = "python_scripts" + self.content.path.local = self.localpath + self.content.single = True + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/python_scripts" + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + compliant = False + for treefile in self.treefiles: + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): + compliant = True + break + if not compliant: + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + async def async_post_registration(self): + """Registration.""" + # Set name + self.update_filenames() + + if self.hacs.system.action: + await self.hacs.validation.async_run_repository_checks(self) + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + # Get python_script objects. + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + compliant = False + for treefile in self.treefiles: + if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): + compliant = True + break + if not compliant: + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Update name + self.update_filenames() + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + + def update_filenames(self) -> None: + """Get the filename to target.""" + for treefile in self.tree: + if treefile.full_path.startswith( + self.content.path.remote + ) and treefile.full_path.endswith(".py"): + self.data.file_name = treefile.filename diff --git a/homeassistant/config/custom_components/hacs/repositories/template.py b/homeassistant/config/custom_components/hacs/repositories/template.py new file mode 100644 index 0000000..fc5678d --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/template.py @@ -0,0 +1,106 @@ +"""Class for themes in HACS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.exceptions import HomeAssistantError + +from ..enums import HacsCategory, HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.decorator import concurrent +from .base import HacsRepository + +if TYPE_CHECKING: + from ..base import HacsBase + + +class HacsTemplateRepository(HacsRepository): + """Custom templates in HACS.""" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.TEMPLATE + self.content.path.remote = "" + self.content.path.local = self.localpath + self.content.single = True + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/custom_templates" + + async def async_post_installation(self): + """Run post installation steps.""" + await self._reload_custom_templates() + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + self.data.file_name = self.repository_manifest.filename + + if ( + not self.data.file_name + or "/" in self.data.file_name + or not self.data.file_name.endswith(".jinja") + or self.data.file_name not in self.treefiles + ): + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + async def async_post_registration(self): + """Registration.""" + # Set filenames + self.data.file_name = self.repository_manifest.filename + self.content.path.local = self.localpath + + if self.hacs.system.action: + await self.hacs.validation.async_run_repository_checks(self) + + async def async_post_uninstall(self) -> None: + """Run post uninstall steps.""" + await self._reload_custom_templates() + + async def _reload_custom_templates(self) -> None: + """Reload custom templates.""" + self.logger.debug("%s Reloading custom templates", self.string) + try: + await self.hacs.hass.services.async_call("homeassistant", "reload_custom_templates", {}) + except HomeAssistantError as exception: + self.logger.exception("%s %s", self.string, exception) + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + # Update filenames + self.data.file_name = self.repository_manifest.filename + self.content.path.local = self.localpath + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) diff --git a/homeassistant/config/custom_components/hacs/repositories/theme.py b/homeassistant/config/custom_components/hacs/repositories/theme.py new file mode 100644 index 0000000..bd90aa4 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/repositories/theme.py @@ -0,0 +1,119 @@ +"""Class for themes in HACS.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.exceptions import HomeAssistantError + +from ..enums import HacsCategory, HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.decorator import concurrent +from .base import HacsRepository + +if TYPE_CHECKING: + from ..base import HacsBase + + +class HacsThemeRepository(HacsRepository): + """Themes in HACS.""" + + def __init__(self, hacs: HacsBase, full_name: str): + """Initialize.""" + super().__init__(hacs=hacs) + self.data.full_name = full_name + self.data.full_name_lower = full_name.lower() + self.data.category = HacsCategory.THEME + self.content.path.remote = "themes" + self.content.path.local = self.localpath + self.content.single = False + + @property + def localpath(self): + """Return localpath.""" + return f"{self.hacs.core.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" + + async def async_post_installation(self): + """Run post installation steps.""" + await self._reload_frontend_themes() + + async def validate_repository(self): + """Validate.""" + # Run common validation steps. + await self.common_validate() + + # Custom step 1: Validate content. + compliant = False + for treefile in self.treefiles: + if treefile.startswith("themes/") and treefile.endswith(".yaml"): + compliant = True + break + if not compliant: + raise HacsException( + f"{self.string} Repository structure for {self.ref.replace('tags/','')} is not compliant" + ) + + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + # Handle potential errors + if self.validate.errors: + for error in self.validate.errors: + if not self.hacs.status.startup: + self.logger.error("%s %s", self.string, error) + return self.validate.success + + async def async_post_registration(self): + """Registration.""" + # Set name + self.update_filenames() + self.content.path.local = self.localpath + + if self.hacs.system.action: + await self.hacs.validation.async_run_repository_checks(self) + + async def _reload_frontend_themes(self) -> None: + """Reload frontend themes.""" + self.logger.debug("%s Reloading frontend themes", self.string) + try: + await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) + except HomeAssistantError as exception: + self.logger.exception("%s %s", self.string, exception) + + async def async_post_uninstall(self) -> None: + """Run post uninstall steps.""" + await self._reload_frontend_themes() + + @concurrent(concurrenttasks=10, backoff_time=5) + async def update_repository(self, ignore_issues=False, force=False): + """Update.""" + if not await self.common_update(ignore_issues, force) and not force: + return + + # Get theme objects. + if self.repository_manifest.content_in_root: + self.content.path.remote = "" + + # Update name + self.update_filenames() + self.content.path.local = self.localpath + + # Signal frontend to refresh + if self.data.installed: + self.hacs.async_dispatch( + HacsDispatchEvent.REPOSITORY, + { + "id": 1337, + "action": "update", + "repository": self.data.full_name, + "repository_id": self.data.id, + }, + ) + + def update_filenames(self) -> None: + """Get the filename to target.""" + for treefile in self.tree: + if treefile.full_path.startswith( + self.content.path.remote + ) and treefile.full_path.endswith(".yaml"): + self.data.file_name = treefile.filename diff --git a/homeassistant/config/custom_components/hacs/switch.py b/homeassistant/config/custom_components/hacs/switch.py new file mode 100644 index 0000000..4fa43d4 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/switch.py @@ -0,0 +1,73 @@ +"""Switch entities for HACS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .base import HacsBase +from .const import DOMAIN +from .entity import HacsRepositoryEntity +from .repositories.base import HacsRepository + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Setup switch platform.""" + hacs: HacsBase = hass.data[DOMAIN] + async_add_entities( + HacsRepositoryPreReleaseSwitchEntity(hacs=hacs, repository=repository) + for repository in hacs.repositories.list_downloaded + ) + + +class HacsRepositoryPreReleaseSwitchEntity(HacsRepositoryEntity, SwitchEntity): + """Pre-release switch entities for repositories downloaded with HACS.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_translation_key = "pre-release" + + def __init__(self, hacs: HacsBase, repository: HacsRepository) -> None: + """Initialize the repository pre-release switch.""" + super().__init__(hacs, repository) + self._attr_entity_registry_enabled_default = self.repository.data.show_beta + + @property + def is_on(self) -> bool: + """Return if the pre-release option is enabled for the repository.""" + return self.repository.data.show_beta + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self._handle_change(value=True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._handle_change(value=False) + + async def _handle_change(self, value: bool) -> None: + """Handle attribute value changes.""" + self.repository.data.show_beta = value + + # As this value is directly affecting what data points is in use by other entities + # we need to update all entities to reflect the change + # Do force an update of the entities we need to clear the last fetched data + # since that is used to limit state updates + # Once we have signaled the update we can restore the last fetched data + _last_fetch = self.repository.data.last_fetched + self.repository.data.last_fetched = None + self.coordinator.async_update_listeners() + self.repository.data.last_fetched = _last_fetch # Restore last fetched + + # Write the HACS data and update the entity state + await self.hacs.data.async_write() + self.async_write_ha_state() diff --git a/homeassistant/config/custom_components/hacs/system_health.py b/homeassistant/config/custom_components/hacs/system_health.py new file mode 100644 index 0000000..2081c37 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/system_health.py @@ -0,0 +1,52 @@ +"""Provide info to system health.""" + +from typing import Any + +from aiogithubapi.common.const import BASE_API_URL +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +from .base import HacsBase +from .const import DOMAIN + +GITHUB_STATUS = "https://www.githubstatus.com/" +CLOUDFLARE_STATUS = "https://www.cloudflarestatus.com/" + + +@callback +def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegistration) -> None: + """Register system health callbacks.""" + register.domain = "Home Assistant Community Store" + register.async_register_info(system_health_info, "/hacs") + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + if DOMAIN not in hass.data: + return {"Disabled": "HACS is not loaded, but HA still requests this information..."} + + hacs: HacsBase = hass.data[DOMAIN] + response = await hacs.githubapi.rate_limit() + + data = { + "GitHub API": system_health.async_check_can_reach_url(hass, BASE_API_URL, GITHUB_STATUS), + "GitHub Content": system_health.async_check_can_reach_url( + hass, "https://raw.githubusercontent.com/hacs/integration/main/hacs.json" + ), + "GitHub Web": system_health.async_check_can_reach_url( + hass, "https://github.com/", GITHUB_STATUS + ), + "HACS Data": system_health.async_check_can_reach_url( + hass, "https://data-v2.hacs.xyz/data.json", CLOUDFLARE_STATUS + ), + "GitHub API Calls Remaining": response.data.resources.core.remaining, + "Installed Version": hacs.version, + "Stage": hacs.stage, + "Available Repositories": len(hacs.repositories.list_all), + "Downloaded Repositories": len(hacs.repositories.list_downloaded), + } + + if hacs.system.disabled: + data["Disabled"] = hacs.system.disabled_reason + + return data diff --git a/homeassistant/config/custom_components/hacs/translations/en.json b/homeassistant/config/custom_components/hacs/translations/en.json new file mode 100644 index 0000000..a4bed76 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/translations/en.json @@ -0,0 +1,84 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of HACS is allowed.", + "min_ha_version": "You need at least version {version} of Home Assistant to setup HACS.", + "github": "Could not authenticate with GitHub, try again later.", + "not_setup": "HACS is not setup.", + "reauth_successful": "Reauthentication was successful." + }, + "error": { + "auth": "Personal Access Token is not correct", + "acc": "You need to acknowledge all the statements before continuing" + }, + "step": { + "user": { + "data": { + "acc_logs": "I know how to access Home Assistant logs", + "acc_addons": "I know that there are no add-ons in HACS", + "acc_untested": "I know that everything inside HACS including HACS itself is custom and untested by Home Assistant", + "acc_disable": "I know that if I get issues with Home Assistant I should disable all my custom_components" + }, + "description": "Before you can setup HACS you need to acknowledge the following" + }, + "device": { + "title": "Waiting for device activation" + }, + "reauth_confirm": { + "title": "Reauthentication needed", + "description": "You need to reauthenticate with GitHub." + } + }, + "progress": { + "wait_for_device": "1. Open {url} \n2. Paste the following key to authorize HACS: \n```\n{code}\n```" + } + }, + "options": { + "abort": { + "not_setup": "HACS is not setup.", + "pending_tasks": "There are pending tasks. Try again later.", + "release_limit_value": "The release limit needs to be between 1 and 100." + }, + "step": { + "user": { + "data": { + "not_in_use": "Not in use with YAML", + "country": "Filter with country code", + "release_limit": "Number of releases to show", + "debug": "Enable debug", + "appdaemon": "Enable AppDaemon apps discovery & tracking", + "sidepanel_icon": "Side panel icon", + "sidepanel_title": "Side panel title" + } + } + } + }, + "issues": { + "restart_required": { + "title": "Restart required", + "fix_flow": { + "step": { + "confirm_restart": { + "title": "Restart required", + "description": "Restart of Home Assistant is required to finish download/update of {name}, click submit to restart now." + } + } + } + }, + "removed": { + "title": "Repository removed from HACS", + "description": "Because {reason}, `{name}` has been removed from HACS. Please visit the [HACS Panel](/hacs/repository/{repositry_id}) to remove it." + } + }, + "entity": { + "switch": { + "pre-release": { + "name": "Pre-release", + "state": { + "off": "No pre-releases", + "on": "Pre-releases preferred" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/hacs/types.py b/homeassistant/config/custom_components/hacs/types.py new file mode 100644 index 0000000..2b2ac01 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/types.py @@ -0,0 +1,10 @@ +"""Custom HACS types.""" + +from typing import TypedDict + + +class DownloadableContent(TypedDict): + """Downloadable content.""" + + url: str + name: str diff --git a/homeassistant/config/custom_components/hacs/update.py b/homeassistant/config/custom_components/hacs/update.py new file mode 100644 index 0000000..ce8a0b2 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/update.py @@ -0,0 +1,158 @@ +"""Update entities for HACS.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, HomeAssistantError, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .base import HacsBase +from .const import DOMAIN +from .entity import HacsRepositoryEntity +from .enums import HacsCategory, HacsDispatchEvent +from .exceptions import HacsException + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Setup update platform.""" + hacs: HacsBase = hass.data[DOMAIN] + async_add_entities( + HacsRepositoryUpdateEntity(hacs=hacs, repository=repository) + for repository in hacs.repositories.list_downloaded + ) + + +class HacsRepositoryUpdateEntity(HacsRepositoryEntity, UpdateEntity): + """Update entities for repositories downloaded with HACS.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.RELEASE_NOTES + ) + + @property + def name(self) -> str | None: + """Return the name.""" + return f"{self.repository.display_name} update" + + @property + def latest_version(self) -> str: + """Return latest version of the entity.""" + return self.repository.display_available_version + + @property + def release_url(self) -> str: + """Return the URL of the release page.""" + if self.repository.display_version_or_commit == "commit": + return f"https://github.com/{self.repository.data.full_name}" + return f"https://github.com/{self.repository.data.full_name}/releases/{self.latest_version}" + + @property + def installed_version(self) -> str: + """Return downloaded version of the entity.""" + return self.repository.display_installed_version + + @property + def release_summary(self) -> str | None: + """Return the release summary.""" + if self.repository.pending_restart: + return "Restart of Home Assistant required" + return None + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend.""" + if ( + self.repository.data.category != HacsCategory.INTEGRATION + or self.repository.data.domain is None + ): + return None + + return f"https://brands.home-assistant.io/_/{self.repository.data.domain}/icon.png" + + async def async_install(self, version: str | None, backup: bool, **kwargs: Any) -> None: + """Install an update.""" + to_download = version or self.latest_version + if to_download == self.installed_version: + raise HomeAssistantError(f"Version {self.installed_version} of { + self.repository.data.full_name} is already downloaded") + try: + await self.repository.async_download_repository(ref=version or self.latest_version) + except HacsException as exception: + raise HomeAssistantError(exception) from exception + + async def async_release_notes(self) -> str | None: + """Return the release notes.""" + if self.repository.pending_restart: + return None + + if self.latest_version not in self.repository.data.published_tags: + releases = await self.repository.get_releases( + prerelease=self.repository.data.show_beta, + returnlimit=self.hacs.configuration.release_limit, + ) + if releases: + self.repository.data.releases = True + self.repository.releases.objects = releases + self.repository.data.published_tags = [x.tag_name for x in releases] + self.repository.data.last_version = next(iter(self.repository.data.published_tags)) + + release_notes = "" + # Compile release notes from installed version up to the latest + if self.installed_version in self.repository.data.published_tags: + for release in self.repository.releases.objects: + if release.tag_name == self.installed_version: + break + release_notes += f"# {release.tag_name}" + if release.tag_name != release.name: + release_notes += f" - {release.name}" + release_notes += f"\n\n{release.body}" + release_notes += "\n\n---\n\n" + elif any(self.repository.releases.objects): + release_notes += self.repository.releases.objects[0].body + + if self.repository.pending_update: + if self.repository.data.category == HacsCategory.INTEGRATION: + release_notes += ( + "\n\nYou need to restart" + " Home Assistant manually after updating.\n\n" + ) + if self.repository.data.category == HacsCategory.PLUGIN: + release_notes += ( + "\n\nYou need to manually" + " clear the frontend cache after updating.\n\n" + ) + + return release_notes.replace("\n#", "\n\n#") + + async def async_added_to_hass(self) -> None: + """Register for status events.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + HacsDispatchEvent.REPOSITORY_DOWNLOAD_PROGRESS, + self._update_download_progress, + ) + ) + + @callback + def _update_download_progress(self, data: dict) -> None: + """Update the download progress.""" + if data["repository"] != self.repository.data.full_name: + return + self._update_in_progress(progress=data["progress"]) + + @callback + def _update_in_progress(self, progress: int | bool) -> None: + """Update the download progress.""" + self._attr_in_progress = progress + self.async_write_ha_state() diff --git a/homeassistant/config/custom_components/hacs/utils/__init__.py b/homeassistant/config/custom_components/hacs/utils/__init__.py new file mode 100644 index 0000000..58b214c --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/__init__.py @@ -0,0 +1 @@ +"""Initialize HACS utils.""" diff --git a/homeassistant/config/custom_components/hacs/utils/backup.py b/homeassistant/config/custom_components/hacs/utils/backup.py new file mode 100644 index 0000000..0c64bfc --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/backup.py @@ -0,0 +1,110 @@ +"""Backup.""" + +from __future__ import annotations + +import os +import shutil +import tempfile +from time import sleep +from typing import TYPE_CHECKING + +from .path import is_safe + +if TYPE_CHECKING: + from ..base import HacsBase + from ..repositories.base import HacsRepository + + +DEFAULT_BACKUP_PATH = f"{tempfile.gettempdir()}/hacs_backup/" + + +class Backup: + """Backup.""" + + def __init__( + self, + hacs: HacsBase, + local_path: str | None = None, + backup_path: str = DEFAULT_BACKUP_PATH, + repository: HacsRepository | None = None, + ) -> None: + """Initialize.""" + self.hacs = hacs + self.repository = repository + self.local_path = local_path or repository.content.path.local + self.backup_path = backup_path + if repository: + self.backup_path = ( + tempfile.gettempdir() + + f"/hacs_persistent_{repository.data.category}/" + + repository.data.name + ) + self.backup_path_full = f"{self.backup_path}{self.local_path.split('/')[-1]}" + + def _init_backup_dir(self) -> bool: + """Init backup dir.""" + if not os.path.exists(self.local_path): + return False + if not is_safe(self.hacs, self.local_path): + return False + if os.path.exists(self.backup_path): + shutil.rmtree(self.backup_path) + + # Wait for the folder to be removed + while os.path.exists(self.backup_path): + sleep(0.1) + os.makedirs(self.backup_path, exist_ok=True) + return True + + def create(self) -> None: + """Create a backup in /tmp""" + if not self._init_backup_dir(): + return + + try: + if os.path.isfile(self.local_path): + shutil.copyfile(self.local_path, self.backup_path_full) + os.remove(self.local_path) + else: + shutil.copytree(self.local_path, self.backup_path_full) + shutil.rmtree(self.local_path) + while os.path.exists(self.local_path): + sleep(0.1) + self.hacs.log.debug( + "Backup for %s, created in %s", + self.local_path, + self.backup_path_full, + ) + except ( + BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except + ) as exception: + self.hacs.log.warning("Could not create backup: %s", exception) + + def restore(self) -> None: + """Restore from backup.""" + if not os.path.exists(self.backup_path_full): + return + + if os.path.isfile(self.backup_path_full): + if os.path.exists(self.local_path): + os.remove(self.local_path) + shutil.copyfile(self.backup_path_full, self.local_path) + else: + if os.path.exists(self.local_path): + shutil.rmtree(self.local_path) + while os.path.exists(self.local_path): + sleep(0.1) + shutil.copytree(self.backup_path_full, self.local_path) + self.hacs.log.debug("Restored %s, from backup %s", self.local_path, self.backup_path_full) + + def cleanup(self) -> None: + """Cleanup backup files.""" + if not os.path.exists(self.backup_path): + return + + shutil.rmtree(self.backup_path) + + # Wait for the folder to be removed + while os.path.exists(self.backup_path): + sleep(0.1) + self.hacs.log.debug("Backup dir %s cleared", self.backup_path) diff --git a/homeassistant/config/custom_components/hacs/utils/configuration_schema.py b/homeassistant/config/custom_components/hacs/utils/configuration_schema.py new file mode 100644 index 0000000..b003bf7 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/configuration_schema.py @@ -0,0 +1,9 @@ +"""HACS Configuration Schemas.""" + +# Configuration: +SIDEPANEL_TITLE = "sidepanel_title" +SIDEPANEL_ICON = "sidepanel_icon" +APPDAEMON = "appdaemon" + +# Options: +COUNTRY = "country" diff --git a/homeassistant/config/custom_components/hacs/utils/data.py b/homeassistant/config/custom_components/hacs/utils/data.py new file mode 100644 index 0000000..f540272 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/data.py @@ -0,0 +1,323 @@ +"""Data handler for HACS.""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime +from typing import Any + +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError + +from ..base import HacsBase +from ..const import HACS_REPOSITORY_ID +from ..enums import HacsDisabledReason, HacsDispatchEvent +from ..repositories.base import TOPIC_FILTER, HacsManifest, HacsRepository +from .logger import LOGGER +from .path import is_safe +from .store import async_load_from_store, async_save_to_store + +EXPORTED_BASE_DATA = ( + ("new", False), + ("full_name", ""), +) + +EXPORTED_REPOSITORY_DATA = EXPORTED_BASE_DATA + ( + ("authors", []), + ("category", ""), + ("description", ""), + ("domain", None), + ("downloads", 0), + ("etag_repository", None), + ("hide", False), + ("last_updated", 0), + ("new", False), + ("stargazers_count", 0), + ("topics", []), +) + +EXPORTED_DOWNLOADED_REPOSITORY_DATA = EXPORTED_REPOSITORY_DATA + ( + ("archived", False), + ("config_flow", False), + ("default_branch", None), + ("first_install", False), + ("installed_commit", None), + ("installed", False), + ("last_commit", None), + ("last_version", None), + ("manifest_name", None), + ("open_issues", 0), + ("prerelease", None), + ("published_tags", []), + ("releases", False), + ("selected_tag", None), + ("show_beta", False), +) + + +class HacsData: + """HacsData class.""" + + def __init__(self, hacs: HacsBase): + """Initialize.""" + self.logger = LOGGER + self.hacs = hacs + self.content = {} + + async def async_force_write(self, _=None): + """Force write.""" + await self.async_write(force=True) + + async def async_write(self, force: bool = False) -> None: + """Write content to the store files.""" + if not force and self.hacs.system.disabled: + return + + self.logger.debug(" Saving data") + + # Hacs + await async_save_to_store( + self.hacs.hass, + "hacs", + { + "archived_repositories": self.hacs.common.archived_repositories, + "renamed_repositories": self.hacs.common.renamed_repositories, + "ignored_repositories": self.hacs.common.ignored_repositories, + }, + ) + await self._async_store_experimental_content_and_repos() + await self._async_store_content_and_repos() + + async def _async_store_content_and_repos(self, _=None): # bb: ignore + """Store the main repos file and each repo that is out of date.""" + # Repositories + self.content = {} + for repository in self.hacs.repositories.list_all: + if repository.data.category in self.hacs.common.categories: + self.async_store_repository_data(repository) + + await async_save_to_store(self.hacs.hass, "repositories", self.content) + for event in (HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.CONFIG): + self.hacs.async_dispatch(event, {}) + + async def _async_store_experimental_content_and_repos(self, _=None): + """Store the main repos file and each repo that is out of date.""" + # Repositories + self.content = {} + for repository in self.hacs.repositories.list_all: + if repository.data.category in self.hacs.common.categories: + self.async_store_experimental_repository_data(repository) + + await async_save_to_store(self.hacs.hass, "data", {"repositories": self.content}) + + @callback + def async_store_repository_data(self, repository: HacsRepository) -> dict: + """Store the repository data.""" + data = {"repository_manifest": repository.repository_manifest.manifest} + + for key, default in ( + EXPORTED_DOWNLOADED_REPOSITORY_DATA + if repository.data.installed + else EXPORTED_REPOSITORY_DATA + ): + if (value := getattr(repository.data, key, default)) != default: + data[key] = value + + if repository.data.installed_version: + data["version_installed"] = repository.data.installed_version + if repository.data.last_fetched: + data["last_fetched"] = repository.data.last_fetched.timestamp() + + self.content[str(repository.data.id)] = data + + @callback + def async_store_experimental_repository_data(self, repository: HacsRepository) -> None: + """Store the experimental repository data for non downloaded repositories.""" + data = {} + self.content.setdefault(repository.data.category, []) + + if repository.data.installed: + data["repository_manifest"] = repository.repository_manifest.manifest + for key, default in EXPORTED_DOWNLOADED_REPOSITORY_DATA: + if (value := getattr(repository.data, key, default)) != default: + data[key] = value + + if repository.data.installed_version: + data["version_installed"] = repository.data.installed_version + if repository.data.last_fetched: + data["last_fetched"] = repository.data.last_fetched.timestamp() + else: + for key, default in EXPORTED_BASE_DATA: + if (value := getattr(repository.data, key, default)) != default: + data[key] = value + + self.content[repository.data.category].append({"id": str(repository.data.id), **data}) + + async def restore(self): + """Restore saved data.""" + self.hacs.status.new = False + repositories = {} + hacs = {} + + try: + hacs = await async_load_from_store(self.hacs.hass, "hacs") or {} + except HomeAssistantError: + pass + + try: + repositories = await async_load_from_store(self.hacs.hass, "repositories") + if not repositories and (data := await async_load_from_store(self.hacs.hass, "data")): + for category, entries in data.get("repositories", {}).items(): + for repository in entries: + repositories[repository["id"]] = {"category": category, **repository} + + except HomeAssistantError as exception: + self.hacs.log.error( + "Could not read %s, restore the file from a backup - %s", + self.hacs.hass.config.path(".storage/hacs.data"), + exception, + ) + self.hacs.disable_hacs(HacsDisabledReason.RESTORE) + return False + + if not hacs and not repositories: + # Assume new install + self.hacs.status.new = True + return True + + self.logger.info(" Restore started") + + # Hacs + self.hacs.common.archived_repositories = set() + self.hacs.common.ignored_repositories = set() + self.hacs.common.renamed_repositories = {} + + # Clear out doubble renamed values + renamed = hacs.get("renamed_repositories", {}) + for entry in renamed: + value = renamed.get(entry) + if value not in renamed: + self.hacs.common.renamed_repositories[entry] = value + + # Clear out doubble archived values + for entry in hacs.get("archived_repositories", set()): + if entry not in self.hacs.common.archived_repositories: + self.hacs.common.archived_repositories.add(entry) + + # Clear out doubble ignored values + for entry in hacs.get("ignored_repositories", set()): + if entry not in self.hacs.common.ignored_repositories: + self.hacs.common.ignored_repositories.add(entry) + + try: + await self.register_unknown_repositories(repositories) + + for entry, repo_data in repositories.items(): + if entry == "0": + # Ignore repositories with ID 0 + self.logger.debug( + " Found repository with ID %s - %s", entry, repo_data + ) + continue + self.async_restore_repository(entry, repo_data) + + self.logger.info(" Restore done") + except ( + # lgtm [py/catch-base-exception] pylint: disable=broad-except + BaseException + ) as exception: + self.logger.critical( + " [%s] Restore Failed!", exception, exc_info=exception + ) + return False + return True + + async def register_unknown_repositories( + self, repositories: dict[str, dict[str, Any]], category: str | None = None + ): + """Registry any unknown repositories.""" + for repo_idx, (entry, repo_data) in enumerate(repositories.items()): + # async_register_repository is awaited in a loop + # since its unlikely to ever suspend at startup + if ( + entry == "0" + or repo_data.get("category", category) is None + or self.hacs.repositories.is_registered(repository_id=entry) + ): + continue + await self.hacs.async_register_repository( + repository_full_name=repo_data["full_name"], + category=repo_data.get("category", category), + check=False, + repository_id=entry, + ) + if repo_idx % 100 == 0: + # yield to avoid blocking the event loop + await asyncio.sleep(0) + + @callback + def async_restore_repository(self, entry: str, repository_data: dict[str, Any]): + """Restore repository.""" + repository: HacsRepository | None = None + if full_name := repository_data.get("full_name"): + repository = self.hacs.repositories.get_by_full_name(full_name) + if not repository: + repository = self.hacs.repositories.get_by_id(entry) + if not repository: + return + + try: + self.hacs.repositories.set_repository_id(repository, entry) + except ValueError as exception: + self.logger.warning(" duplicate IDs %s", exception) + return + + # Restore repository attributes + repository.data.authors = repository_data.get("authors", []) + repository.data.description = repository_data.get("description", "") + repository.data.downloads = repository_data.get("downloads", 0) + repository.data.last_updated = repository_data.get("last_updated", 0) + if self.hacs.system.generator: + repository.data.etag_releases = repository_data.get("etag_releases") + repository.data.open_issues = repository_data.get("open_issues", 0) + repository.data.etag_repository = repository_data.get("etag_repository") + repository.data.topics = [ + topic for topic in repository_data.get("topics", []) if topic not in TOPIC_FILTER + ] + repository.data.domain = repository_data.get("domain") + repository.data.stargazers_count = repository_data.get( + "stargazers_count" + ) or repository_data.get("stars", 0) + repository.releases.last_release = repository_data.get("last_release_tag") + repository.data.releases = repository_data.get("releases", False) + repository.data.installed = repository_data.get("installed", False) + repository.data.new = repository_data.get("new", False) + repository.data.selected_tag = repository_data.get("selected_tag") + repository.data.show_beta = repository_data.get("show_beta", False) + repository.data.last_version = repository_data.get("last_version") + repository.data.prerelease = repository_data.get("prerelease") + repository.data.last_commit = repository_data.get("last_commit") + repository.data.installed_version = repository_data.get("version_installed") + repository.data.installed_commit = repository_data.get("installed_commit") + repository.data.manifest_name = repository_data.get("manifest_name") + + if last_fetched := repository_data.get("last_fetched"): + repository.data.last_fetched = datetime.fromtimestamp(last_fetched, UTC) + + repository.repository_manifest = HacsManifest.from_dict( + repository_data.get("manifest") or repository_data.get("repository_manifest") or {} + ) + + if repository.data.prerelease == repository.data.last_version: + repository.data.prerelease = None + + if repository.localpath is not None and is_safe(self.hacs, repository.localpath): + # Set local path + repository.content.path.local = repository.localpath + + if repository.data.installed: + repository.data.first_install = False + + if entry == HACS_REPOSITORY_ID: + repository.data.installed_version = self.hacs.version + repository.data.installed = True diff --git a/homeassistant/config/custom_components/hacs/utils/decode.py b/homeassistant/config/custom_components/hacs/utils/decode.py new file mode 100644 index 0000000..708f22f --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/decode.py @@ -0,0 +1,8 @@ +"""Util to decode content from the github API.""" + +from base64 import b64decode + + +def decode_content(content: str) -> str: + """Decode content.""" + return b64decode(bytearray(content, "utf-8")).decode() diff --git a/homeassistant/config/custom_components/hacs/utils/decorator.py b/homeassistant/config/custom_components/hacs/utils/decorator.py new file mode 100644 index 0000000..7c5e76a --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/decorator.py @@ -0,0 +1,43 @@ +"""HACS Decorators.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +from functools import wraps +from typing import TYPE_CHECKING, Any + +from ..const import DEFAULT_CONCURRENT_BACKOFF_TIME, DEFAULT_CONCURRENT_TASKS + +if TYPE_CHECKING: + from ..base import HacsBase + + +def concurrent( + concurrenttasks: int = DEFAULT_CONCURRENT_TASKS, + backoff_time: int = DEFAULT_CONCURRENT_BACKOFF_TIME, +) -> Coroutine[Any, Any, None]: + """Return a modified function.""" + + max_concurrent = asyncio.Semaphore(concurrenttasks) + + def inner_function(function) -> Coroutine[Any, Any, None]: + @wraps(function) + async def wrapper(*args, **kwargs) -> None: + hacs: HacsBase = getattr(args[0], "hacs", None) + + async with max_concurrent: + result = await function(*args, **kwargs) + if ( + hacs is None + or hacs.queue is None + or hacs.queue.has_pending_tasks + or "update" not in function.__name__ + ): + await asyncio.sleep(backoff_time) + + return result + + return wrapper + + return inner_function diff --git a/homeassistant/config/custom_components/hacs/utils/file_system.py b/homeassistant/config/custom_components/hacs/utils/file_system.py new file mode 100644 index 0000000..e3af18e --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/file_system.py @@ -0,0 +1,42 @@ +"""File system functions.""" + +from __future__ import annotations + +import os +import shutil +from typing import TypeAlias + +from homeassistant.core import HomeAssistant + +# From typeshed +StrOrBytesPath: TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes] +FileDescriptorOrPath: TypeAlias = int | StrOrBytesPath + + +async def async_exists(hass: HomeAssistant, path: FileDescriptorOrPath) -> bool: + """Test whether a path exists.""" + return await hass.async_add_executor_job(os.path.exists, path) + + +async def async_remove( + hass: HomeAssistant, path: StrOrBytesPath, *, missing_ok: bool = False +) -> None: + """Remove a path.""" + try: + return await hass.async_add_executor_job(os.remove, path) + except FileNotFoundError: + if missing_ok: + return + raise + + +async def async_remove_directory( + hass: HomeAssistant, path: StrOrBytesPath, *, missing_ok: bool = False +) -> None: + """Remove a directory.""" + try: + return await hass.async_add_executor_job(shutil.rmtree, path) + except FileNotFoundError: + if missing_ok: + return + raise diff --git a/homeassistant/config/custom_components/hacs/utils/filters.py b/homeassistant/config/custom_components/hacs/utils/filters.py new file mode 100644 index 0000000..1482c75 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/filters.py @@ -0,0 +1,47 @@ +"""Filter functions.""" + +from __future__ import annotations + +from typing import Any + + +def filter_content_return_one_of_type( + content: list[str | Any], + namestartswith: str, + filterfiltype: str, + attr: str = "name", +) -> list[str]: + """Only match 1 of the filter.""" + contents = [] + filetypefound = False + for filename in content: + if isinstance(filename, str): + if filename.startswith(namestartswith): + if filename.endswith(f".{filterfiltype}"): + if not filetypefound: + contents.append(filename) + filetypefound = True + continue + else: + contents.append(filename) + else: + if getattr(filename, attr).startswith(namestartswith): + if getattr(filename, attr).endswith(f".{filterfiltype}"): + if not filetypefound: + contents.append(filename) + filetypefound = True + continue + else: + contents.append(filename) + return contents + + +def get_first_directory_in_directory(content: list[str | Any], dirname: str) -> str | None: + """Return the first directory in dirname or None.""" + directory = None + for path in content: + if path.full_path.startswith(dirname) and path.full_path != dirname: + if path.is_directory: + directory = path.filename + break + return directory diff --git a/homeassistant/config/custom_components/hacs/utils/github_graphql_query.py b/homeassistant/config/custom_components/hacs/utils/github_graphql_query.py new file mode 100644 index 0000000..43ceb74 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/github_graphql_query.py @@ -0,0 +1,19 @@ +"""GitHub GraphQL Queries.""" + +GET_REPOSITORY_RELEASES = """ +query ($owner: String!, $name: String!, $first: Int!) { + rateLimit { + cost + } + repository(owner: $owner, name: $name) { + releases(first: $first, orderBy: {field: CREATED_AT, direction: DESC}) { + nodes { + tagName + name + isPrerelease + publishedAt + } + } + } +} +""" diff --git a/homeassistant/config/custom_components/hacs/utils/json.py b/homeassistant/config/custom_components/hacs/utils/json.py new file mode 100644 index 0000000..b490cb8 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/json.py @@ -0,0 +1,5 @@ +"""JSON utils.""" + +from homeassistant.util.json import json_loads + +__all__ = ["json_loads"] diff --git a/homeassistant/config/custom_components/hacs/utils/logger.py b/homeassistant/config/custom_components/hacs/utils/logger.py new file mode 100644 index 0000000..47a77c0 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/logger.py @@ -0,0 +1,7 @@ +"""Custom logger for HACS.""" + +import logging + +from ..const import PACKAGE_NAME + +LOGGER: logging.Logger = logging.getLogger(PACKAGE_NAME) diff --git a/homeassistant/config/custom_components/hacs/utils/path.py b/homeassistant/config/custom_components/hacs/utils/path.py new file mode 100644 index 0000000..7994a8d --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/path.py @@ -0,0 +1,41 @@ +"""Path utils""" + +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..base import HacsBase + + +@lru_cache(maxsize=1) +def _get_safe_paths( + config_path: str, + appdaemon_path: str, + plugin_path: str, + python_script_path: str, + theme_path: str, +) -> set[str]: + """Get safe paths.""" + return { + Path(f"{config_path}/{appdaemon_path}").as_posix(), + Path(f"{config_path}/{plugin_path}").as_posix(), + Path(f"{config_path}/{python_script_path}").as_posix(), + Path(f"{config_path}/{theme_path}").as_posix(), + Path(f"{config_path}/custom_components/").as_posix(), + Path(f"{config_path}/custom_templates/").as_posix(), + } + + +def is_safe(hacs: HacsBase, path: str | Path) -> bool: + """Helper to check if path is safe to remove.""" + configuration = hacs.configuration + return Path(path).as_posix() not in _get_safe_paths( + hacs.core.config_path, + configuration.appdaemon_path, + configuration.plugin_path, + configuration.python_script_path, + configuration.theme_path, + ) diff --git a/homeassistant/config/custom_components/hacs/utils/queue_manager.py b/homeassistant/config/custom_components/hacs/utils/queue_manager.py new file mode 100644 index 0000000..e8498be --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/queue_manager.py @@ -0,0 +1,82 @@ +"""The QueueManager class.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +import time + +from homeassistant.core import HomeAssistant + +from ..exceptions import HacsExecutionStillInProgress +from .logger import LOGGER + +_LOGGER = LOGGER + + +class QueueManager: + """The QueueManager class.""" + + def __init__(self, hass: HomeAssistant) -> None: + self.hass = hass + self.queue: list[Coroutine] = [] + self.running = False + + @property + def pending_tasks(self) -> int: + """Return a count of pending tasks in the queue.""" + return len(self.queue) + + @property + def has_pending_tasks(self) -> bool: + """Return a count of pending tasks in the queue.""" + return self.pending_tasks != 0 + + def clear(self) -> None: + """Clear the queue.""" + self.queue = [] + + def add(self, task: Coroutine) -> None: + """Add a task to the queue.""" + self.queue.append(task) + + async def execute(self, number_of_tasks: int | None = None) -> None: + """Execute the tasks in the queue.""" + if self.running: + _LOGGER.debug(" Execution is already running") + raise HacsExecutionStillInProgress + if len(self.queue) == 0: + _LOGGER.debug(" The queue is empty") + return + + self.running = True + + _LOGGER.debug(" Checking out tasks to execute") + local_queue = [] + + if number_of_tasks: + for task in self.queue[:number_of_tasks]: + local_queue.append(task) + else: + for task in self.queue: + local_queue.append(task) + + _LOGGER.debug(" Starting queue execution for %s tasks", len(local_queue)) + start = time.time() + result = await asyncio.gather(*local_queue, return_exceptions=True) + for entry in result: + if isinstance(entry, Exception): + _LOGGER.error(" %s", entry) + end = time.time() - start + + for task in local_queue: + self.queue.remove(task) + + _LOGGER.debug( + " Queue execution finished for %s tasks finished in %.2f seconds", + len(local_queue), + end, + ) + if self.has_pending_tasks: + _LOGGER.debug(" %s tasks remaining in the queue", len(self.queue)) + self.running = False diff --git a/homeassistant/config/custom_components/hacs/utils/regex.py b/homeassistant/config/custom_components/hacs/utils/regex.py new file mode 100644 index 0000000..845e403 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/regex.py @@ -0,0 +1,17 @@ +"""Regex utils""" + +from __future__ import annotations + +import re + +RE_REPOSITORY = re.compile( + r"(?:(?:.*github.com.)|^)([A-Za-z0-9-]+\/[\w.-]+?)(?:(?:\.git)?|(?:[^\w.-].*)?)$" +) + + +def extract_repository_from_url(url: str) -> str | None: + """Extract the owner/repo part form a URL.""" + match = re.match(RE_REPOSITORY, url) + if not match: + return None + return match.group(1).lower() diff --git a/homeassistant/config/custom_components/hacs/utils/store.py b/homeassistant/config/custom_components/hacs/utils/store.py new file mode 100644 index 0000000..f0afa07 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/store.py @@ -0,0 +1,79 @@ +"""Storage handers.""" + +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store +from homeassistant.util import json as json_util + +from ..const import VERSION_STORAGE +from ..exceptions import HacsException +from .logger import LOGGER + +_LOGGER = LOGGER + + +class HACSStore(Store): + """A subclass of Store that allows multiple loads in the executor.""" + + def load(self): + """Load the data from disk if version matches.""" + try: + data = json_util.load_json(self.path) + except ( + BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except + ) as exception: + _LOGGER.critical( + "Could not load '%s', restore it from a backup or delete the file: %s", + self.path, + exception, + ) + raise HacsException(exception) from exception + if data == {} or data["version"] != self.version: + return None + return data["data"] + + +def get_store_key(key): + """Return the key to use with homeassistant.helpers.storage.Storage.""" + return key if "/" in key else f"hacs.{key}" + + +def _get_store_for_key(hass, key, encoder): + """Create a Store object for the key.""" + return HACSStore(hass, VERSION_STORAGE, get_store_key(key), encoder=encoder, atomic_writes=True) + + +def get_store_for_key(hass, key): + """Create a Store object for the key.""" + return _get_store_for_key(hass, key, JSONEncoder) + + +async def async_load_from_store(hass, key): + """Load the retained data from store and return de-serialized data.""" + return await get_store_for_key(hass, key).async_load() or {} + + +async def async_save_to_store(hass, key, data): + """Generate dynamic data to store and save it to the filesystem. + + The data is only written if the content on the disk has changed + by reading the existing content and comparing it. + + If the data has changed this will generate two executor jobs + + If the data has not changed this will generate one executor job + """ + current = await async_load_from_store(hass, key) + if current is None or current != data: + await get_store_for_key(hass, key).async_save(data) + return + _LOGGER.debug( + " Did not store data for '%s'. Content did not change", + get_store_key(key), + ) + + +async def async_remove_store(hass, key): + """Remove a store element that should no longer be used.""" + if "/" not in key: + return + await get_store_for_key(hass, key).async_remove() diff --git a/homeassistant/config/custom_components/hacs/utils/url.py b/homeassistant/config/custom_components/hacs/utils/url.py new file mode 100644 index 0000000..b4b1a57 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/url.py @@ -0,0 +1,30 @@ +"""Various URL utils for HACS.""" + +import re +from typing import Literal + +GIT_SHA = re.compile(r"^[a-fA-F0-9]{40}$") + + +def github_release_asset( + *, + repository: str, + version: str, + filename: str, + **_, +) -> str: + """Generate a download URL for a release asset.""" + return f"https://github.com/{repository}/releases/download/{version}/{filename}" + + +def github_archive( + *, + repository: str, + version: str, + variant: Literal["heads", "tags"] = "heads", + **_, +) -> str: + """Generate a download URL for a repository zip.""" + if GIT_SHA.match(version): + return f"https://github.com/{repository}/archive/{version}.zip" + return f"https://github.com/{repository}/archive/refs/{variant}/{version}.zip" diff --git a/homeassistant/config/custom_components/hacs/utils/validate.py b/homeassistant/config/custom_components/hacs/utils/validate.py new file mode 100644 index 0000000..fa25be9 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/validate.py @@ -0,0 +1,215 @@ +"""Validation utilities.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + +from awesomeversion import AwesomeVersion +from homeassistant.helpers.config_validation import url as url_validator +import voluptuous as vol + +from ..const import LOCALE + + +@dataclass +class Validate: + """Validate.""" + + errors: list[str] = field(default_factory=list) + + @property + def success(self) -> bool: + """Return bool if the validation was a success.""" + return len(self.errors) == 0 + + +def _country_validator(values) -> list[str]: + """Custom country validator.""" + countries = [] + if isinstance(values, str): + countries.append(values.upper()) + elif isinstance(values, list): + for value in values: + countries.append(value.upper()) + else: + raise vol.Invalid(f"Value '{values}' is not a string or list.", path=["country"]) + + for country in countries: + if country not in LOCALE: + raise vol.Invalid(f"Value '{country}' is not in {LOCALE}.", path=["country"]) + + return countries + + +HACS_MANIFEST_JSON_SCHEMA = vol.Schema( + { + vol.Optional("content_in_root"): bool, + vol.Optional("country"): _country_validator, + vol.Optional("filename"): str, + vol.Optional("hacs"): str, + vol.Optional("hide_default_branch"): bool, + vol.Optional("homeassistant"): str, + vol.Optional("persistent_directory"): str, + vol.Optional("render_readme"): bool, + vol.Optional("zip_release"): bool, + vol.Required("name"): str, + }, + extra=vol.PREVENT_EXTRA, +) + +INTEGRATION_MANIFEST_JSON_SCHEMA = vol.Schema( + { + vol.Required("codeowners"): list, + vol.Required("documentation"): url_validator, + vol.Required("domain"): str, + vol.Required("issue_tracker"): url_validator, + vol.Required("name"): str, + vol.Required("version"): vol.Coerce(AwesomeVersion), + }, + extra=vol.ALLOW_EXTRA, +) + + +def validate_repo_data(schema: dict[str, Any], extra: int) -> Callable[[Any], Any]: + """Return a validator for repo data. + + This is used instead of vol.All to always try both the repo schema and + and the validate_version validator. + """ + _schema = vol.Schema(schema, extra=extra) + + def validate_repo_data(data: Any) -> Any: + """Validate integration repo data.""" + schema_errors: vol.MultipleInvalid | None = None + try: + _schema(data) + except vol.MultipleInvalid as err: + schema_errors = err + try: + validate_version(data) + except vol.Invalid as err: + if schema_errors: + schema_errors.add(err) + else: + raise + if schema_errors: + raise schema_errors + return data + + return validate_repo_data + + +def validate_version(data: Any) -> Any: + """Ensure at least one of last_commit or last_version is present.""" + if "last_commit" not in data and "last_version" not in data: + raise vol.Invalid("Expected at least one of [`last_commit`, `last_version`], got none") + return data + + +V2_COMMON_DATA_JSON_SCHEMA = { + vol.Required("description"): vol.Any(str, None), + vol.Optional("downloads"): int, + vol.Optional("etag_releases"): str, + vol.Required("etag_repository"): str, + vol.Required("full_name"): str, + vol.Optional("last_commit"): str, + vol.Required("last_fetched"): vol.Any(int, float), + vol.Required("last_updated"): str, + vol.Optional("last_version"): str, + vol.Optional("prerelease"): str, + vol.Required("manifest"): { + vol.Optional("country"): vol.Any([str], False), + vol.Optional("name"): str, + }, + vol.Optional("open_issues"): int, + vol.Optional("stargazers_count"): int, + vol.Optional("topics"): [str], +} + +V2_INTEGRATION_DATA_JSON_SCHEMA = { + **V2_COMMON_DATA_JSON_SCHEMA, + vol.Required("domain"): str, + vol.Required("manifest_name"): str, +} + +_V2_REPO_SCHEMAS = { + "appdaemon": V2_COMMON_DATA_JSON_SCHEMA, + "integration": V2_INTEGRATION_DATA_JSON_SCHEMA, + "plugin": V2_COMMON_DATA_JSON_SCHEMA, + "python_script": V2_COMMON_DATA_JSON_SCHEMA, + "template": V2_COMMON_DATA_JSON_SCHEMA, + "theme": V2_COMMON_DATA_JSON_SCHEMA, +} + +# Used when validating repos in the hacs integration, discards extra keys +VALIDATE_FETCHED_V2_REPO_DATA = { + category: validate_repo_data(schema, vol.REMOVE_EXTRA) + for category, schema in _V2_REPO_SCHEMAS.items() +} + +# Used when validating repos when generating data, fails on extra keys +VALIDATE_GENERATED_V2_REPO_DATA = { + category: vol.Schema({str: validate_repo_data(schema, vol.PREVENT_EXTRA)}) + for category, schema in _V2_REPO_SCHEMAS.items() +} + +V2_CRITICAL_REPO_DATA_SCHEMA = { + vol.Required("link"): str, + vol.Required("reason"): str, + vol.Required("repository"): str, +} + +# Used when validating critical repos in the hacs integration, discards extra keys +VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA = vol.Schema( + V2_CRITICAL_REPO_DATA_SCHEMA, + extra=vol.REMOVE_EXTRA, +) + +# Used when validating critical repos when generating data, fails on extra keys +VALIDATE_GENERATED_V2_CRITICAL_REPO_SCHEMA = vol.Schema( + [ + vol.Schema( + V2_CRITICAL_REPO_DATA_SCHEMA, + extra=vol.PREVENT_EXTRA, + ) + ] +) + +V2_REMOVED_REPO_DATA_SCHEMA = { + vol.Optional("link"): str, + vol.Optional("reason"): str, + vol.Required("removal_type"): vol.In( + [ + "Integration is missing a version, and is abandoned.", + "Remove", + "archived", + "blacklist", + "critical", + "deprecated", + "removal", + "remove", + "removed", + "replaced", + "repository", + ] + ), + vol.Required("repository"): str, +} + +# Used when validating removed repos in the hacs integration, discards extra keys +VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA = vol.Schema( + V2_REMOVED_REPO_DATA_SCHEMA, + extra=vol.REMOVE_EXTRA, +) + +# Used when validating removed repos when generating data, fails on extra keys +VALIDATE_GENERATED_V2_REMOVED_REPO_SCHEMA = vol.Schema( + [ + vol.Schema( + V2_REMOVED_REPO_DATA_SCHEMA, + extra=vol.PREVENT_EXTRA, + ) + ] +) diff --git a/homeassistant/config/custom_components/hacs/utils/version.py b/homeassistant/config/custom_components/hacs/utils/version.py new file mode 100644 index 0000000..6c44b2c --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/version.py @@ -0,0 +1,36 @@ +"""Version utils.""" + +from __future__ import annotations + +from functools import lru_cache + +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) + + +@lru_cache(maxsize=1024) +def version_left_higher_then_right(left: str, right: str) -> bool | None: + """Return a bool if source is newer than target, will also be true if identical.""" + try: + left_version = AwesomeVersion(left) + right_version = AwesomeVersion(right) + if ( + left_version.strategy != AwesomeVersionStrategy.UNKNOWN + and right_version.strategy != AwesomeVersionStrategy.UNKNOWN + ): + return left_version > right_version + except (AwesomeVersionException, AttributeError, KeyError): + pass + + return None + + +def version_left_higher_or_equal_then_right(left: str, right: str) -> bool: + """Return a bool if source is newer than target, will also be true if identical.""" + if left == right: + return True + + return version_left_higher_then_right(left, right) diff --git a/homeassistant/config/custom_components/hacs/utils/workarounds.py b/homeassistant/config/custom_components/hacs/utils/workarounds.py new file mode 100644 index 0000000..4d7867c --- /dev/null +++ b/homeassistant/config/custom_components/hacs/utils/workarounds.py @@ -0,0 +1,37 @@ +"""Workarounds.""" + +from homeassistant.core import HomeAssistant + +DOMAIN_OVERRIDES = { + # https://github.com/hacs/integration/issues/2465 + "custom-components/sensor.custom_aftership": "custom_aftership" +} + + +try: + from homeassistant.components.http import StaticPathConfig + + async def async_register_static_path( + hass: HomeAssistant, + url_path: str, + path: str, + cache_headers: bool = True, + ) -> None: + """Register a static path with the HTTP component.""" + await hass.http.async_register_static_paths( + [StaticPathConfig(url_path, path, cache_headers)] + ) +except ImportError: + + async def async_register_static_path( + hass: HomeAssistant, + url_path: str, + path: str, + cache_headers: bool = True, + ) -> None: + """Register a static path with the HTTP component. + + Legacy: Can be removed when min version is 2024.7 + https://developers.home-assistant.io/blog/2024/06/18/async_register_static_paths/ + """ + hass.http.register_static_path(url_path, path, cache_headers) diff --git a/homeassistant/config/custom_components/hacs/validate/README.md b/homeassistant/config/custom_components/hacs/validate/README.md new file mode 100644 index 0000000..08e2bc4 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/README.md @@ -0,0 +1,30 @@ +# Repository validation + +This is where the validation rules that run against the various repository categories live. + +## Structure + +- There is one file pr. rule. +- All rule needs tests to verify every possible outcome for the rule. +- It's better with multiple files than a big rule. +- All rules uses `ActionValidationBase` as the base class. +- Only use `validate` or `async_validate` methods to define validation rules. +- If a rule should fail, raise `ValidationException` with the failure message. + + +## Example + +```python +from .base import ( + ActionValidationBase, + ValidationBase, + ValidationException, +) + +class SuperAwesomeRepository(ActionValidationBase): + category = "integration" + + async def async_validate(self): + if self.repository != "super-awesome": + raise ValidationException("The repository is not super-awesome") +``` \ No newline at end of file diff --git a/homeassistant/config/custom_components/hacs/validate/__init__.py b/homeassistant/config/custom_components/hacs/validate/__init__.py new file mode 100644 index 0000000..43eaa43 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/__init__.py @@ -0,0 +1 @@ +"""Initialize validation.""" diff --git a/homeassistant/config/custom_components/hacs/validate/archived.py b/homeassistant/config/custom_components/hacs/validate/archived.py new file mode 100644 index 0000000..2205d56 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/archived.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-archived" + allow_fork = False + + async def async_validate(self) -> None: + """Validate the repository.""" + if self.repository.data.archived: + raise ValidationException("The repository is archived") diff --git a/homeassistant/config/custom_components/hacs/validate/base.py b/homeassistant/config/custom_components/hacs/validate/base.py new file mode 100644 index 0000000..c69e37a --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/base.py @@ -0,0 +1,54 @@ +"""Base class for validation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from ..exceptions import HacsException + +if TYPE_CHECKING: + from ..enums import HacsCategory + from ..repositories.base import HacsRepository + + +class ValidationException(HacsException): + """Raise when there is a validation issue.""" + + +class ActionValidationBase: + """Base class for action validation.""" + + categories: tuple[HacsCategory, ...] = () + allow_fork: bool = True + more_info: str = "https://hacs.xyz/docs/publish/action" + + def __init__(self, repository: HacsRepository) -> None: + self.hacs = repository.hacs + self.repository = repository + self.failed = False + + @property + def slug(self) -> str: + """Return the check slug.""" + return self.__class__.__module__.rsplit(".", maxsplit=1)[-1] + + async def async_validate(self) -> None: + """Validate the repository.""" + + async def execute_validation(self, *_: Any, **__: Any) -> None: + """Execute the task defined in subclass.""" + self.failed = False + + try: + await self.async_validate() + except ValidationException as exception: + self.failed = True + self.hacs.log.error( + " failed: %s (More info: %s )", + self.slug, + exception, + self.more_info, + ) + + else: + self.hacs.log.info(" completed", self.slug) diff --git a/homeassistant/config/custom_components/hacs/validate/brands.py b/homeassistant/config/custom_components/hacs/validate/brands.py new file mode 100644 index 0000000..20869c9 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/brands.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from custom_components.hacs.enums import HacsCategory + +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + +URL = "https://brands.home-assistant.io/domains.json" + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-brands" + categories = (HacsCategory.INTEGRATION,) + + async def async_validate(self) -> None: + """Validate the repository.""" + + response = await self.hacs.session.get(URL) + content = await response.json() + + if self.repository.data.domain not in content["custom"]: + raise ValidationException( + "The repository has not been added as a custom domain to the brands repo" + ) diff --git a/homeassistant/config/custom_components/hacs/validate/description.py b/homeassistant/config/custom_components/hacs/validate/description.py new file mode 100644 index 0000000..1dbe9f8 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/description.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-repository" + allow_fork = False + + async def async_validate(self) -> None: + """Validate the repository.""" + if not self.repository.data.description: + raise ValidationException("The repository has no description") diff --git a/homeassistant/config/custom_components/hacs/validate/hacsjson.py b/homeassistant/config/custom_components/hacs/validate/hacsjson.py new file mode 100644 index 0000000..ea4a610 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/hacsjson.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from voluptuous.error import Invalid +from voluptuous.humanize import humanize_error + +from ..enums import HacsCategory, RepositoryFile +from ..repositories.base import HacsManifest, HacsRepository +from ..utils.validate import HACS_MANIFEST_JSON_SCHEMA +from .base import ActionValidationBase, ValidationException + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-hacs-manifest" + + async def async_validate(self) -> None: + """Validate the repository.""" + if RepositoryFile.HACS_JSON not in [x.filename for x in self.repository.tree]: + raise ValidationException(f"The repository has no '{RepositoryFile.HACS_JSON}' file") + + content = await self.repository.async_get_hacs_json(self.repository.ref) + try: + hacsjson = HacsManifest.from_dict(HACS_MANIFEST_JSON_SCHEMA(content)) + except Invalid as exception: + raise ValidationException(humanize_error(content, exception)) from exception + + if self.repository.data.category == HacsCategory.INTEGRATION: + if hacsjson.zip_release and not hacsjson.filename: + raise ValidationException("zip_release is True, but filename is not set") diff --git a/homeassistant/config/custom_components/hacs/validate/images.py b/homeassistant/config/custom_components/hacs/validate/images.py new file mode 100644 index 0000000..313a7d3 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/images.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..enums import HacsCategory +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + +IGNORED = ["-shield", "img.shields.io", "buymeacoffee.com"] + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + categories = (HacsCategory.PLUGIN, HacsCategory.THEME) + more_info = "https://hacs.xyz/docs/publish/include#check-images" + + async def async_validate(self) -> None: + """Validate the repository.""" + info = await self.repository.async_get_info_file_contents(version=self.repository.ref) + for line in info.split("\n"): + if " Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-info" + + async def async_validate(self) -> None: + """Validate the repository.""" + filenames = [x.filename.lower() for x in self.repository.tree] + if "readme" in filenames: + pass + elif "readme.md" in filenames: + pass + elif "info" in filenames: + pass + elif "info.md" in filenames: + pass + else: + raise ValidationException("The repository has no information file") diff --git a/homeassistant/config/custom_components/hacs/validate/integration_manifest.py b/homeassistant/config/custom_components/hacs/validate/integration_manifest.py new file mode 100644 index 0000000..edd54b1 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/integration_manifest.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from voluptuous.error import Invalid + +from ..enums import HacsCategory, RepositoryFile +from ..utils.validate import INTEGRATION_MANIFEST_JSON_SCHEMA +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + from ..repositories.integration import HacsIntegrationRepository + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + repository: HacsIntegrationRepository + more_info = "https://hacs.xyz/docs/publish/include#check-manifest" + categories = (HacsCategory.INTEGRATION,) + + async def async_validate(self) -> None: + """Validate the repository.""" + if RepositoryFile.MAINIFEST_JSON not in [x.filename for x in self.repository.tree]: + raise ValidationException( + f"The repository has no '{RepositoryFile.MAINIFEST_JSON}' file" + ) + + content = await self.repository.get_integration_manifest(version=self.repository.ref) + try: + INTEGRATION_MANIFEST_JSON_SCHEMA(content) + except Invalid as exception: + raise ValidationException(exception) from exception diff --git a/homeassistant/config/custom_components/hacs/validate/issues.py b/homeassistant/config/custom_components/hacs/validate/issues.py new file mode 100644 index 0000000..88afa6e --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/issues.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-repository" + allow_fork = False + + async def async_validate(self) -> None: + """Validate the repository.""" + if not self.repository.data.has_issues: + raise ValidationException("The repository does not have issues enabled") diff --git a/homeassistant/config/custom_components/hacs/validate/manager.py b/homeassistant/config/custom_components/hacs/validate/manager.py new file mode 100644 index 0000000..94d3f88 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/manager.py @@ -0,0 +1,81 @@ +"""Hacs validation manager.""" + +from __future__ import annotations + +import asyncio +from importlib import import_module +import os +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from ..base import HacsBase + from ..repositories.base import HacsRepository + from .base import ActionValidationBase + + +class ValidationManager: + """Hacs validation manager.""" + + def __init__(self, hacs: HacsBase, hass: HomeAssistant) -> None: + """Initialize the setup manager class.""" + self.hacs = hacs + self.hass = hass + self._validators: dict[str, ActionValidationBase] = {} + + @property + def validators(self) -> list[ActionValidationBase]: + """Return all list of all tasks.""" + return list(self._validators.values()) + + async def async_load(self, repository: HacsRepository) -> None: + """Load all tasks.""" + self._validators = {} + validator_files = Path(__file__).parent + validator_modules = ( + module.stem + for module in validator_files.glob("*.py") + if module.name not in ("base.py", "__init__.py", "manager.py") + ) + + async def _load_module(module: str) -> None: + task_module = import_module(f"{__package__}.{module}") + if task := await task_module.async_setup_validator(repository=repository): + self._validators[task.slug] = task + + await asyncio.gather(*[_load_module(task) for task in validator_modules]) + + async def async_run_repository_checks(self, repository: HacsRepository) -> None: + """Run all validators for a repository.""" + if not self.hacs.system.action: + return + + await self.async_load(repository) + + is_pull_from_fork = ( + not os.getenv("INPUT_REPOSITORY") + and os.getenv("GITHUB_REPOSITORY") != repository.data.full_name + ) + + validators = [ + validator + for validator in self.validators or [] + if ( + (not validator.categories or repository.data.category in validator.categories) + and validator.slug not in os.getenv("INPUT_IGNORE", "").split(" ") + and (not is_pull_from_fork or validator.allow_fork) + ) + ] + + await asyncio.gather(*[validator.execute_validation() for validator in validators]) + + total = len(validators) + failed = len([x for x in validators if x.failed]) + + if failed != 0: + repository.logger.error("%s %s/%s checks failed", repository.string, failed, total) + exit(1) + else: + repository.logger.info("%s All (%s) checks passed", repository.string, total) diff --git a/homeassistant/config/custom_components/hacs/validate/topics.py b/homeassistant/config/custom_components/hacs/validate/topics.py new file mode 100644 index 0000000..ec8ddd1 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/validate/topics.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import ActionValidationBase, ValidationException + +if TYPE_CHECKING: + from ..repositories.base import HacsRepository + + +async def async_setup_validator(repository: HacsRepository) -> Validator: + """Set up this validator.""" + return Validator(repository=repository) + + +class Validator(ActionValidationBase): + """Validate the repository.""" + + more_info = "https://hacs.xyz/docs/publish/include#check-repository" + allow_fork = False + + async def async_validate(self) -> None: + """Validate the repository.""" + if not self.repository.data.topics: + raise ValidationException("The repository has no valid topics") diff --git a/homeassistant/config/custom_components/hacs/websocket/__init__.py b/homeassistant/config/custom_components/hacs/websocket/__init__.py new file mode 100644 index 0000000..6e9e2ac --- /dev/null +++ b/homeassistant/config/custom_components/hacs/websocket/__init__.py @@ -0,0 +1,123 @@ +"""Register_commands.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import voluptuous as vol + +from ..const import DOMAIN +from .critical import hacs_critical_acknowledge, hacs_critical_list +from .repositories import ( + hacs_repositories_add, + hacs_repositories_clear_new, + hacs_repositories_list, + hacs_repositories_remove, + hacs_repositories_removed, +) +from .repository import ( + hacs_repository_beta, + hacs_repository_download, + hacs_repository_ignore, + hacs_repository_info, + hacs_repository_refresh, + hacs_repository_release_notes, + hacs_repository_releases, + hacs_repository_remove, + hacs_repository_state, + hacs_repository_version, +) + +if TYPE_CHECKING: + from ..base import HacsBase + + +@callback +def async_register_websocket_commands(hass: HomeAssistant) -> None: + """Register_commands.""" + websocket_api.async_register_command(hass, hacs_info) + websocket_api.async_register_command(hass, hacs_subscribe) + + websocket_api.async_register_command(hass, hacs_repository_info) + websocket_api.async_register_command(hass, hacs_repository_download) + websocket_api.async_register_command(hass, hacs_repository_ignore) + websocket_api.async_register_command(hass, hacs_repository_state) + websocket_api.async_register_command(hass, hacs_repository_version) + websocket_api.async_register_command(hass, hacs_repository_beta) + websocket_api.async_register_command(hass, hacs_repository_refresh) + websocket_api.async_register_command(hass, hacs_repository_release_notes) + websocket_api.async_register_command(hass, hacs_repository_remove) + + websocket_api.async_register_command(hass, hacs_critical_acknowledge) + websocket_api.async_register_command(hass, hacs_critical_list) + + websocket_api.async_register_command(hass, hacs_repositories_list) + websocket_api.async_register_command(hass, hacs_repositories_add) + websocket_api.async_register_command(hass, hacs_repositories_clear_new) + websocket_api.async_register_command(hass, hacs_repositories_removed) + websocket_api.async_register_command(hass, hacs_repositories_remove) + websocket_api.async_register_command(hass, hacs_repository_releases) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/subscribe", + vol.Required("signal"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_subscribe( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Handle websocket subscriptions.""" + + @callback + def forward_messages(data: dict | None = None) -> None: + """Forward events to websocket.""" + connection.send_message(websocket_api.event_message(msg["id"], data)) + + connection.subscriptions[msg["id"]] = async_dispatcher_connect( + hass, + msg["signal"], + forward_messages, + ) + connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/info", + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return information about HACS.""" + hacs: HacsBase = hass.data.get(DOMAIN) + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "categories": hacs.common.categories, + "country": hacs.configuration.country, + "debug": hacs.configuration.debug, + "dev": hacs.configuration.dev, + "disabled_reason": hacs.system.disabled_reason, + "has_pending_tasks": hacs.queue.has_pending_tasks, + "lovelace_mode": hacs.core.lovelace_mode, + "stage": hacs.stage, + "startup": hacs.status.startup, + "version": hacs.version, + }, + ) + ) diff --git a/homeassistant/config/custom_components/hacs/websocket/critical.py b/homeassistant/config/custom_components/hacs/websocket/critical.py new file mode 100644 index 0000000..a0258a7 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/websocket/critical.py @@ -0,0 +1,59 @@ +"""Register info websocket commands.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components import websocket_api +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from ..utils.store import async_load_from_store, async_save_to_store + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/critical/list", + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_critical_list( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """List critical repositories.""" + connection.send_message( + websocket_api.result_message( + msg["id"], + (await async_load_from_store(hass, "critical") or []), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/critical/acknowledge", + vol.Optional("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_critical_acknowledge( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Acknowledge critical repository.""" + repository = msg["repository"] + + critical = await async_load_from_store(hass, "critical") + for repo in critical: + if repository == repo["repository"]: + repo["acknowledged"] = True + await async_save_to_store(hass, "critical", critical) + connection.send_message(websocket_api.result_message(msg["id"], critical)) diff --git a/homeassistant/config/custom_components/hacs/websocket/repositories.py b/homeassistant/config/custom_components/hacs/websocket/repositories.py new file mode 100644 index 0000000..879f68a --- /dev/null +++ b/homeassistant/config/custom_components/hacs/websocket/repositories.py @@ -0,0 +1,216 @@ +"""Register info websocket commands.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Any + +from homeassistant.components import websocket_api +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from custom_components.hacs.utils import regex + +from ..const import DOMAIN +from ..enums import HacsDispatchEvent + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from ..base import HacsBase + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repositories/list", + vol.Optional("categories"): [str], + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repositories_list( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """List repositories.""" + hacs: HacsBase = hass.data.get(DOMAIN) + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + { + "authors": repo.data.authors, + "available_version": repo.display_available_version, + "installed_version": repo.display_installed_version, + "config_flow": repo.data.config_flow, + "can_download": repo.can_download, + "category": repo.data.category, + "country": repo.repository_manifest.country, + "custom": not hacs.repositories.is_default(str(repo.data.id)), + "description": repo.data.description, + "domain": repo.data.domain, + "downloads": repo.data.downloads, + "file_name": repo.data.file_name, + "full_name": repo.data.full_name, + "hide": repo.data.hide, + "homeassistant": repo.repository_manifest.homeassistant, + "id": repo.data.id, + "installed": repo.data.installed, + "last_updated": repo.data.last_updated, + "local_path": repo.content.path.local, + "name": repo.display_name, + "new": repo.data.new, + "pending_upgrade": repo.pending_update, + "stars": repo.data.stargazers_count, + "state": repo.state, + "status": repo.display_status, + "topics": repo.data.topics, + } + for repo in hacs.repositories.list_all + if repo.data.category in msg.get("categories", hacs.common.categories) + and not repo.ignored_by_country_configuration + and repo.data.last_fetched + ], + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repositories/clear_new", + vol.Optional("categories"): cv.ensure_list, + vol.Optional("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repositories_clear_new( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Clear new repositories for specific categories.""" + hacs: HacsBase = hass.data.get(DOMAIN) + + if repo := msg.get("repository"): + repository = hacs.repositories.get_by_id(repo) + repository.data.new = False + + else: + for repo in hacs.repositories.list_all: + if repo.data.new and repo.data.category in msg.get("categories", []): + hacs.log.debug( + "Clearing new flag from '%s'", + repo.data.full_name, + ) + repo.data.new = False + hacs.async_dispatch(HacsDispatchEvent.REPOSITORY, {}) + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repositories/removed", + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repositories_removed( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get information about removed repositories.""" + hacs: HacsBase = hass.data.get(DOMAIN) + content = [] + for repo in hacs.repositories.list_removed: + if repo.repository not in hacs.common.ignored_repositories: + content.append(repo.to_json()) + connection.send_message(websocket_api.result_message(msg["id"], content)) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repositories/add", + vol.Required("repository"): cv.string, + vol.Required("category"): vol.Lower, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repositories_add( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Add custom repositoriy.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = regex.extract_repository_from_url(msg["repository"]) + category = msg["category"] + + if repository is None: + return + + if repository in hacs.common.skip: + hacs.common.skip.remove(repository) + + if renamed := hacs.common.renamed_repositories.get(repository): + repository = renamed + + if category not in hacs.common.categories: + hacs.log.error("%s is not a valid category for %s", category, repository) + + elif not hacs.repositories.get_by_full_name(repository): + try: + await hacs.async_register_repository( + repository_full_name=repository, + category=category, + ) + + except ( + BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except + ) as exception: + hacs.async_dispatch( + HacsDispatchEvent.ERROR, + { + "action": "add_repository", + "exception": str(sys.exc_info()[0].__name__), + "message": str(exception), + }, + ) + + else: + hacs.async_dispatch( + HacsDispatchEvent.ERROR, + { + "action": "add_repository", + "message": f"Repository '{repository}' exists in the store.", + }, + ) + + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repositories/remove", + vol.Required("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repositories_remove( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Remove custom repositoriy.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + repository.remove() + await hacs.data.async_write() + + connection.send_message(websocket_api.result_message(msg["id"], {})) diff --git a/homeassistant/config/custom_components/hacs/websocket/repository.py b/homeassistant/config/custom_components/hacs/websocket/repository.py new file mode 100644 index 0000000..7075238 --- /dev/null +++ b/homeassistant/config/custom_components/hacs/websocket/repository.py @@ -0,0 +1,369 @@ +"""Register info websocket commands.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components import websocket_api +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from ..const import DOMAIN +from ..enums import HacsDispatchEvent +from ..exceptions import HacsException +from ..utils.version import version_left_higher_then_right + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from ..base import HacsBase + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/info", + vol.Required("repository_id"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return information about a repository.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository_id = msg["repository_id"] + repository = hacs.repositories.get_by_id(repository_id) + if repository is None: + connection.send_error( + msg["id"], + "repository_not_found", + f"Repository with ID ({repository_id}) not found", + ) + return + + if not repository.updated_info: + try: + await repository.update_repository(ignore_issues=True, force=True) + except Exception as exception: # pylint: disable=broad-except + repository.logger.error("%s %s", repository.string, exception) + repository.updated_info = True + + if repository.data.new: + repository.data.new = False + await hacs.data.async_write() + + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "additional_info": repository.additional_info, + "authors": repository.data.authors, + "available_version": repository.display_available_version, + "beta": repository.data.show_beta, + "can_download": repository.can_download, + "category": repository.data.category, + "config_flow": repository.data.config_flow, + "country": repository.repository_manifest.country, + "custom": not hacs.repositories.is_default(str(repository.data.id)), + "default_branch": repository.data.default_branch, + "description": repository.data.description, + "domain": repository.data.domain, + "downloads": repository.data.downloads, + "file_name": repository.data.file_name, + "full_name": repository.data.full_name, + "hide_default_branch": repository.repository_manifest.hide_default_branch, + "homeassistant": repository.repository_manifest.homeassistant, + "id": repository.data.id, + "installed_version": repository.display_installed_version, + "installed": repository.data.installed, + "issues": repository.data.open_issues, + "last_updated": repository.data.last_updated, + "local_path": repository.content.path.local, + "name": repository.display_name, + "new": False, + "pending_upgrade": repository.pending_update, + "releases": repository.data.published_tags, + "ref": repository.ref, + "selected_tag": repository.data.selected_tag, + "stars": repository.data.stargazers_count, + "state": repository.state, + "status": repository.display_status, + "topics": repository.data.topics, + "version_or_commit": repository.display_version_or_commit, + }, + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/ignore", + vol.Required("repository"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_ignore( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Ignore a repository.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository_id = msg["repository"] + hacs.log.info("Ignoring %s", repository_id) + repository = hacs.repositories.get_by_id(repository_id) + if repository is None: + connection.send_error( + msg["id"], + "repository_not_found", + f"Repository with ID ({repository_id}) not found", + ) + return + + hacs.common.ignored_repositories.add(repository.data.full_name) + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/state", + vol.Required("repository"): cv.string, + vol.Required("state"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_state( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Set the state of a repository""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + repository.state = msg["state"] + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/version", + vol.Required("repository"): cv.string, + vol.Required("version"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_version( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Set the version of a repository""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + if msg["version"] == repository.data.default_branch: + repository.data.selected_tag = None + else: + repository.data.selected_tag = msg["version"] + + await repository.update_repository(force=True) + repository.state = None + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/beta", + vol.Required("repository"): cv.string, + vol.Required("show_beta"): cv.boolean, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_beta( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Show or hide beta versions of a repository""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + repository.data.show_beta = msg["show_beta"] + + await repository.update_repository(force=True) + repository.state = None + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/download", + vol.Required("repository"): cv.string, + vol.Optional("version"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_download( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Set the version of a repository""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + try: + was_installed = repository.data.installed + await repository.async_download_repository(ref=msg.get("version")) + if not was_installed: + hacs.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True}) + await hacs.async_recreate_entities() + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) + except HacsException as exception: + repository.logger.error("%s %s", repository.string, exception) + connection.send_error(msg["id"], "error", str(exception)) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/remove", + vol.Required("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_remove( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Remove a repository.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + repository.data.new = False + try: + await repository.update_repository(ignore_issues=True, force=True) + except Exception as exception: # pylint: disable=broad-except + repository.logger.error("%s %s", repository.string, exception) + await repository.uninstall() + + await hacs.data.async_write() + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/refresh", + vol.Required("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_refresh( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Refresh a repository.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + await repository.update_repository(ignore_issues=True, force=True) + await hacs.data.async_write() + # Update state of update entity + hacs.coordinators[repository.data.category].async_update_listeners() + + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/release_notes", + vol.Required("repository"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_release_notes( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return release notes.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository"]) + + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + { + "name": x.name, + "body": x.body, + "tag": x.tag_name, + } + for x in repository.releases.objects + if not repository.data.installed_version + or version_left_higher_then_right(x.tag_name, repository.data.installed_version) + ], + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hacs/repository/releases", + vol.Required("repository_id"): cv.string, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def hacs_repository_releases( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return releases.""" + hacs: HacsBase = hass.data.get(DOMAIN) + repository = hacs.repositories.get_by_id(msg["repository_id"]) + try: + releases = await repository.async_get_releases() + except Exception as exception: + hacs.log.exception(exception) + connection.send_error(msg["id"], "unknown", str(exception)) + return + + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + { + "name": release.name, + "tag": release.tag_name, + "published_at": release.published_at, + "prerelease": release.prerelease, + } + for release in releases + ], + ) + ) diff --git a/homeassistant/config/custom_components/localtuya/__init__.py b/homeassistant/config/custom_components/localtuya/__init__.py new file mode 100644 index 0000000..a54656d --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/__init__.py @@ -0,0 +1,378 @@ +"""The LocalTuya integration.""" +import asyncio +import logging +import time +from datetime import timedelta + +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.entity_registry as er +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DEVICE_ID, + CONF_DEVICES, + CONF_ENTITIES, + CONF_HOST, + CONF_ID, + CONF_PLATFORM, + CONF_REGION, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import async_register_admin_service + +from .cloud_api import TuyaCloudApi +from .common import TuyaDevice, async_config_entry_by_device_id +from .config_flow import ENTRIES_VERSION, config_schema +from .const import ( + ATTR_UPDATED_AT, + CONF_NO_CLOUD, + CONF_PRODUCT_KEY, + CONF_USER_ID, + DATA_CLOUD, + DATA_DISCOVERY, + DOMAIN, + TUYA_DEVICES, +) +from .discovery import TuyaDiscovery + +_LOGGER = logging.getLogger(__name__) + +UNSUB_LISTENER = "unsub_listener" + +RECONNECT_INTERVAL = timedelta(seconds=60) + +CONFIG_SCHEMA = config_schema() + +CONF_DP = "dp" +CONF_VALUE = "value" + +SERVICE_SET_DP = "set_dp" +SERVICE_SET_DP_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_DP): int, + vol.Required(CONF_VALUE): object, + } +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the LocalTuya integration component.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][TUYA_DEVICES] = {} + + device_cache = {} + + async def _handle_reload(service): + """Handle reload service call.""" + _LOGGER.info("Service %s.reload called: reloading integration", DOMAIN) + + current_entries = hass.config_entries.async_entries(DOMAIN) + + reload_tasks = [ + hass.config_entries.async_reload(entry.entry_id) + for entry in current_entries + ] + + await asyncio.gather(*reload_tasks) + + async def _handle_set_dp(event): + """Handle set_dp service call.""" + dev_id = event.data[CONF_DEVICE_ID] + if dev_id not in hass.data[DOMAIN][TUYA_DEVICES]: + raise HomeAssistantError("unknown device id") + + device = hass.data[DOMAIN][TUYA_DEVICES][dev_id] + if not device.connected: + raise HomeAssistantError("not connected to device") + + await device.set_dp(event.data[CONF_VALUE], event.data[CONF_DP]) + + def _device_discovered(device): + """Update address of device if it has changed.""" + device_ip = device["ip"] + device_id = device["gwId"] + product_key = device["productKey"] + + # If device is not in cache, check if a config entry exists + entry = async_config_entry_by_device_id(hass, device_id) + if entry is None: + return + + if device_id not in device_cache: + if entry and device_id in entry.data[CONF_DEVICES]: + # Save address from config entry in cache to trigger + # potential update below + host_ip = entry.data[CONF_DEVICES][device_id][CONF_HOST] + device_cache[device_id] = host_ip + + if device_id not in device_cache: + return + + dev_entry = entry.data[CONF_DEVICES][device_id] + + new_data = entry.data.copy() + updated = False + + if device_cache[device_id] != device_ip: + updated = True + new_data[CONF_DEVICES][device_id][CONF_HOST] = device_ip + device_cache[device_id] = device_ip + + if dev_entry.get(CONF_PRODUCT_KEY) != product_key: + updated = True + new_data[CONF_DEVICES][device_id][CONF_PRODUCT_KEY] = product_key + + # Update settings if something changed, otherwise try to connect. Updating + # settings triggers a reload of the config entry, which tears down the device + # so no need to connect in that case. + if updated: + _LOGGER.debug( + "Updating keys for device %s: %s %s", device_id, device_ip, product_key + ) + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + hass.config_entries.async_update_entry(entry, data=new_data) + + elif device_id in hass.data[DOMAIN][TUYA_DEVICES]: + _LOGGER.debug("Device %s found with IP %s", device_id, device_ip) + + device = hass.data[DOMAIN][TUYA_DEVICES].get(device_id) + if not device: + _LOGGER.warning(f"Could not find device for device_id {device_id}") + elif not device.connected: + device.async_connect() + + + def _shutdown(event): + """Clean up resources when shutting down.""" + discovery.close() + + async def _async_reconnect(now): + """Try connecting to devices not already connected to.""" + for device_id, device in hass.data[DOMAIN][TUYA_DEVICES].items(): + if not device.connected: + device.async_connect() + + async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + _handle_reload, + ) + + hass.services.async_register( + DOMAIN, SERVICE_SET_DP, _handle_set_dp, schema=SERVICE_SET_DP_SCHEMA + ) + + discovery = TuyaDiscovery(_device_discovered) + try: + await discovery.start() + hass.data[DOMAIN][DATA_DISCOVERY] = discovery + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("failed to set up discovery") + + return True + + +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entries merging all of them in one.""" + new_version = ENTRIES_VERSION + stored_entries = hass.config_entries.async_entries(DOMAIN) + if config_entry.version == 1: + _LOGGER.debug("Migrating config entry from version %s", config_entry.version) + + if config_entry.entry_id == stored_entries[0].entry_id: + _LOGGER.debug( + "Migrating the first config entry (%s)", config_entry.entry_id + ) + new_data = {} + new_data[CONF_REGION] = "eu" + new_data[CONF_CLIENT_ID] = "" + new_data[CONF_CLIENT_SECRET] = "" + new_data[CONF_USER_ID] = "" + new_data[CONF_USERNAME] = DOMAIN + new_data[CONF_NO_CLOUD] = True + new_data[CONF_DEVICES] = { + config_entry.data[CONF_DEVICE_ID]: config_entry.data.copy() + } + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + config_entry.version = new_version + hass.config_entries.async_update_entry( + config_entry, title=DOMAIN, data=new_data + ) + else: + _LOGGER.debug( + "Merging the config entry %s into the main one", config_entry.entry_id + ) + new_data = stored_entries[0].data.copy() + new_data[CONF_DEVICES].update( + {config_entry.data[CONF_DEVICE_ID]: config_entry.data.copy()} + ) + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + hass.config_entries.async_update_entry(stored_entries[0], data=new_data) + await hass.config_entries.async_remove(config_entry.entry_id) + + _LOGGER.info( + "Entry %s successfully migrated to version %s.", + config_entry.entry_id, + new_version, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up LocalTuya integration from a config entry.""" + if entry.version < ENTRIES_VERSION: + _LOGGER.debug( + "Skipping setup for entry %s since its version (%s) is old", + entry.entry_id, + entry.version, + ) + return + + region = entry.data[CONF_REGION] + client_id = entry.data[CONF_CLIENT_ID] + secret = entry.data[CONF_CLIENT_SECRET] + user_id = entry.data[CONF_USER_ID] + tuya_api = TuyaCloudApi(hass, region, client_id, secret, user_id) + no_cloud = True + if CONF_NO_CLOUD in entry.data: + no_cloud = entry.data.get(CONF_NO_CLOUD) + if no_cloud: + _LOGGER.info("Cloud API account not configured.") + # wait 1 second to make sure possible migration has finished + await asyncio.sleep(1) + else: + res = await tuya_api.async_get_access_token() + if res != "ok": + _LOGGER.error("Cloud API connection failed: %s", res) + else: + _LOGGER.info("Cloud API connection succeeded.") + res = await tuya_api.async_get_devices_list() + hass.data[DOMAIN][DATA_CLOUD] = tuya_api + + platforms = set() + for dev_id in entry.data[CONF_DEVICES].keys(): + entities = entry.data[CONF_DEVICES][dev_id][CONF_ENTITIES] + platforms = platforms.union( + set(entity[CONF_PLATFORM] for entity in entities) + ) + hass.data[DOMAIN][TUYA_DEVICES][dev_id] = TuyaDevice(hass, entry, dev_id) + + # Setup all platforms at once, letting HA handling each platform and avoiding + # potential integration restarts while elements are still initialising. + await hass.config_entries.async_forward_entry_setups(entry, platforms) + + async def setup_entities(device_ids): + for dev_id in device_ids: + hass.data[DOMAIN][TUYA_DEVICES][dev_id].async_connect() + + await async_remove_orphan_entities(hass, entry) + + hass.async_create_task(setup_entities(entry.data[CONF_DEVICES].keys())) + + unsub_listener = entry.add_update_listener(update_listener) + hass.data[DOMAIN][entry.entry_id] = {UNSUB_LISTENER: unsub_listener} + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + platforms = {} + + for dev_id, dev_entry in entry.data[CONF_DEVICES].items(): + for entity in dev_entry[CONF_ENTITIES]: + platforms[entity[CONF_PLATFORM]] = True + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in platforms + ] + ) + ) + + hass.data[DOMAIN][entry.entry_id][UNSUB_LISTENER]() + for dev_id, device in hass.data[DOMAIN][TUYA_DEVICES].items(): + if device.connected: + await device.close() + + if unload_ok: + hass.data[DOMAIN][TUYA_DEVICES] = {} + + return True + + +async def update_listener(hass, config_entry): + """Update listener.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + dev_id = list(device_entry.identifiers)[0][1].split("_")[-1] + + ent_reg = er.async_get(hass) + entities = { + ent.unique_id: ent.entity_id + for ent in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) + if dev_id in ent.unique_id + } + for entity_id in entities.values(): + ent_reg.async_remove(entity_id) + + if dev_id not in config_entry.data[CONF_DEVICES]: + _LOGGER.info( + "Device %s not found in config entry: finalizing device removal", dev_id + ) + return True + + await hass.data[DOMAIN][TUYA_DEVICES][dev_id].close() + + new_data = config_entry.data.copy() + new_data[CONF_DEVICES].pop(dev_id) + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + ) + + _LOGGER.info("Device %s removed.", dev_id) + + return True + + +async def async_remove_orphan_entities(hass, entry): + """Remove entities associated with config entry that has been removed.""" + return + ent_reg = er.async_get(hass) + entities = { + ent.unique_id: ent.entity_id + for ent in er.async_entries_for_config_entry(ent_reg, entry.entry_id) + } + _LOGGER.info("ENTITIES ORPHAN %s", entities) + return + + for entity in entry.data[CONF_ENTITIES]: + if entity[CONF_ID] in entities: + del entities[entity[CONF_ID]] + + for entity_id in entities.values(): + ent_reg.async_remove(entity_id) diff --git a/homeassistant/config/custom_components/localtuya/binary_sensor.py b/homeassistant/config/custom_components/localtuya/binary_sensor.py new file mode 100644 index 0000000..273880c --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/binary_sensor.py @@ -0,0 +1,76 @@ +"""Platform to present any Tuya DP as a binary sensor.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + DOMAIN, + BinarySensorEntity, +) +from homeassistant.const import CONF_DEVICE_CLASS + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_STATE_ON = "state_on" +CONF_STATE_OFF = "state_off" + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Required(CONF_STATE_ON, default="True"): str, + vol.Required(CONF_STATE_OFF, default="False"): str, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + } + + +class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity): + """Representation of a Tuya binary sensor.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya binary sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._is_on = False + + @property + def is_on(self): + """Return sensor state.""" + return self._is_on + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + def status_updated(self): + """Device status was updated.""" + super().status_updated() + + state = str(self.dps(self._dp_id)).lower() + if state == self._config[CONF_STATE_ON].lower(): + self._is_on = True + elif state == self._config[CONF_STATE_OFF].lower(): + self._is_on = False + else: + self.warning( + "State for entity %s did not match state patterns", self.entity_id + ) + + # No need to restore state for a sensor + async def restore_state_when_connected(self): + """Do nothing for a sensor.""" + return + + +async_setup_entry = partial( + async_setup_entry, DOMAIN, LocaltuyaBinarySensor, flow_schema +) diff --git a/homeassistant/config/custom_components/localtuya/climate.py b/homeassistant/config/custom_components/localtuya/climate.py new file mode 100644 index 0000000..1bf3b9b --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/climate.py @@ -0,0 +1,522 @@ +"""Platform to locally control Tuya-based climate devices.""" +import asyncio +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.climate import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + DOMAIN, + ClimateEntity, +) +from homeassistant.components.climate.const import ( + HVACAction, + HVACMode, + PRESET_AWAY, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, + ClimateEntityFeature, + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_TOP, + SWING_ON, + SWING_OFF, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_TEMPERATURE_UNIT, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + UnitOfTemperature, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_CURRENT_TEMPERATURE_DP, + CONF_TEMP_MAX, + CONF_TEMP_MIN, + CONF_ECO_DP, + CONF_ECO_VALUE, + CONF_HEURISTIC_ACTION, + CONF_HVAC_ACTION_DP, + CONF_HVAC_ACTION_SET, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, + CONF_MAX_TEMP_DP, + CONF_MIN_TEMP_DP, + CONF_PRECISION, + CONF_PRESET_DP, + CONF_PRESET_SET, + CONF_TARGET_PRECISION, + CONF_TARGET_TEMPERATURE_DP, + CONF_TEMPERATURE_STEP, + CONF_HVAC_FAN_MODE_DP, + CONF_HVAC_FAN_MODE_SET, + CONF_HVAC_SWING_MODE_DP, + CONF_HVAC_SWING_MODE_SET, +) + +_LOGGER = logging.getLogger(__name__) + +HVAC_MODE_SETS = { + "manual/auto": { + HVACMode.HEAT: "manual", + HVACMode.AUTO: "auto", + }, + "Manual/Auto": { + HVACMode.HEAT: "Manual", + HVACMode.AUTO: "Auto", + }, + "MANUAL/AUTO": { + HVACMode.HEAT: "MANUAL", + HVACMode.AUTO: "AUTO", + }, + "Manual/Program": { + HVACMode.HEAT: "Manual", + HVACMode.AUTO: "Program", + }, + "m/p": { + HVACMode.HEAT: "m", + HVACMode.AUTO: "p", + }, + "True/False": { + HVACMode.HEAT: True, + }, + "Auto/Cold/Dry/Wind/Hot": { + HVACMode.HEAT: "hot", + HVACMode.FAN_ONLY: "wind", + HVACMode.DRY: "wet", + HVACMode.COOL: "cold", + HVACMode.AUTO: "auto", + }, + "Cold/Dehumidify/Hot": { + HVACMode.HEAT: "hot", + HVACMode.DRY: "dehumidify", + HVACMode.COOL: "cold", + }, + "1/0": { + HVACMode.HEAT: "1", + HVACMode.AUTO: "0", + }, +} +HVAC_ACTION_SETS = { + "True/False": { + HVACAction.HEATING: True, + HVACAction.IDLE: False, + }, + "open/close": { + HVACAction.HEATING: "open", + HVACAction.IDLE: "close", + }, + "heating/no_heating": { + HVACAction.HEATING: "heating", + HVACAction.IDLE: "no_heating", + }, + "Heat/Warming": { + HVACAction.HEATING: "Heat", + HVACAction.IDLE: "Warming", + }, + "heating/warming": { + HVACAction.HEATING: "heating", + HVACAction.IDLE: "warming", + }, +} +HVAC_FAN_MODE_SETS = { + "Auto/Low/Middle/High/Strong": { + FAN_AUTO: "auto", + FAN_LOW: "low", + FAN_MEDIUM: "middle", + FAN_HIGH: "high", + FAN_TOP: "strong", + } +} +HVAC_SWING_MODE_SETS = { + "True/False": { + SWING_ON: True, + SWING_OFF: False, + } +} +PRESET_SETS = { + "Manual/Holiday/Program": { + PRESET_AWAY: "Holiday", + PRESET_HOME: "Program", + PRESET_NONE: "Manual", + }, + "smart/holiday/hold": { + PRESET_AWAY: "holiday", + PRESET_HOME: "smart", + PRESET_NONE: "hold", + }, +} + +TEMPERATURE_CELSIUS = "celsius" +TEMPERATURE_FAHRENHEIT = "fahrenheit" +DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS +DEFAULT_PRECISION = PRECISION_TENTHS +DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES +# Empirically tested to work for AVATTO thermostat +MODE_WAIT = 0.1 + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_TARGET_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_CURRENT_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_TEMPERATURE_STEP, default=PRECISION_WHOLE): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP_DP): vol.In(dps), + vol.Optional(CONF_MIN_TEMP_DP): vol.In(dps), + vol.Optional(CONF_PRECISION, default=PRECISION_WHOLE): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps), + vol.Optional(CONF_HVAC_MODE_SET): vol.In(list(HVAC_MODE_SETS.keys())), + vol.Optional(CONF_HVAC_FAN_MODE_DP): vol.In(dps), + vol.Optional(CONF_HVAC_FAN_MODE_SET): vol.In(list(HVAC_FAN_MODE_SETS.keys())), + vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps), + vol.Optional(CONF_HVAC_ACTION_SET): vol.In(list(HVAC_ACTION_SETS.keys())), + vol.Optional(CONF_ECO_DP): vol.In(dps), + vol.Optional(CONF_ECO_VALUE): str, + vol.Optional(CONF_PRESET_DP): vol.In(dps), + vol.Optional(CONF_PRESET_SET): vol.In(list(PRESET_SETS.keys())), + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( + [TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT] + ), + vol.Optional(CONF_TARGET_PRECISION, default=PRECISION_WHOLE): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_HEURISTIC_ACTION): bool, + } + + +class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): + """Tuya climate device.""" + + def __init__( + self, + device, + config_entry, + switchid, + **kwargs, + ): + """Initialize a new LocaltuyaClimate.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + self._state = None + self._target_temperature = None + self._current_temperature = None + self._hvac_mode = None + self._fan_mode = None + self._swing_mode = None + self._preset_mode = None + self._hvac_action = None + self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION) + self._target_precision = self._config.get( + CONF_TARGET_PRECISION, self._precision + ) + self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP) + self._conf_hvac_mode_set = HVAC_MODE_SETS.get( + self._config.get(CONF_HVAC_MODE_SET), {} + ) + self._conf_hvac_fan_mode_dp = self._config.get(CONF_HVAC_FAN_MODE_DP) + self._conf_hvac_fan_mode_set = HVAC_FAN_MODE_SETS.get( + self._config.get(CONF_HVAC_FAN_MODE_SET), {} + ) + self._conf_hvac_swing_mode_dp = self._config.get(CONF_HVAC_SWING_MODE_DP) + self._conf_hvac_swing_mode_set = HVAC_SWING_MODE_SETS.get( + self._config.get(CONF_HVAC_SWING_MODE_SET), {} + ) + self._conf_preset_dp = self._config.get(CONF_PRESET_DP) + self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {}) + self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP) + self._conf_hvac_action_set = HVAC_ACTION_SETS.get( + self._config.get(CONF_HVAC_ACTION_SET), {} + ) + self._conf_eco_dp = self._config.get(CONF_ECO_DP) + self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") + self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config( + CONF_PRESET_DP + ) + _LOGGER.debug("Initialized climate [%s]", self.name) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + supported_features = supported_features | ClimateEntityFeature.TARGET_TEMPERATURE + if self.has_config(CONF_MAX_TEMP_DP): + supported_features = supported_features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP): + supported_features = supported_features | ClimateEntityFeature.PRESET_MODE + if self.has_config(CONF_HVAC_FAN_MODE_DP) and self.has_config(CONF_HVAC_FAN_MODE_SET): + supported_features = supported_features | ClimateEntityFeature.FAN_MODE + if self.has_config(CONF_HVAC_SWING_MODE_DP): + supported_features = supported_features | ClimateEntityFeature.SWING_MODE + return supported_features + + @property + def precision(self): + """Return the precision of the system.""" + return self._precision + + @property + def target_precision(self): + """Return the precision of the target.""" + return self._target_precision + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + if ( + self._config.get(CONF_TEMPERATURE_UNIT, DEFAULT_TEMPERATURE_UNIT) + == TEMPERATURE_FAHRENHEIT + ): + return UnitOfTemperature.FAHRENHEIT + return UnitOfTemperature.CELSIUS + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self._hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + if not self.has_config(CONF_HVAC_MODE_DP): + return None + return list(self._conf_hvac_mode_set) + [HVACMode.OFF] + + @property + def hvac_action(self): + """Return the current running hvac operation if supported. + + Need to be one of CURRENT_HVAC_*. + """ + if self._config.get(CONF_HEURISTIC_ACTION, False): + if self._hvac_mode == HVACMode.HEAT: + if self._current_temperature < ( + self._target_temperature - self._precision + ): + self._hvac_action = HVACAction.HEATING + if self._current_temperature == ( + self._target_temperature - self._precision + ): + if self._hvac_action == HVACAction.HEATING: + self._hvac_action = HVACAction.HEATING + if self._hvac_action == HVACAction.IDLE: + self._hvac_action = HVACAction.IDLE + if ( + self._current_temperature + self._precision + ) > self._target_temperature: + self._hvac_action = HVACAction.IDLE + return self._hvac_action + return self._hvac_action + + @property + def preset_mode(self): + """Return current preset.""" + return self._preset_mode + + @property + def preset_modes(self): + """Return the list of available presets modes.""" + if not self._has_presets: + return None + presets = list(self._conf_preset_set) + if self._conf_eco_dp: + presets.append(PRESET_ECO) + return presets + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._config.get(CONF_TEMPERATURE_STEP, DEFAULT_TEMPERATURE_STEP) + + @property + def fan_mode(self): + """Return the fan setting.""" + return self._fan_mode + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + if not self.has_config(CONF_HVAC_FAN_MODE_DP): + return None + return list(self._conf_hvac_fan_mode_set) + + @property + def swing_mode(self): + """Return the swing setting.""" + return self._swing_mode + + @property + def swing_modes(self): + """Return the list of available swing modes.""" + if not self.has_config(CONF_HVAC_SWING_MODE_DP): + return None + return list(self._conf_hvac_swing_mode_set) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs and self.has_config(CONF_TARGET_TEMPERATURE_DP): + temperature = round(kwargs[ATTR_TEMPERATURE] / self._target_precision) + await self._device.set_dp( + temperature, self._config[CONF_TARGET_TEMPERATURE_DP] + ) + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + if self._conf_hvac_fan_mode_dp is None: + _LOGGER.error("Fan speed unsupported (no DP)") + return + if fan_mode not in self._conf_hvac_fan_mode_set: + _LOGGER.error("Unsupported fan_mode: %s" % fan_mode) + return + await self._device.set_dp( + self._conf_hvac_fan_mode_set[fan_mode], self._conf_hvac_fan_mode_dp + ) + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target operation mode.""" + if hvac_mode == HVACMode.OFF: + await self._device.set_dp(False, self._dp_id) + return + if not self._state and self._conf_hvac_mode_dp != self._dp_id: + await self._device.set_dp(True, self._dp_id) + # Some thermostats need a small wait before sending another update + await asyncio.sleep(MODE_WAIT) + await self._device.set_dp( + self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp + ) + + async def async_set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + if self._conf_hvac_swing_mode_dp is None: + _LOGGER.error("Swing mode unsupported (no DP)") + return + if swing_mode not in self._conf_hvac_swing_mode_set: + _LOGGER.error("Unsupported swing_mode: %s" % swing_mode) + return + await self._device.set_dp( + self._conf_hvac_swing_mode_set[swing_mode], self._conf_hvac_swing_mode_dp + ) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self._device.set_dp(True, self._dp_id) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self._device.set_dp(False, self._dp_id) + + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + if preset_mode == PRESET_ECO: + await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp) + return + await self._device.set_dp( + self._conf_preset_set[preset_mode], self._conf_preset_dp + ) + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self.has_config(CONF_MIN_TEMP_DP): + return self.dps_conf(CONF_MIN_TEMP_DP) + return self._config[CONF_TEMP_MIN] + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.has_config(CONF_MAX_TEMP_DP): + return self.dps_conf(CONF_MAX_TEMP_DP) + return self._config[CONF_TEMP_MAX] + + def status_updated(self): + """Device status was updated.""" + self._state = self.dps(self._dp_id) + + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + self._target_temperature = ( + self.dps_conf(CONF_TARGET_TEMPERATURE_DP) * self._target_precision + ) + + if self.has_config(CONF_CURRENT_TEMPERATURE_DP): + self._current_temperature = ( + self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision + ) + + if self._has_presets: + if ( + self.has_config(CONF_ECO_DP) + and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value + ): + self._preset_mode = PRESET_ECO + else: + for preset, value in self._conf_preset_set.items(): # todo remove + if self.dps_conf(CONF_PRESET_DP) == value: + self._preset_mode = preset + break + else: + self._preset_mode = PRESET_NONE + + # Update the HVAC status + if self.has_config(CONF_HVAC_MODE_DP): + if not self._state: + self._hvac_mode = HVACMode.OFF + else: + for mode, value in self._conf_hvac_mode_set.items(): + if self.dps_conf(CONF_HVAC_MODE_DP) == value: + self._hvac_mode = mode + break + else: + # in case hvac mode and preset share the same dp + self._hvac_mode = HVACMode.AUTO + + # Update the fan status + if self.has_config(CONF_HVAC_FAN_MODE_DP): + for mode, value in self._conf_hvac_fan_mode_set.items(): + if self.dps_conf(CONF_HVAC_FAN_MODE_DP) == value: + self._fan_mode = mode + break + else: + # in case fan mode and preset share the same dp + _LOGGER.debug("Unknown fan mode %s" % self.dps_conf(CONF_HVAC_FAN_MODE_DP)) + self._fan_mode = FAN_AUTO + + # Update the swing status + if self.has_config(CONF_HVAC_SWING_MODE_DP): + for mode, value in self._conf_hvac_swing_mode_set.items(): + if self.dps_conf(CONF_HVAC_SWING_MODE_DP) == value: + self._swing_mode = mode + break + else: + _LOGGER.debug("Unknown swing mode %s" % self.dps_conf(CONF_HVAC_SWING_MODE_DP)) + self._swing_mode = SWING_OFF + + # Update the current action + for action, value in self._conf_hvac_action_set.items(): + if self.dps_conf(CONF_HVAC_ACTION_DP) == value: + self._hvac_action = action + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/cloud_api.py b/homeassistant/config/custom_components/localtuya/cloud_api.py new file mode 100644 index 0000000..a0c5128 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/cloud_api.py @@ -0,0 +1,139 @@ +"""Class to perform requests to Tuya Cloud APIs.""" +import functools +import hashlib +import hmac +import json +import logging +import time + +import requests + +_LOGGER = logging.getLogger(__name__) + + +# Signature algorithm. +def calc_sign(msg, key): + """Calculate signature for request.""" + sign = ( + hmac.new( + msg=bytes(msg, "latin-1"), + key=bytes(key, "latin-1"), + digestmod=hashlib.sha256, + ) + .hexdigest() + .upper() + ) + return sign + + +class TuyaCloudApi: + """Class to send API calls.""" + + def __init__(self, hass, region_code, client_id, secret, user_id): + """Initialize the class.""" + self._hass = hass + self._base_url = f"https://openapi.tuya{region_code}.com" + self._client_id = client_id + self._secret = secret + self._user_id = user_id + self._access_token = "" + self.device_list = {} + + def generate_payload(self, method, timestamp, url, headers, body=None): + """Generate signed payload for requests.""" + payload = self._client_id + self._access_token + timestamp + + payload += method + "\n" + # Content-SHA256 + payload += hashlib.sha256(bytes((body or "").encode("utf-8"))).hexdigest() + payload += ( + "\n" + + "".join( + [ + "%s:%s\n" % (key, headers[key]) # Headers + for key in headers.get("Signature-Headers", "").split(":") + if key in headers + ] + ) + + "\n/" + + url.split("//", 1)[-1].split("/", 1)[-1] # Url + ) + # _LOGGER.debug("PAYLOAD: %s", payload) + return payload + + async def async_make_request(self, method, url, body=None, headers={}): + """Perform requests.""" + timestamp = str(int(time.time() * 1000)) + payload = self.generate_payload(method, timestamp, url, headers, body) + default_par = { + "client_id": self._client_id, + "access_token": self._access_token, + "sign": calc_sign(payload, self._secret), + "t": timestamp, + "sign_method": "HMAC-SHA256", + } + full_url = self._base_url + url + # _LOGGER.debug("\n" + method + ": [%s]", full_url) + + if method == "GET": + func = functools.partial( + requests.get, full_url, headers=dict(default_par, **headers) + ) + elif method == "POST": + func = functools.partial( + requests.post, + full_url, + headers=dict(default_par, **headers), + data=json.dumps(body), + ) + # _LOGGER.debug("BODY: [%s]", body) + elif method == "PUT": + func = functools.partial( + requests.put, + full_url, + headers=dict(default_par, **headers), + data=json.dumps(body), + ) + + resp = await self._hass.async_add_executor_job(func) + # r = json.dumps(r.json(), indent=2, ensure_ascii=False) # Beautify the format + return resp + + async def async_get_access_token(self): + """Obtain a valid access token.""" + try: + resp = await self.async_make_request("GET", "/v1.0/token?grant_type=1") + except requests.exceptions.ConnectionError: + return "Request failed, status ConnectionError" + + if not resp.ok: + return "Request failed, status " + str(resp.status) + + r_json = resp.json() + if not r_json["success"]: + return f"Error {r_json['code']}: {r_json['msg']}" + + self._access_token = resp.json()["result"]["access_token"] + return "ok" + + async def async_get_devices_list(self): + """Obtain the list of devices associated to a user.""" + resp = await self.async_make_request( + "GET", url=f"/v1.0/users/{self._user_id}/devices" + ) + + if not resp.ok: + return "Request failed, status " + str(resp.status) + + r_json = resp.json() + if not r_json["success"]: + # _LOGGER.debug( + # "Request failed, reply is %s", + # json.dumps(r_json, indent=2, ensure_ascii=False) + # ) + return f"Error {r_json['code']}: {r_json['msg']}" + + self.device_list = {dev["id"]: dev for dev in r_json["result"]} + # _LOGGER.debug("DEV_LIST: %s", self.device_list) + + return "ok" diff --git a/homeassistant/config/custom_components/localtuya/common.py b/homeassistant/config/custom_components/localtuya/common.py new file mode 100644 index 0000000..fdb3503 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/common.py @@ -0,0 +1,607 @@ +"""Code shared between all platforms.""" +import asyncio +import json.decoder +import logging +import time +from datetime import timedelta + +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DEVICES, + CONF_ENTITIES, + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_ID, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.restore_state import RestoreEntity + +from . import pytuya +from .const import ( + ATTR_STATE, + ATTR_UPDATED_AT, + CONF_DEFAULT_VALUE, + CONF_ENABLE_DEBUG, + CONF_LOCAL_KEY, + CONF_MODEL, + CONF_PASSIVE_ENTITY, + CONF_PROTOCOL_VERSION, + CONF_RESET_DPIDS, + CONF_RESTORE_ON_RECONNECT, + DATA_CLOUD, + DOMAIN, + TUYA_DEVICES, +) + +_LOGGER = logging.getLogger(__name__) + + +def prepare_setup_entities(hass, config_entry, platform): + """Prepare ro setup entities for a platform.""" + entities_to_setup = [ + entity + for entity in config_entry.data[CONF_ENTITIES] + if entity[CONF_PLATFORM] == platform + ] + if not entities_to_setup: + return None, None + + tuyainterface = [] + + return tuyainterface, entities_to_setup + + +async def async_setup_entry( + domain, entity_class, flow_schema, hass, config_entry, async_add_entities +): + """Set up a Tuya platform based on a config entry. + + This is a generic method and each platform should lock domain and + entity_class with functools.partial. + """ + entities = [] + + for dev_id in config_entry.data[CONF_DEVICES]: + # entities_to_setup = prepare_setup_entities( + # hass, config_entry.data[dev_id], domain + # ) + dev_entry = config_entry.data[CONF_DEVICES][dev_id] + entities_to_setup = [ + entity + for entity in dev_entry[CONF_ENTITIES] + if entity[CONF_PLATFORM] == domain + ] + + if entities_to_setup: + + tuyainterface = hass.data[DOMAIN][TUYA_DEVICES][dev_id] + + dps_config_fields = list(get_dps_for_platform(flow_schema)) + + for entity_config in entities_to_setup: + # Add DPS used by this platform to the request list + for dp_conf in dps_config_fields: + if dp_conf in entity_config: + tuyainterface.dps_to_request[entity_config[dp_conf]] = None + + entities.append( + entity_class( + tuyainterface, + dev_entry, + entity_config[CONF_ID], + ) + ) + # Once the entities have been created, add to the TuyaDevice instance + tuyainterface.add_entities(entities) + async_add_entities(entities) + + +def get_dps_for_platform(flow_schema): + """Return config keys for all platform keys that depends on a datapoint.""" + for key, value in flow_schema(None).items(): + if hasattr(value, "container") and value.container is None: + yield key.schema + + +def get_entity_config(config_entry, dp_id): + """Return entity config for a given DPS id.""" + for entity in config_entry[CONF_ENTITIES]: + if entity[CONF_ID] == dp_id: + return entity + raise Exception(f"missing entity config for id {dp_id}") + + +@callback +def async_config_entry_by_device_id(hass, device_id): + """Look up config entry by device id.""" + current_entries = hass.config_entries.async_entries(DOMAIN) + for entry in current_entries: + if device_id in entry.data.get(CONF_DEVICES, []): + return entry + else: + _LOGGER.debug(f"Missing device configuration for device_id {device_id}") + return None + + +class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): + """Cache wrapper for pytuya.TuyaInterface.""" + + def __init__(self, hass, config_entry, dev_id): + """Initialize the cache.""" + super().__init__() + self._hass = hass + self._config_entry = config_entry + self._dev_config_entry = config_entry.data[CONF_DEVICES][dev_id].copy() + self._interface = None + self._status = {} + self.dps_to_request = {} + self._is_closing = False + self._connect_task = None + self._disconnect_task = None + self._unsub_interval = None + self._entities = [] + self._local_key = self._dev_config_entry[CONF_LOCAL_KEY] + self._default_reset_dpids = None + if CONF_RESET_DPIDS in self._dev_config_entry: + reset_ids_str = self._dev_config_entry[CONF_RESET_DPIDS].split(",") + + self._default_reset_dpids = [] + for reset_id in reset_ids_str: + self._default_reset_dpids.append(int(reset_id.strip())) + + self.set_logger(_LOGGER, self._dev_config_entry[CONF_DEVICE_ID]) + + # This has to be done in case the device type is type_0d + for entity in self._dev_config_entry[CONF_ENTITIES]: + self.dps_to_request[entity[CONF_ID]] = None + + def add_entities(self, entities): + """Set the entities associated with this device.""" + self._entities.extend(entities) + + @property + def is_connecting(self): + """Return whether device is currently connecting.""" + return self._connect_task is not None + + @property + def connected(self): + """Return if connected to device.""" + return self._interface is not None + + def async_connect(self): + """Connect to device if not already connected.""" + # self.info("async_connect: %d %r %r", self._is_closing, self._connect_task, self._interface) + if not self._is_closing and self._connect_task is None and not self._interface: + self._connect_task = asyncio.create_task(self._make_connection()) + + async def _make_connection(self): + """Subscribe localtuya entity events.""" + self.info("Trying to connect to %s...", self._dev_config_entry[CONF_HOST]) + + try: + self._interface = await pytuya.connect( + self._dev_config_entry[CONF_HOST], + self._dev_config_entry[CONF_DEVICE_ID], + self._local_key, + float(self._dev_config_entry[CONF_PROTOCOL_VERSION]), + self._dev_config_entry.get(CONF_ENABLE_DEBUG, False), + self, + ) + self._interface.add_dps_to_request(self.dps_to_request) + except Exception as ex: # pylint: disable=broad-except + self.warning( + f"Failed to connect to {self._dev_config_entry[CONF_HOST]}: %s", ex + ) + if self._interface is not None: + await self._interface.close() + self._interface = None + + if self._interface is not None: + try: + try: + self.debug("Retrieving initial state") + status = await self._interface.status() + if status is None: + raise Exception("Failed to retrieve status") + + self._interface.start_heartbeat() + self.status_updated(status) + + except Exception as ex: + if (self._default_reset_dpids is not None) and ( + len(self._default_reset_dpids) > 0 + ): + self.debug( + "Initial state update failed, trying reset command " + + "for DP IDs: %s", + self._default_reset_dpids, + ) + await self._interface.reset(self._default_reset_dpids) + + self.debug("Update completed, retrying initial state") + status = await self._interface.status() + if status is None or not status: + raise Exception("Failed to retrieve status") from ex + + self._interface.start_heartbeat() + self.status_updated(status) + else: + self.error("Initial state update failed, giving up: %r", ex) + if self._interface is not None: + await self._interface.close() + self._interface = None + + except (UnicodeDecodeError, json.decoder.JSONDecodeError) as ex: + self.warning("Initial state update failed (%s), trying key update", ex) + await self.update_local_key() + + if self._interface is not None: + await self._interface.close() + self._interface = None + + if self._interface is not None: + # Attempt to restore status for all entities that need to first set + # the DPS value before the device will respond with status. + for entity in self._entities: + await entity.restore_state_when_connected() + + def _new_entity_handler(entity_id): + self.debug( + "New entity %s was added to %s", + entity_id, + self._dev_config_entry[CONF_HOST], + ) + self._dispatch_status() + + signal = f"localtuya_entity_{self._dev_config_entry[CONF_DEVICE_ID]}" + self._disconnect_task = async_dispatcher_connect( + self._hass, signal, _new_entity_handler + ) + + if ( + CONF_SCAN_INTERVAL in self._dev_config_entry + and int(self._dev_config_entry[CONF_SCAN_INTERVAL]) > 0 + ): + self._unsub_interval = async_track_time_interval( + self._hass, + self._async_refresh, + timedelta(seconds=int(self._dev_config_entry[CONF_SCAN_INTERVAL])), + ) + + self.info(f"Successfully connected to {self._dev_config_entry[CONF_HOST]}") + + self._connect_task = None + + async def update_local_key(self): + """Retrieve updated local_key from Cloud API and update the config_entry.""" + dev_id = self._dev_config_entry[CONF_DEVICE_ID] + await self._hass.data[DOMAIN][DATA_CLOUD].async_get_devices_list() + cloud_devs = self._hass.data[DOMAIN][DATA_CLOUD].device_list + if dev_id in cloud_devs: + self._local_key = cloud_devs[dev_id].get(CONF_LOCAL_KEY) + new_data = self._config_entry.data.copy() + new_data[CONF_DEVICES][dev_id][CONF_LOCAL_KEY] = self._local_key + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + self._hass.config_entries.async_update_entry( + self._config_entry, + data=new_data, + ) + self.info("local_key updated for device %s.", dev_id) + + async def _async_refresh(self, _now): + if self._interface is not None: + await self._interface.update_dps() + + async def close(self): + """Close connection and stop re-connect loop.""" + self._is_closing = True + if self._connect_task is not None: + self._connect_task.cancel() + await self._connect_task + if self._interface is not None: + await self._interface.close() + if self._disconnect_task is not None: + self._disconnect_task() + self.info( + "Closed connection with device %s.", + self._dev_config_entry[CONF_FRIENDLY_NAME], + ) + + async def set_dp(self, state, dp_index): + """Change value of a DP of the Tuya device.""" + if self._interface is not None: + try: + await self._interface.set_dp(state, dp_index) + except Exception: # pylint: disable=broad-except + self.exception("Failed to set DP %d to %s", dp_index, str(state)) + else: + self.error( + "Not connected to device %s", self._dev_config_entry[CONF_FRIENDLY_NAME] + ) + + async def set_dps(self, states): + """Change value of a DPs of the Tuya device.""" + if self._interface is not None: + try: + await self._interface.set_dps(states) + except Exception: # pylint: disable=broad-except + self.exception("Failed to set DPs %r", states) + else: + self.error( + "Not connected to device %s", self._dev_config_entry[CONF_FRIENDLY_NAME] + ) + + @callback + def status_updated(self, status): + """Device updated status.""" + self._status.update(status) + self._dispatch_status() + + def _dispatch_status(self): + signal = f"localtuya_{self._dev_config_entry[CONF_DEVICE_ID]}" + async_dispatcher_send(self._hass, signal, self._status) + + @callback + def disconnected(self): + """Device disconnected.""" + signal = f"localtuya_{self._dev_config_entry[CONF_DEVICE_ID]}" + async_dispatcher_send(self._hass, signal, None) + if self._unsub_interval is not None: + self._unsub_interval() + self._unsub_interval = None + self._interface = None + + if self._connect_task is not None: + self._connect_task.cancel() + self._connect_task = None + self.warning("Disconnected - waiting for discovery broadcast") + + +class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): + """Representation of a Tuya entity.""" + + def __init__(self, device, config_entry, dp_id, logger, **kwargs): + """Initialize the Tuya entity.""" + super().__init__() + self._device = device + self._dev_config_entry = config_entry + self._config = get_entity_config(config_entry, dp_id) + self._dp_id = dp_id + self._status = {} + self._state = None + self._last_state = None + + # Default value is available to be provided by Platform entities if required + self._default_value = self._config.get(CONF_DEFAULT_VALUE) + + # Determine whether is a passive entity + self._is_passive_entity = self._config.get(CONF_PASSIVE_ENTITY) or False + + """ Restore on connect setting is available to be provided by Platform entities + if required""" + self._restore_on_reconnect = ( + self._config.get(CONF_RESTORE_ON_RECONNECT) or False + ) + self.set_logger(logger, self._dev_config_entry[CONF_DEVICE_ID]) + + async def async_added_to_hass(self): + """Subscribe localtuya events.""" + await super().async_added_to_hass() + + self.debug("Adding %s with configuration: %s", self.entity_id, self._config) + + state = await self.async_get_last_state() + if state: + self.status_restored(state) + + def _update_handler(status): + """Update entity state when status was updated.""" + if status is None: + status = {} + if self._status != status: + self._status = status.copy() + if status: + self.status_updated() + + # Update HA + self.schedule_update_ha_state() + + signal = f"localtuya_{self._dev_config_entry[CONF_DEVICE_ID]}" + + self.async_on_remove( + async_dispatcher_connect(self.hass, signal, _update_handler) + ) + + signal = f"localtuya_entity_{self._dev_config_entry[CONF_DEVICE_ID]}" + async_dispatcher_send(self.hass, signal, self.entity_id) + + @property + def extra_state_attributes(self): + """Return entity specific state attributes to be saved. + + These attributes are then available for restore when the + entity is restored at startup. + """ + attributes = {} + if self._state is not None: + attributes[ATTR_STATE] = self._state + elif self._last_state is not None: + attributes[ATTR_STATE] = self._last_state + + self.debug("Entity %s - Additional attributes: %s", self.name, attributes) + return attributes + + @property + def device_info(self): + """Return device information for the device registry.""" + model = self._dev_config_entry.get(CONF_MODEL, "Tuya generic") + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, f"local_{self._dev_config_entry[CONF_DEVICE_ID]}") + }, + "name": self._dev_config_entry[CONF_FRIENDLY_NAME], + "manufacturer": "Tuya", + "model": f"{model} ({self._dev_config_entry[CONF_DEVICE_ID]})", + "sw_version": self._dev_config_entry[CONF_PROTOCOL_VERSION], + } + + @property + def name(self): + """Get name of Tuya entity.""" + return self._config[CONF_FRIENDLY_NAME] + + @property + def should_poll(self): + """Return if platform should poll for updates.""" + return False + + @property + def unique_id(self): + """Return unique device identifier.""" + return f"local_{self._dev_config_entry[CONF_DEVICE_ID]}_{self._dp_id}" + + def has_config(self, attr): + """Return if a config parameter has a valid value.""" + value = self._config.get(attr, "-1") + return value is not None and value != "-1" + + @property + def available(self): + """Return if device is available or not.""" + return str(self._dp_id) in self._status + + def dps(self, dp_index): + """Return cached value for DPS index.""" + value = self._status.get(str(dp_index)) + if value is None: + self.warning( + "Entity %s is requesting unknown DPS index %s", + self.entity_id, + dp_index, + ) + + return value + + def dps_conf(self, conf_item): + """Return value of datapoint for user specified config item. + + This method looks up which DP a certain config item uses based on + user configuration and returns its value. + """ + dp_index = self._config.get(conf_item) + if dp_index is None: + self.warning( + "Entity %s is requesting unset index for option %s", + self.entity_id, + conf_item, + ) + return self.dps(dp_index) + + def status_updated(self): + """Device status was updated. + + Override in subclasses and update entity specific state. + """ + state = self.dps(self._dp_id) + self._state = state + + # Keep record in last_state as long as not during connection/re-connection, + # as last state will be used to restore the previous state + if (state is not None) and (not self._device.is_connecting): + self._last_state = state + + def status_restored(self, stored_state): + """Device status was restored. + + Override in subclasses and update entity specific state. + """ + raw_state = stored_state.attributes.get(ATTR_STATE) + if raw_state is not None: + self._last_state = raw_state + self.debug( + "Restoring state for entity: %s - state: %s", + self.name, + str(self._last_state), + ) + + def default_value(self): + """Return default value of this entity. + + Override in subclasses to specify the default value for the entity. + """ + # Check if default value has been set - if not, default to the entity defaults. + if self._default_value is None: + self._default_value = self.entity_default_value() + + return self._default_value + + def entity_default_value(self): # pylint: disable=no-self-use + """Return default value of the entity type. + + Override in subclasses to specify the default value for the entity. + """ + return 0 + + @property + def restore_on_reconnect(self): + """Return whether the last state should be restored on a reconnect. + + Useful where the device loses settings if powered off + """ + return self._restore_on_reconnect + + async def restore_state_when_connected(self): + """Restore if restore_on_reconnect is set, or if no status has been yet found. + + Which indicates a DPS that needs to be set before it starts returning + status. + """ + if (not self.restore_on_reconnect) and ( + (str(self._dp_id) in self._status) or (not self._is_passive_entity) + ): + self.debug( + "Entity %s (DP %d) - Not restoring as restore on reconnect is " + + "disabled for this entity and the entity has an initial status " + + "or it is not a passive entity", + self.name, + self._dp_id, + ) + return + + self.debug("Attempting to restore state for entity: %s", self.name) + # Attempt to restore the current state - in case reset. + restore_state = self._state + + # If no state stored in the entity currently, go from last saved state + if (restore_state == STATE_UNKNOWN) | (restore_state is None): + self.debug("No current state for entity") + restore_state = self._last_state + + # If no current or saved state, then use the default value + if restore_state is None: + if self._is_passive_entity: + self.debug("No last restored state - using default") + restore_state = self.default_value() + else: + self.debug("Not a passive entity and no state found - aborting restore") + return + + self.debug( + "Entity %s (DP %d) - Restoring state: %s", + self.name, + self._dp_id, + str(restore_state), + ) + + # Manually initialise + await self._device.set_dp(restore_state, self._dp_id) diff --git a/homeassistant/config/custom_components/localtuya/config_flow.py b/homeassistant/config/custom_components/localtuya/config_flow.py new file mode 100644 index 0000000..2573bc4 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/config_flow.py @@ -0,0 +1,819 @@ +"""Config flow for LocalTuya integration integration.""" +import errno +import logging +import time +from importlib import import_module + +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.entity_registry as er +import voluptuous as vol +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DEVICE_ID, + CONF_DEVICES, + CONF_ENTITIES, + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PLATFORM, + CONF_REGION, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from .cloud_api import TuyaCloudApi +from .common import pytuya +from .const import ( + ATTR_UPDATED_AT, + CONF_ACTION, + CONF_ADD_DEVICE, + CONF_DPS_STRINGS, + CONF_EDIT_DEVICE, + CONF_ENABLE_DEBUG, + CONF_LOCAL_KEY, + CONF_MANUAL_DPS, + CONF_MODEL, + CONF_NO_CLOUD, + CONF_PRODUCT_NAME, + CONF_PROTOCOL_VERSION, + CONF_RESET_DPIDS, + CONF_SETUP_CLOUD, + CONF_USER_ID, + CONF_ENABLE_ADD_ENTITIES, + DATA_CLOUD, + DATA_DISCOVERY, + DOMAIN, + PLATFORMS, +) +from .discovery import discover + +_LOGGER = logging.getLogger(__name__) + +ENTRIES_VERSION = 2 + +PLATFORM_TO_ADD = "platform_to_add" +NO_ADDITIONAL_ENTITIES = "no_additional_entities" +SELECTED_DEVICE = "selected_device" + +CUSTOM_DEVICE = "..." + +CONF_ACTIONS = { + CONF_ADD_DEVICE: "Add a new device", + CONF_EDIT_DEVICE: "Edit a device", + CONF_SETUP_CLOUD: "Reconfigure Cloud API account", +} + +CONFIGURE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ACTION, default=CONF_ADD_DEVICE): vol.In(CONF_ACTIONS), + } +) + +CLOUD_SETUP_SCHEMA = vol.Schema( + { + vol.Required(CONF_REGION, default="eu"): vol.In(["eu", "us", "cn", "in"]), + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_USER_ID): cv.string, + vol.Optional(CONF_USERNAME, default=DOMAIN): cv.string, + vol.Required(CONF_NO_CLOUD, default=False): bool, + } +) + + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In( + ["3.1", "3.2", "3.3", "3.4"] + ), + vol.Required(CONF_ENABLE_DEBUG, default=False): bool, + vol.Optional(CONF_SCAN_INTERVAL): int, + vol.Optional(CONF_MANUAL_DPS): cv.string, + vol.Optional(CONF_RESET_DPIDS): str, + } +) + +PICK_ENTITY_SCHEMA = vol.Schema( + {vol.Required(PLATFORM_TO_ADD, default="switch"): vol.In(PLATFORMS)} +) + + +def devices_schema(discovered_devices, cloud_devices_list, add_custom_device=True): + """Create schema for devices step.""" + devices = {} + for dev_id, dev_host in discovered_devices.items(): + dev_name = dev_id + if dev_id in cloud_devices_list.keys(): + dev_name = cloud_devices_list[dev_id][CONF_NAME] + devices[dev_id] = f"{dev_name} ({dev_host})" + + if add_custom_device: + devices.update({CUSTOM_DEVICE: CUSTOM_DEVICE}) + + # devices.update( + # { + # ent.data[CONF_DEVICE_ID]: ent.data[CONF_FRIENDLY_NAME] + # for ent in entries + # } + # ) + return vol.Schema({vol.Required(SELECTED_DEVICE): vol.In(devices)}) + + +def options_schema(entities): + """Create schema for options.""" + entity_names = [ + f"{entity[CONF_ID]}: {entity[CONF_FRIENDLY_NAME]}" for entity in entities + ] + return vol.Schema( + { + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In( + ["3.1", "3.2", "3.3", "3.4"] + ), + vol.Required(CONF_ENABLE_DEBUG, default=False): bool, + vol.Optional(CONF_SCAN_INTERVAL): int, + vol.Optional(CONF_MANUAL_DPS): cv.string, + vol.Optional(CONF_RESET_DPIDS): cv.string, + vol.Required( + CONF_ENTITIES, description={"suggested_value": entity_names} + ): cv.multi_select(entity_names), + vol.Required(CONF_ENABLE_ADD_ENTITIES, default=False): bool, + } + ) + + +def schema_defaults(schema, dps_list=None, **defaults): + """Create a new schema with default values filled in.""" + copy = schema.extend({}) + for field, field_type in copy.schema.items(): + if isinstance(field_type, vol.In): + value = None + for dps in dps_list or []: + if dps.startswith(f"{defaults.get(field)} "): + value = dps + break + + if value in field_type.container: + field.default = vol.default_factory(value) + continue + + if field.schema in defaults: + field.default = vol.default_factory(defaults[field]) + return copy + + +def dps_string_list(dps_data): + """Return list of friendly DPS values.""" + return [f"{id} (value: {value})" for id, value in dps_data.items()] + + +def gen_dps_strings(): + """Generate list of DPS values.""" + return [f"{dp} (value: ?)" for dp in range(1, 256)] + + +def platform_schema(platform, dps_strings, allow_id=True, yaml=False): + """Generate input validation schema for a platform.""" + schema = {} + if yaml: + # In YAML mode we force the specified platform to match flow schema + schema[vol.Required(CONF_PLATFORM)] = vol.In([platform]) + if allow_id: + schema[vol.Required(CONF_ID)] = vol.In(dps_strings) + schema[vol.Required(CONF_FRIENDLY_NAME)] = str + return vol.Schema(schema).extend(flow_schema(platform, dps_strings)) + + +def flow_schema(platform, dps_strings): + """Return flow schema for a specific platform.""" + integration_module = ".".join(__name__.split(".")[:-1]) + return import_module("." + platform, integration_module).flow_schema(dps_strings) + + +def strip_dps_values(user_input, dps_strings): + """Remove values and keep only index for DPS config items.""" + stripped = {} + for field, value in user_input.items(): + if value in dps_strings: + stripped[field] = int(user_input[field].split(" ")[0]) + else: + stripped[field] = user_input[field] + return stripped + + +def config_schema(): + """Build schema used for setting up component.""" + entity_schemas = [ + platform_schema(platform, range(1, 256), yaml=True) for platform in PLATFORMS + ] + return vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + DEVICE_SCHEMA.extend( + {vol.Required(CONF_ENTITIES): [vol.Any(*entity_schemas)]} + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, + ) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + detected_dps = {} + + interface = None + + reset_ids = None + try: + interface = await pytuya.connect( + data[CONF_HOST], + data[CONF_DEVICE_ID], + data[CONF_LOCAL_KEY], + float(data[CONF_PROTOCOL_VERSION]), + data[CONF_ENABLE_DEBUG], + ) + if CONF_RESET_DPIDS in data: + reset_ids_str = data[CONF_RESET_DPIDS].split(",") + reset_ids = [] + for reset_id in reset_ids_str: + reset_ids.append(int(reset_id.strip())) + _LOGGER.debug( + "Reset DPIDs configured: %s (%s)", + data[CONF_RESET_DPIDS], + reset_ids, + ) + try: + detected_dps = await interface.detect_available_dps() + except Exception as ex: + try: + _LOGGER.debug( + "Initial state update failed (%s), trying reset command", ex + ) + if len(reset_ids) > 0: + await interface.reset(reset_ids) + detected_dps = await interface.detect_available_dps() + except Exception as ex: + _LOGGER.debug("No DPS able to be detected: %s", ex) + detected_dps = {} + + # if manual DPs are set, merge these. + _LOGGER.debug("Detected DPS: %s", detected_dps) + if CONF_MANUAL_DPS in data: + manual_dps_list = [dps.strip() for dps in data[CONF_MANUAL_DPS].split(",")] + _LOGGER.debug( + "Manual DPS Setting: %s (%s)", data[CONF_MANUAL_DPS], manual_dps_list + ) + # merge the lists + for new_dps in manual_dps_list + (reset_ids or []): + # If the DPS not in the detected dps list, then add with a + # default value indicating that it has been manually added + if str(new_dps) not in detected_dps: + detected_dps[new_dps] = -1 + + except (ConnectionRefusedError, ConnectionResetError) as ex: + raise CannotConnect from ex + except ValueError as ex: + raise InvalidAuth from ex + finally: + if interface: + await interface.close() + + # Indicate an error if no datapoints found as the rest of the flow + # won't work in this case + if not detected_dps: + raise EmptyDpsList + + _LOGGER.debug("Total DPS: %s", detected_dps) + + return dps_string_list(detected_dps) + + +async def attempt_cloud_connection(hass, user_input): + """Create device.""" + cloud_api = TuyaCloudApi( + hass, + user_input.get(CONF_REGION), + user_input.get(CONF_CLIENT_ID), + user_input.get(CONF_CLIENT_SECRET), + user_input.get(CONF_USER_ID), + ) + + res = await cloud_api.async_get_access_token() + if res != "ok": + _LOGGER.error("Cloud API connection failed: %s", res) + return cloud_api, {"reason": "authentication_failed", "msg": res} + + res = await cloud_api.async_get_devices_list() + if res != "ok": + _LOGGER.error("Cloud API get_devices_list failed: %s", res) + return cloud_api, {"reason": "device_list_failed", "msg": res} + _LOGGER.info("Cloud API connection succeeded.") + + return cloud_api, {} + + +class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LocalTuya integration.""" + + VERSION = ENTRIES_VERSION + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow for this handler.""" + return LocalTuyaOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize a new LocaltuyaConfigFlow.""" + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + placeholders = {} + if user_input is not None: + if user_input.get(CONF_NO_CLOUD): + for i in [CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_USER_ID]: + user_input[i] = "" + return await self._create_entry(user_input) + + cloud_api, res = await attempt_cloud_connection(self.hass, user_input) + + if not res: + return await self._create_entry(user_input) + errors["base"] = res["reason"] + placeholders = {"msg": res["msg"]} + + defaults = {} + defaults.update(user_input or {}) + + return self.async_show_form( + step_id="user", + data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), + errors=errors, + description_placeholders=placeholders, + ) + + async def _create_entry(self, user_input): + """Register new entry.""" + # if self._async_current_entries(): + # return self.async_abort(reason="already_configured") + + await self.async_set_unique_id(user_input.get(CONF_USER_ID)) + user_input[CONF_DEVICES] = {} + + return self.async_create_entry( + title=user_input.get(CONF_USERNAME), + data=user_input, + ) + + async def async_step_import(self, user_input): + """Handle import from YAML.""" + _LOGGER.error( + "Configuration via YAML file is no longer supported by this integration." + ) + + +class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): + """Handle options flow for LocalTuya integration.""" + + def __init__(self, config_entry): + """Initialize localtuya options flow.""" + self._config_entry = config_entry + # self.dps_strings = config_entry.data.get(CONF_DPS_STRINGS, gen_dps_strings()) + # self.entities = config_entry.data[CONF_ENTITIES] + self.selected_device = None + self.editing_device = False + self.device_data = None + self.dps_strings = [] + self.selected_platform = None + self.discovered_devices = {} + self.entities = [] + + async def async_step_init(self, user_input=None): + """Manage basic options.""" + # device_id = self.config_entry.data[CONF_DEVICE_ID] + if user_input is not None: + if user_input.get(CONF_ACTION) == CONF_SETUP_CLOUD: + return await self.async_step_cloud_setup() + if user_input.get(CONF_ACTION) == CONF_ADD_DEVICE: + return await self.async_step_add_device() + if user_input.get(CONF_ACTION) == CONF_EDIT_DEVICE: + return await self.async_step_edit_device() + + return self.async_show_form( + step_id="init", + data_schema=CONFIGURE_SCHEMA, + ) + + async def async_step_cloud_setup(self, user_input=None): + """Handle the initial step.""" + errors = {} + placeholders = {} + if user_input is not None: + if user_input.get(CONF_NO_CLOUD): + new_data = self.config_entry.data.copy() + new_data.update(user_input) + for i in [CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_USER_ID]: + new_data[i] = "" + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + ) + return self.async_create_entry( + title=new_data.get(CONF_USERNAME), data={} + ) + + cloud_api, res = await attempt_cloud_connection(self.hass, user_input) + + if not res: + new_data = self.config_entry.data.copy() + new_data.update(user_input) + cloud_devs = cloud_api.device_list + for dev_id, dev in new_data[CONF_DEVICES].items(): + if CONF_MODEL not in dev and dev_id in cloud_devs: + model = cloud_devs[dev_id].get(CONF_PRODUCT_NAME) + new_data[CONF_DEVICES][dev_id][CONF_MODEL] = model + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + ) + return self.async_create_entry( + title=new_data.get(CONF_USERNAME), data={} + ) + errors["base"] = res["reason"] + placeholders = {"msg": res["msg"]} + + defaults = self.config_entry.data.copy() + defaults.update(user_input or {}) + defaults[CONF_NO_CLOUD] = False + + return self.async_show_form( + step_id="cloud_setup", + data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), + errors=errors, + description_placeholders=placeholders, + ) + + async def async_step_add_device(self, user_input=None): + """Handle adding a new device.""" + # Use cache if available or fallback to manual discovery + self.editing_device = False + self.selected_device = None + errors = {} + if user_input is not None: + if user_input[SELECTED_DEVICE] != CUSTOM_DEVICE: + self.selected_device = user_input[SELECTED_DEVICE] + + return await self.async_step_configure_device() + + self.discovered_devices = {} + data = self.hass.data.get(DOMAIN) + + if data and DATA_DISCOVERY in data: + self.discovered_devices = data[DATA_DISCOVERY].devices + else: + try: + self.discovered_devices = await discover() + except OSError as ex: + if ex.errno == errno.EADDRINUSE: + errors["base"] = "address_in_use" + else: + errors["base"] = "discovery_failed" + except Exception as ex: + _LOGGER.exception("discovery failed: %s", ex) + errors["base"] = "discovery_failed" + + devices = { + dev_id: dev["ip"] + for dev_id, dev in self.discovered_devices.items() + if dev["gwId"] not in self.config_entry.data[CONF_DEVICES] + } + + return self.async_show_form( + step_id="add_device", + data_schema=devices_schema( + devices, self.hass.data[DOMAIN][DATA_CLOUD].device_list + ), + errors=errors, + ) + + async def async_step_edit_device(self, user_input=None): + """Handle editing a device.""" + self.editing_device = True + # Use cache if available or fallback to manual discovery + errors = {} + if user_input is not None: + self.selected_device = user_input[SELECTED_DEVICE] + dev_conf = self.config_entry.data[CONF_DEVICES][self.selected_device] + self.dps_strings = dev_conf.get(CONF_DPS_STRINGS, gen_dps_strings()) + self.entities = dev_conf[CONF_ENTITIES] + + return await self.async_step_configure_device() + + devices = {} + for dev_id, configured_dev in self.config_entry.data[CONF_DEVICES].items(): + devices[dev_id] = configured_dev[CONF_HOST] + + return self.async_show_form( + step_id="edit_device", + data_schema=devices_schema( + devices, self.hass.data[DOMAIN][DATA_CLOUD].device_list, False + ), + errors=errors, + ) + + async def async_step_configure_device(self, user_input=None): + """Handle input of basic info.""" + errors = {} + dev_id = self.selected_device + if user_input is not None: + try: + self.device_data = user_input.copy() + if dev_id is not None: + # self.device_data[CONF_PRODUCT_KEY] = self.devices[ + # self.selected_device + # ]["productKey"] + cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD].device_list + if dev_id in cloud_devs: + self.device_data[CONF_MODEL] = cloud_devs[dev_id].get( + CONF_PRODUCT_NAME + ) + if self.editing_device: + if user_input[CONF_ENABLE_ADD_ENTITIES]: + self.editing_device = False + user_input[CONF_DEVICE_ID] = dev_id + self.device_data.update( + { + CONF_DEVICE_ID: dev_id, + CONF_DPS_STRINGS: self.dps_strings, + } + ) + return await self.async_step_pick_entity_type() + + self.device_data.update( + { + CONF_DEVICE_ID: dev_id, + CONF_DPS_STRINGS: self.dps_strings, + CONF_ENTITIES: [], + } + ) + if len(user_input[CONF_ENTITIES]) == 0: + return self.async_abort( + reason="no_entities", + description_placeholders={}, + ) + if user_input[CONF_ENTITIES]: + entity_ids = [ + int(entity.split(":")[0]) + for entity in user_input[CONF_ENTITIES] + ] + device_config = self.config_entry.data[CONF_DEVICES][dev_id] + self.entities = [ + entity + for entity in device_config[CONF_ENTITIES] + if entity[CONF_ID] in entity_ids + ] + return await self.async_step_configure_entity() + + self.dps_strings = await validate_input(self.hass, user_input) + return await self.async_step_pick_entity_type() + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except EmptyDpsList: + errors["base"] = "empty_dps" + except Exception as ex: + _LOGGER.exception("Unexpected exception: %s", ex) + errors["base"] = "unknown" + + defaults = {} + if self.editing_device: + # If selected device exists as a config entry, load config from it + defaults = self.config_entry.data[CONF_DEVICES][dev_id].copy() + cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD].device_list + placeholders = {"for_device": f" for device `{dev_id}`"} + if dev_id in cloud_devs: + cloud_local_key = cloud_devs[dev_id].get(CONF_LOCAL_KEY) + if defaults[CONF_LOCAL_KEY] != cloud_local_key: + _LOGGER.info( + "New local_key detected: new %s vs old %s", + cloud_local_key, + defaults[CONF_LOCAL_KEY], + ) + defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY) + note = "\nNOTE: a new local_key has been retrieved using cloud API" + placeholders = {"for_device": f" for device `{dev_id}`.{note}"} + defaults[CONF_ENABLE_ADD_ENTITIES] = False + schema = schema_defaults(options_schema(self.entities), **defaults) + else: + defaults[CONF_PROTOCOL_VERSION] = "3.3" + defaults[CONF_HOST] = "" + defaults[CONF_DEVICE_ID] = "" + defaults[CONF_LOCAL_KEY] = "" + defaults[CONF_FRIENDLY_NAME] = "" + if dev_id is not None: + # Insert default values from discovery and cloud if present + device = self.discovered_devices[dev_id] + defaults[CONF_HOST] = device.get("ip") + defaults[CONF_DEVICE_ID] = device.get("gwId") + defaults[CONF_PROTOCOL_VERSION] = device.get("version") + cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD].device_list + if dev_id in cloud_devs: + defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY) + defaults[CONF_FRIENDLY_NAME] = cloud_devs[dev_id].get(CONF_NAME) + schema = schema_defaults(DEVICE_SCHEMA, **defaults) + + placeholders = {"for_device": ""} + + return self.async_show_form( + step_id="configure_device", + data_schema=schema, + errors=errors, + description_placeholders=placeholders, + ) + + async def async_step_pick_entity_type(self, user_input=None): + """Handle asking if user wants to add another entity.""" + if user_input is not None: + if user_input.get(NO_ADDITIONAL_ENTITIES): + config = { + **self.device_data, + CONF_DPS_STRINGS: self.dps_strings, + CONF_ENTITIES: self.entities, + } + + dev_id = self.device_data.get(CONF_DEVICE_ID) + + new_data = self.config_entry.data.copy() + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + new_data[CONF_DEVICES].update({dev_id: config}) + + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + ) + return self.async_create_entry(title="", data={}) + + self.selected_platform = user_input[PLATFORM_TO_ADD] + return await self.async_step_configure_entity() + + # Add a checkbox that allows bailing out from config flow if at least one + # entity has been added + schema = PICK_ENTITY_SCHEMA + if self.selected_platform is not None: + schema = schema.extend( + {vol.Required(NO_ADDITIONAL_ENTITIES, default=True): bool} + ) + + return self.async_show_form(step_id="pick_entity_type", data_schema=schema) + + def available_dps_strings(self): + """Return list of DPs use by the device's entities.""" + available_dps = [] + used_dps = [str(entity[CONF_ID]) for entity in self.entities] + for dp_string in self.dps_strings: + dp = dp_string.split(" ")[0] + if dp not in used_dps: + available_dps.append(dp_string) + return available_dps + + async def async_step_entity(self, user_input=None): + """Manage entity settings.""" + errors = {} + if user_input is not None: + entity = strip_dps_values(user_input, self.dps_strings) + entity[CONF_ID] = self.current_entity[CONF_ID] + entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM] + self.device_data[CONF_ENTITIES].append(entity) + + if len(self.entities) == len(self.device_data[CONF_ENTITIES]): + self.hass.config_entries.async_update_entry( + self.config_entry, + title=self.device_data[CONF_FRIENDLY_NAME], + data=self.device_data, + ) + return self.async_create_entry(title="", data={}) + + schema = platform_schema( + self.current_entity[CONF_PLATFORM], self.dps_strings, allow_id=False + ) + return self.async_show_form( + step_id="entity", + errors=errors, + data_schema=schema_defaults( + schema, self.dps_strings, **self.current_entity + ), + description_placeholders={ + "id": self.current_entity[CONF_ID], + "platform": self.current_entity[CONF_PLATFORM], + }, + ) + + async def async_step_configure_entity(self, user_input=None): + """Manage entity settings.""" + errors = {} + if user_input is not None: + if self.editing_device: + entity = strip_dps_values(user_input, self.dps_strings) + entity[CONF_ID] = self.current_entity[CONF_ID] + entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM] + self.device_data[CONF_ENTITIES].append(entity) + + if len(self.entities) == len(self.device_data[CONF_ENTITIES]): + # finished editing device. Let's store the new config entry.... + dev_id = self.device_data[CONF_DEVICE_ID] + new_data = self.config_entry.data.copy() + entry_id = self.config_entry.entry_id + # removing entities from registry (they will be recreated) + ent_reg = er.async_get(self.hass) + reg_entities = { + ent.unique_id: ent.entity_id + for ent in er.async_entries_for_config_entry(ent_reg, entry_id) + if dev_id in ent.unique_id + } + for entity_id in reg_entities.values(): + ent_reg.async_remove(entity_id) + + new_data[CONF_DEVICES][dev_id] = self.device_data + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + ) + return self.async_create_entry(title="", data={}) + else: + user_input[CONF_PLATFORM] = self.selected_platform + self.entities.append(strip_dps_values(user_input, self.dps_strings)) + # new entity added. Let's check if there are more left... + user_input = None + if len(self.available_dps_strings()) == 0: + user_input = {NO_ADDITIONAL_ENTITIES: True} + return await self.async_step_pick_entity_type(user_input) + + if self.editing_device: + schema = platform_schema( + self.current_entity[CONF_PLATFORM], self.dps_strings, allow_id=False + ) + schema = schema_defaults(schema, self.dps_strings, **self.current_entity) + placeholders = { + "entity": f"entity with DP {self.current_entity[CONF_ID]}", + "platform": self.current_entity[CONF_PLATFORM], + } + else: + available_dps = self.available_dps_strings() + schema = platform_schema(self.selected_platform, available_dps) + placeholders = { + "entity": "an entity", + "platform": self.selected_platform, + } + + return self.async_show_form( + step_id="configure_entity", + data_schema=schema, + errors=errors, + description_placeholders=placeholders, + ) + + async def async_step_yaml_import(self, user_input=None): + """Manage YAML imports.""" + _LOGGER.error( + "Configuration via YAML file is no longer supported by this integration." + ) + # if user_input is not None: + # return self.async_create_entry(title="", data={}) + # return self.async_show_form(step_id="yaml_import") + + @property + def current_entity(self): + """Existing configuration for entity currently being edited.""" + return self.entities[len(self.device_data[CONF_ENTITIES])] + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class EmptyDpsList(exceptions.HomeAssistantError): + """Error to indicate no datapoints found.""" diff --git a/homeassistant/config/custom_components/localtuya/const.py b/homeassistant/config/custom_components/localtuya/const.py new file mode 100644 index 0000000..6e7556b --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/const.py @@ -0,0 +1,143 @@ +"""Constants for localtuya integration.""" + +DOMAIN = "localtuya" + +DATA_DISCOVERY = "discovery" +DATA_CLOUD = "cloud_data" + +# Platforms in this list must support config flows +PLATFORMS = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "number", + "select", + "sensor", + "switch", + "vacuum", +] + +TUYA_DEVICES = "tuya_devices" + +ATTR_CURRENT = "current" +ATTR_CURRENT_CONSUMPTION = "current_consumption" +ATTR_VOLTAGE = "voltage" +ATTR_UPDATED_AT = "updated_at" + +# config flow +CONF_LOCAL_KEY = "local_key" +CONF_ENABLE_DEBUG = "enable_debug" +CONF_PROTOCOL_VERSION = "protocol_version" +CONF_DPS_STRINGS = "dps_strings" +CONF_MODEL = "model" +CONF_PRODUCT_KEY = "product_key" +CONF_PRODUCT_NAME = "product_name" +CONF_USER_ID = "user_id" +CONF_ENABLE_ADD_ENTITIES = "add_entities" + + +CONF_ACTION = "action" +CONF_ADD_DEVICE = "add_device" +CONF_EDIT_DEVICE = "edit_device" +CONF_SETUP_CLOUD = "setup_cloud" +CONF_NO_CLOUD = "no_cloud" +CONF_MANUAL_DPS = "manual_dps_strings" +CONF_DEFAULT_VALUE = "dps_default_value" +CONF_RESET_DPIDS = "reset_dpids" +CONF_PASSIVE_ENTITY = "is_passive_entity" + +# light +CONF_BRIGHTNESS_LOWER = "brightness_lower" +CONF_BRIGHTNESS_UPPER = "brightness_upper" +CONF_COLOR = "color" +CONF_COLOR_MODE = "color_mode" +CONF_COLOR_MODE_SET = "color_mode_set" +CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin" +CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin" +CONF_COLOR_TEMP_REVERSE = "color_temp_reverse" +CONF_MUSIC_MODE = "music_mode" + +# switch +CONF_CURRENT = "current" +CONF_CURRENT_CONSUMPTION = "current_consumption" +CONF_VOLTAGE = "voltage" + +# cover +CONF_COMMANDS_SET = "commands_set" +CONF_POSITIONING_MODE = "positioning_mode" +CONF_CURRENT_POSITION_DP = "current_position_dp" +CONF_SET_POSITION_DP = "set_position_dp" +CONF_POSITION_INVERTED = "position_inverted" +CONF_SPAN_TIME = "span_time" + +# fan +CONF_FAN_SPEED_CONTROL = "fan_speed_control" +CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control" +CONF_FAN_SPEED_MIN = "fan_speed_min" +CONF_FAN_SPEED_MAX = "fan_speed_max" +CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" +CONF_FAN_DIRECTION = "fan_direction" +CONF_FAN_DIRECTION_FWD = "fan_direction_forward" +CONF_FAN_DIRECTION_REV = "fan_direction_reverse" +CONF_FAN_DPS_TYPE = "fan_dps_type" + +# sensor +CONF_SCALING = "scaling" + +# climate +CONF_TARGET_TEMPERATURE_DP = "target_temperature_dp" +CONF_CURRENT_TEMPERATURE_DP = "current_temperature_dp" +CONF_TEMPERATURE_STEP = "temperature_step" +CONF_MAX_TEMP_DP = "max_temperature_dp" +CONF_MIN_TEMP_DP = "min_temperature_dp" +CONF_TEMP_MAX = "max_temperature_const" +CONF_TEMP_MIN = "min_temperature_const" +CONF_PRECISION = "precision" +CONF_TARGET_PRECISION = "target_precision" +CONF_HVAC_MODE_DP = "hvac_mode_dp" +CONF_HVAC_MODE_SET = "hvac_mode_set" +CONF_HVAC_FAN_MODE_DP = "hvac_fan_mode_dp" +CONF_HVAC_FAN_MODE_SET = "hvac_fan_mode_set" +CONF_HVAC_SWING_MODE_DP = "hvac_swing_mode_dp" +CONF_HVAC_SWING_MODE_SET = "hvac_swing_mode_set" +CONF_PRESET_DP = "preset_dp" +CONF_PRESET_SET = "preset_set" +CONF_HEURISTIC_ACTION = "heuristic_action" +CONF_HVAC_ACTION_DP = "hvac_action_dp" +CONF_HVAC_ACTION_SET = "hvac_action_set" +CONF_ECO_DP = "eco_dp" +CONF_ECO_VALUE = "eco_value" + +# vacuum +CONF_POWERGO_DP = "powergo_dp" +CONF_IDLE_STATUS_VALUE = "idle_status_value" +CONF_RETURNING_STATUS_VALUE = "returning_status_value" +CONF_DOCKED_STATUS_VALUE = "docked_status_value" +CONF_BATTERY_DP = "battery_dp" +CONF_MODE_DP = "mode_dp" +CONF_MODES = "modes" +CONF_FAN_SPEED_DP = "fan_speed_dp" +CONF_FAN_SPEEDS = "fan_speeds" +CONF_CLEAN_TIME_DP = "clean_time_dp" +CONF_CLEAN_AREA_DP = "clean_area_dp" +CONF_CLEAN_RECORD_DP = "clean_record_dp" +CONF_LOCATE_DP = "locate_dp" +CONF_FAULT_DP = "fault_dp" +CONF_PAUSED_STATE = "paused_state" +CONF_RETURN_MODE = "return_mode" +CONF_STOP_STATUS = "stop_status" + +# number +CONF_MIN_VALUE = "min_value" +CONF_MAX_VALUE = "max_value" +CONF_STEPSIZE_VALUE = "step_size" + +# select +CONF_OPTIONS = "select_options" +CONF_OPTIONS_FRIENDLY = "select_options_friendly" + +# States +ATTR_STATE = "raw_state" +CONF_RESTORE_ON_RECONNECT = "restore_on_reconnect" diff --git a/homeassistant/config/custom_components/localtuya/cover.py b/homeassistant/config/custom_components/localtuya/cover.py new file mode 100644 index 0000000..700dc3f --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/cover.py @@ -0,0 +1,233 @@ +"""Platform to locally control Tuya-based cover devices.""" +import asyncio +import logging +import time +from functools import partial + +import voluptuous as vol +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN, + CoverEntity, CoverEntityFeature, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_COMMANDS_SET, + CONF_CURRENT_POSITION_DP, + CONF_POSITION_INVERTED, + CONF_POSITIONING_MODE, + CONF_SET_POSITION_DP, + CONF_SPAN_TIME, +) + +_LOGGER = logging.getLogger(__name__) + +COVER_ONOFF_CMDS = "on_off_stop" +COVER_OPENCLOSE_CMDS = "open_close_stop" +COVER_FZZZ_CMDS = "fz_zz_stop" +COVER_12_CMDS = "1_2_3" +COVER_MODE_NONE = "none" +COVER_MODE_POSITION = "position" +COVER_MODE_TIMED = "timed" +COVER_TIMEOUT_TOLERANCE = 3.0 + +DEFAULT_COMMANDS_SET = COVER_ONOFF_CMDS +DEFAULT_POSITIONING_MODE = COVER_MODE_NONE +DEFAULT_SPAN_TIME = 25.0 + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_COMMANDS_SET): vol.In( + [COVER_ONOFF_CMDS, COVER_OPENCLOSE_CMDS, COVER_FZZZ_CMDS, COVER_12_CMDS] + ), + vol.Optional(CONF_POSITIONING_MODE, default=DEFAULT_POSITIONING_MODE): vol.In( + [COVER_MODE_NONE, COVER_MODE_POSITION, COVER_MODE_TIMED] + ), + vol.Optional(CONF_CURRENT_POSITION_DP): vol.In(dps), + vol.Optional(CONF_SET_POSITION_DP): vol.In(dps), + vol.Optional(CONF_POSITION_INVERTED, default=False): bool, + vol.Optional(CONF_SPAN_TIME, default=DEFAULT_SPAN_TIME): vol.All( + vol.Coerce(float), vol.Range(min=1.0, max=300.0) + ), + } + + +class LocaltuyaCover(LocalTuyaEntity, CoverEntity): + """Tuya cover device.""" + + def __init__(self, device, config_entry, switchid, **kwargs): + """Initialize a new LocaltuyaCover.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + commands_set = DEFAULT_COMMANDS_SET + if self.has_config(CONF_COMMANDS_SET): + commands_set = self._config[CONF_COMMANDS_SET] + self._open_cmd = commands_set.split("_")[0] + self._close_cmd = commands_set.split("_")[1] + self._stop_cmd = commands_set.split("_")[2] + self._timer_start = time.time() + self._state = self._stop_cmd + self._previous_state = self._state + self._current_cover_position = 0 + _LOGGER.debug("Initialized cover [%s]", self.name) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + if self._config[CONF_POSITIONING_MODE] != COVER_MODE_NONE: + supported_features = supported_features | CoverEntityFeature.SET_POSITION + return supported_features + + @property + def current_cover_position(self): + """Return current cover position in percent.""" + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_NONE: + return None + return self._current_cover_position + + @property + def is_opening(self): + """Return if cover is opening.""" + state = self._state + return state == self._open_cmd + + @property + def is_closing(self): + """Return if cover is closing.""" + state = self._state + return state == self._close_cmd + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_NONE: + return False + + if self._current_cover_position == 0: + return True + if self._current_cover_position == 100: + return False + return False + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.debug("Setting cover position: %r", kwargs[ATTR_POSITION]) + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + newpos = float(kwargs[ATTR_POSITION]) + + currpos = self.current_cover_position + posdiff = abs(newpos - currpos) + mydelay = posdiff / 100.0 * self._config[CONF_SPAN_TIME] + if newpos > currpos: + self.debug("Opening to %f: delay %f", newpos, mydelay) + await self.async_open_cover() + else: + self.debug("Closing to %f: delay %f", newpos, mydelay) + await self.async_close_cover() + self.hass.async_create_task(self.async_stop_after_timeout(mydelay)) + self.debug("Done") + + elif self._config[CONF_POSITIONING_MODE] == COVER_MODE_POSITION: + converted_position = int(kwargs[ATTR_POSITION]) + if self._config[CONF_POSITION_INVERTED]: + converted_position = 100 - converted_position + + if 0 <= converted_position <= 100 and self.has_config(CONF_SET_POSITION_DP): + await self._device.set_dp( + converted_position, self._config[CONF_SET_POSITION_DP] + ) + + async def async_stop_after_timeout(self, delay_sec): + """Stop the cover if timeout (max movement span) occurred.""" + await asyncio.sleep(delay_sec) + await self.async_stop_cover() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self.debug("Launching command %s to cover ", self._open_cmd) + await self._device.set_dp(self._open_cmd, self._dp_id) + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + # for timed positioning, stop the cover after a full opening timespan + # instead of waiting the internal timeout + self.hass.async_create_task( + self.async_stop_after_timeout( + self._config[CONF_SPAN_TIME] + COVER_TIMEOUT_TOLERANCE + ) + ) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self.debug("Launching command %s to cover ", self._close_cmd) + await self._device.set_dp(self._close_cmd, self._dp_id) + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + # for timed positioning, stop the cover after a full opening timespan + # instead of waiting the internal timeout + self.hass.async_create_task( + self.async_stop_after_timeout( + self._config[CONF_SPAN_TIME] + COVER_TIMEOUT_TOLERANCE + ) + ) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + self.debug("Launching command %s to cover ", self._stop_cmd) + await self._device.set_dp(self._stop_cmd, self._dp_id) + + def status_restored(self, stored_state): + """Restore the last stored cover status.""" + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + stored_pos = stored_state.attributes.get("current_position") + if stored_pos is not None: + self._current_cover_position = stored_pos + self.debug("Restored cover position %s", self._current_cover_position) + + def status_updated(self): + """Device status was updated.""" + self._previous_state = self._state + self._state = self.dps(self._dp_id) + if self._state.isupper(): + self._open_cmd = self._open_cmd.upper() + self._close_cmd = self._close_cmd.upper() + self._stop_cmd = self._stop_cmd.upper() + + if self.has_config(CONF_CURRENT_POSITION_DP): + curr_pos = self.dps_conf(CONF_CURRENT_POSITION_DP) + if self._config[CONF_POSITION_INVERTED]: + self._current_cover_position = 100 - curr_pos + else: + self._current_cover_position = curr_pos + if ( + self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED + and self._state != self._previous_state + ): + if self._previous_state != self._stop_cmd: + # the state has changed, and the cover was moving + time_diff = time.time() - self._timer_start + pos_diff = round(time_diff / self._config[CONF_SPAN_TIME] * 100.0) + if self._previous_state == self._close_cmd: + pos_diff = -pos_diff + self._current_cover_position = min( + 100, max(0, self._current_cover_position + pos_diff) + ) + + change = "stopped" if self._state == self._stop_cmd else "inverted" + self.debug( + "Movement %s after %s sec., position difference %s", + change, + time_diff, + pos_diff, + ) + + # store the time of the last movement change + self._timer_start = time.time() + + # Keep record in last_state as long as not during connection/re-connection, + # as last state will be used to restore the previous state + if (self._state is not None) and (not self._device.is_connecting): + self._last_state = self._state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaCover, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/diagnostics.py b/homeassistant/config/custom_components/localtuya/diagnostics.py new file mode 100644 index 0000000..9c84a93 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/diagnostics.py @@ -0,0 +1,65 @@ +"""Diagnostics support for LocalTuya.""" +from __future__ import annotations + +import copy +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DEVICES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import CONF_LOCAL_KEY, CONF_USER_ID, DATA_CLOUD, DOMAIN + +CLOUD_DEVICES = "cloud_devices" +DEVICE_CONFIG = "device_config" +DEVICE_CLOUD_INFO = "device_cloud_info" + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = {} + data = dict(entry.data) + tuya_api = hass.data[DOMAIN][DATA_CLOUD] + # censoring private information on integration diagnostic data + for field in [CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_USER_ID]: + data[field] = f"{data[field][0:3]}...{data[field][-3:]}" + data[CONF_DEVICES] = copy.deepcopy(entry.data[CONF_DEVICES]) + for dev_id, dev in data[CONF_DEVICES].items(): + local_key = dev[CONF_LOCAL_KEY] + local_key_obfuscated = f"{local_key[0:3]}...{local_key[-3:]}" + dev[CONF_LOCAL_KEY] = local_key_obfuscated + data[CLOUD_DEVICES] = tuya_api.device_list + for dev_id, dev in data[CLOUD_DEVICES].items(): + local_key = data[CLOUD_DEVICES][dev_id][CONF_LOCAL_KEY] + local_key_obfuscated = f"{local_key[0:3]}...{local_key[-3:]}" + data[CLOUD_DEVICES][dev_id][CONF_LOCAL_KEY] = local_key_obfuscated + return data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + data = {} + dev_id = list(device.identifiers)[0][1].split("_")[-1] + data[DEVICE_CONFIG] = entry.data[CONF_DEVICES][dev_id].copy() + # NOT censoring private information on device diagnostic data + # local_key = data[DEVICE_CONFIG][CONF_LOCAL_KEY] + # data[DEVICE_CONFIG][CONF_LOCAL_KEY] = f"{local_key[0:3]}...{local_key[-3:]}" + + tuya_api = hass.data[DOMAIN][DATA_CLOUD] + if dev_id in tuya_api.device_list: + data[DEVICE_CLOUD_INFO] = tuya_api.device_list[dev_id] + # NOT censoring private information on device diagnostic data + # local_key = data[DEVICE_CLOUD_INFO][CONF_LOCAL_KEY] + # local_key_obfuscated = "{local_key[0:3]}...{local_key[-3:]}" + # data[DEVICE_CLOUD_INFO][CONF_LOCAL_KEY] = local_key_obfuscated + + # data["log"] = hass.data[DOMAIN][CONF_DEVICES][dev_id].logger.retrieve_log() + return data diff --git a/homeassistant/config/custom_components/localtuya/discovery.py b/homeassistant/config/custom_components/localtuya/discovery.py new file mode 100644 index 0000000..0c93ab7 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/discovery.py @@ -0,0 +1,90 @@ +"""Discovery module for Tuya devices. + +Entirely based on tuya-convert.py from tuya-convert: + +https://github.com/ct-Open-Source/tuya-convert/blob/master/scripts/tuya-discovery.py +""" +import asyncio +import json +import logging +from hashlib import md5 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +_LOGGER = logging.getLogger(__name__) + +UDP_KEY = md5(b"yGAdlopoPVldABfn").digest() + +DEFAULT_TIMEOUT = 6.0 + + +def decrypt_udp(message): + """Decrypt encrypted UDP broadcasts.""" + + def _unpad(data): + return data[: -ord(data[len(data) - 1 :])] + + cipher = Cipher(algorithms.AES(UDP_KEY), modes.ECB(), default_backend()) + decryptor = cipher.decryptor() + return _unpad(decryptor.update(message) + decryptor.finalize()).decode() + + +class TuyaDiscovery(asyncio.DatagramProtocol): + """Datagram handler listening for Tuya broadcast messages.""" + + def __init__(self, callback=None): + """Initialize a new BaseDiscovery.""" + self.devices = {} + self._listeners = [] + self._callback = callback + + async def start(self): + """Start discovery by listening to broadcasts.""" + loop = asyncio.get_running_loop() + listener = loop.create_datagram_endpoint( + lambda: self, local_addr=("0.0.0.0", 6666), reuse_port=True + ) + encrypted_listener = loop.create_datagram_endpoint( + lambda: self, local_addr=("0.0.0.0", 6667), reuse_port=True + ) + + self._listeners = await asyncio.gather(listener, encrypted_listener) + _LOGGER.debug("Listening to broadcasts on UDP port 6666 and 6667") + + def close(self): + """Stop discovery.""" + self._callback = None + for transport, _ in self._listeners: + transport.close() + + def datagram_received(self, data, addr): + """Handle received broadcast message.""" + data = data[20:-8] + try: + data = decrypt_udp(data) + except Exception: # pylint: disable=broad-except + data = data.decode() + + decoded = json.loads(data) + self.device_found(decoded) + + def device_found(self, device): + """Discover a new device.""" + if device.get("gwId") not in self.devices: + self.devices[device.get("gwId")] = device + _LOGGER.debug("Discovered device: %s", device) + + if self._callback: + self._callback(device) + + +async def discover(): + """Discover and return devices on local network.""" + discovery = TuyaDiscovery() + try: + await discovery.start() + await asyncio.sleep(DEFAULT_TIMEOUT) + finally: + discovery.close() + return discovery.devices diff --git a/homeassistant/config/custom_components/localtuya/fan.py b/homeassistant/config/custom_components/localtuya/fan.py new file mode 100644 index 0000000..59c33ac --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/fan.py @@ -0,0 +1,259 @@ +"""Platform to locally control Tuya-based fan devices.""" +import logging +import math +from functools import partial + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN, + FanEntityFeature, + FanEntity, +) +from homeassistant.util.percentage import ( + int_states_in_range, + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_FAN_DIRECTION, + CONF_FAN_DIRECTION_FWD, + CONF_FAN_DIRECTION_REV, + CONF_FAN_DPS_TYPE, + CONF_FAN_ORDERED_LIST, + CONF_FAN_OSCILLATING_CONTROL, + CONF_FAN_SPEED_CONTROL, + CONF_FAN_SPEED_MAX, + CONF_FAN_SPEED_MIN, +) + +_LOGGER = logging.getLogger(__name__) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), + vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), + vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), + vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, + vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, + vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, + vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, + vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, + vol.Optional(CONF_FAN_DPS_TYPE, default="str"): vol.In(["str", "int"]), + } + + +class LocaltuyaFan(LocalTuyaEntity, FanEntity): + """Representation of a Tuya fan.""" + + def __init__( + self, + device, + config_entry, + fanid, + **kwargs, + ): + """Initialize the entity.""" + super().__init__(device, config_entry, fanid, _LOGGER, **kwargs) + self._is_on = False + self._oscillating = None + self._direction = None + self._percentage = None + self._speed_range = ( + self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MAX), + ) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") + self._ordered_list_mode = None + self._dps_type = int if self._config.get(CONF_FAN_DPS_TYPE) == "int" else str + + if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1: + self._use_ordered_list = True + _LOGGER.debug( + "Fan _use_ordered_list: %s > %s", + self._use_ordered_list, + self._ordered_list, + ) + else: + self._use_ordered_list = False + _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) + + @property + def oscillating(self): + """Return current oscillating status.""" + return self._oscillating + + @property + def current_direction(self): + """Return the current direction of the fan.""" + return self._direction + + @property + def is_on(self): + """Check if Tuya fan is on.""" + return self._is_on + + @property + def percentage(self): + """Return the current percentage.""" + return self._percentage + + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turn on the entity.""" + _LOGGER.debug("Fan async_turn_on") + await self._device.set_dp(True, self._dp_id) + if percentage is not None: + await self.async_set_percentage(percentage) + else: + self.schedule_update_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + _LOGGER.debug("Fan async_turn_off") + + await self._device.set_dp(False, self._dp_id) + self.schedule_update_ha_state() + + async def async_set_percentage(self, percentage): + """Set the speed of the fan.""" + _LOGGER.debug("Fan async_set_percentage: %s", percentage) + + if percentage is not None: + if percentage == 0: + return await self.async_turn_off() + if not self.is_on: + await self.async_turn_on() + if self._use_ordered_list: + await self._device.set_dp( + self._dps_type( + percentage_to_ordered_list_item(self._ordered_list, percentage) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ordered_list_item(self._ordered_list, percentage), + ) + + else: + await self._device.set_dp( + self._dps_type( + math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ranged_value(self._speed_range, percentage), + ) + self.schedule_update_ha_state() + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + _LOGGER.debug("Fan async_oscillate: %s", oscillating) + await self._device.set_dp( + oscillating, self._config.get(CONF_FAN_OSCILLATING_CONTROL) + ) + self.schedule_update_ha_state() + + async def async_set_direction(self, direction): + """Set the direction of the fan.""" + _LOGGER.debug("Fan async_set_direction: %s", direction) + + if direction == DIRECTION_FORWARD: + value = self._config.get(CONF_FAN_DIRECTION_FWD) + + if direction == DIRECTION_REVERSE: + value = self._config.get(CONF_FAN_DIRECTION_REV) + await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION)) + self.schedule_update_ha_state() + + @property + def supported_features(self) -> FanEntityFeature: + """Flag supported features.""" + features = FanEntityFeature(0) + + if self.has_config(CONF_FAN_OSCILLATING_CONTROL): + features |= FanEntityFeature.OSCILLATE + + if self.has_config(CONF_FAN_SPEED_CONTROL): + features |= FanEntityFeature.SET_SPEED + + if self.has_config(CONF_FAN_DIRECTION): + features |= FanEntityFeature.DIRECTION + + features |= FanEntityFeature.TURN_OFF + features |= FanEntityFeature.TURN_ON + + return features + + @property + def speed_count(self) -> int: + """Speed count for the fan.""" + speed_count = int_states_in_range(self._speed_range) + _LOGGER.debug("Fan speed_count: %s", speed_count) + return speed_count + + def status_updated(self): + """Get state of Tuya fan.""" + self._is_on = self.dps(self._dp_id) + + current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) + if self._use_ordered_list: + _LOGGER.debug( + "Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, + self._ordered_list, + ) + if current_speed is not None: + self._percentage = ordered_list_item_to_percentage( + self._ordered_list, str(current_speed) + ) + + else: + _LOGGER.debug( + "Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, + self._speed_range, + ) + if current_speed is not None: + self._percentage = ranged_value_to_percentage( + self._speed_range, int(current_speed) + ) + + _LOGGER.debug("Fan current_percentage: %s", self._percentage) + + if self.has_config(CONF_FAN_OSCILLATING_CONTROL): + self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) + _LOGGER.debug("Fan current_oscillating : %s", self._oscillating) + + if self.has_config(CONF_FAN_DIRECTION): + value = self.dps_conf(CONF_FAN_DIRECTION) + if value is not None: + if value == self._config.get(CONF_FAN_DIRECTION_FWD): + self._direction = DIRECTION_FORWARD + + if value == self._config.get(CONF_FAN_DIRECTION_REV): + self._direction = DIRECTION_REVERSE + _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/light.py b/homeassistant/config/custom_components/localtuya/light.py new file mode 100644 index 0000000..66773c2 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/light.py @@ -0,0 +1,506 @@ +"""Platform to locally control Tuya-based light devices.""" +import logging +import textwrap +from dataclasses import dataclass +from functools import partial + +import homeassistant.util.color as color_util +import voluptuous as vol +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + DOMAIN, + LightEntity, + LightEntityFeature, + ColorMode, +) +from homeassistant.const import CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_SCENE + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_BRIGHTNESS_LOWER, + CONF_BRIGHTNESS_UPPER, + CONF_COLOR, + CONF_COLOR_MODE, + CONF_COLOR_TEMP_MAX_KELVIN, + CONF_COLOR_TEMP_MIN_KELVIN, + CONF_COLOR_TEMP_REVERSE, + CONF_MUSIC_MODE, CONF_COLOR_MODE_SET, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_MIN_KELVIN = 2700 # MIRED 370 +DEFAULT_MAX_KELVIN = 6500 # MIRED 153 + +DEFAULT_COLOR_TEMP_REVERSE = False + +DEFAULT_LOWER_BRIGHTNESS = 29 +DEFAULT_UPPER_BRIGHTNESS = 1000 + +MODE_MANUAL = "manual" +MODE_COLOR = "colour" +MODE_MUSIC = "music" +MODE_SCENE = "scene" +MODE_WHITE = "white" + +SCENE_CUSTOM = "Custom" +SCENE_MUSIC = "Music" + +MODES_SET = {"Colour, Music, Scene and White": 0, "Manual, Music, Scene and White": 1} + +SCENE_LIST_RGBW_1000 = { + "Night": "000e0d0000000000000000c80000", + "Read": "010e0d0000000000000003e801f4", + "Meeting": "020e0d0000000000000003e803e8", + "Leasure": "030e0d0000000000000001f401f4", + "Soft": "04464602007803e803e800000000464602007803e8000a00000000", + "Rainbow": "05464601000003e803e800000000464601007803e803e80000000046460100f003e803" + + "e800000000", + "Shine": "06464601000003e803e800000000464601007803e803e80000000046460100f003e803e8" + + "00000000", + "Beautiful": "07464602000003e803e800000000464602007803e803e80000000046460200f003e8" + + "03e800000000464602003d03e803e80000000046460200ae03e803e800000000464602011303e80" + + "3e800000000", +} + +SCENE_LIST_RGBW_255 = { + "Night": "bd76000168ffff", + "Read": "fffcf70168ffff", + "Meeting": "cf38000168ffff", + "Leasure": "3855b40168ffff", + "Scenario 1": "scene_1", + "Scenario 2": "scene_2", + "Scenario 3": "scene_3", + "Scenario 4": "scene_4", +} + +SCENE_LIST_RGB_1000 = { + "Night": "000e0d00002e03e802cc00000000", + "Read": "010e0d000084000003e800000000", + "Working": "020e0d00001403e803e800000000", + "Leisure": "030e0d0000e80383031c00000000", + "Soft": "04464602007803e803e800000000464602007803e8000a00000000", + "Colorful": "05464601000003e803e800000000464601007803e803e80000000046460100f003e80" + + "3e800000000464601003d03e803e80000000046460100ae03e803e800000000464601011303e803" + + "e800000000", + "Dazzling": "06464601000003e803e800000000464601007803e803e80000000046460100f003e80" + + "3e800000000", + "Music": "07464602000003e803e800000000464602007803e803e80000000046460200f003e803e8" + + "00000000464602003d03e803e80000000046460200ae03e803e800000000464602011303e803e80" + + "0000000", +} + +@dataclass(frozen=True) +class Mode: + color: str = MODE_COLOR + music: str = MODE_MUSIC + scene: str = MODE_SCENE + white: str = MODE_WHITE + + def as_list(self) -> list: + return [self.color, self.music, self.scene, self.white] + + def as_dict(self) -> dict[str, str]: + default = {"Default": self.white} + return {**default, "Mode Color": self.color, "Mode Scene": self.scene} + +MAP_MODE_SET = {0: Mode(), 1: Mode(color=MODE_MANUAL)} + + +def map_range(value, from_lower, from_upper, to_lower, to_upper): + """Map a value in one range to another.""" + mapped = (value - from_lower) * (to_upper - to_lower) / ( + from_upper - from_lower + ) + to_lower + return round(min(max(mapped, to_lower), to_upper)) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_BRIGHTNESS): vol.In(dps), + vol.Optional(CONF_COLOR_TEMP): vol.In(dps), + vol.Optional(CONF_BRIGHTNESS_LOWER, default=DEFAULT_LOWER_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=10000) + ), + vol.Optional(CONF_BRIGHTNESS_UPPER, default=DEFAULT_UPPER_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=10000) + ), + vol.Optional(CONF_COLOR_MODE): vol.In(dps), + vol.Optional(CONF_COLOR): vol.In(dps), + vol.Optional(CONF_COLOR_TEMP_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1500, max=8000) + ), + vol.Optional(CONF_COLOR_TEMP_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1500, max=8000) + ), + vol.Optional( + CONF_COLOR_TEMP_REVERSE, + default=DEFAULT_COLOR_TEMP_REVERSE, + description={"suggested_value": DEFAULT_COLOR_TEMP_REVERSE}, + ): bool, + vol.Optional(CONF_SCENE): vol.In(dps), + vol.Optional( + CONF_MUSIC_MODE, default=False, description={"suggested_value": False} + ): bool, + } + + +class LocaltuyaLight(LocalTuyaEntity, LightEntity): + """Representation of a Tuya light.""" + + def __init__( + self, + device, + config_entry, + lightid, + **kwargs, + ): + """Initialize the Tuya light.""" + super().__init__(device, config_entry, lightid, _LOGGER, **kwargs) + self._state = False + self._brightness = None + self._color_temp = None + self._lower_brightness = self._config.get( + CONF_BRIGHTNESS_LOWER, DEFAULT_LOWER_BRIGHTNESS + ) + self._upper_brightness = self._config.get( + CONF_BRIGHTNESS_UPPER, DEFAULT_UPPER_BRIGHTNESS + ) + self._upper_color_temp = self._upper_brightness + self._max_mired = color_util.color_temperature_kelvin_to_mired( + self._config.get(CONF_COLOR_TEMP_MIN_KELVIN, DEFAULT_MIN_KELVIN) + ) + self._min_mired = color_util.color_temperature_kelvin_to_mired( + self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) + ) + self._color_temp_reverse = self._config.get( + CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE + ) + self._modes = MAP_MODE_SET[int(self._config.get(CONF_COLOR_MODE_SET, 0))] + self._hs = None + self._effect = None + self._effect_list = [] + self._scenes = {} + + if self.has_config(CONF_SCENE): + if self._config.get(CONF_SCENE) < 20: + self._scenes = SCENE_LIST_RGBW_255 + elif self._config.get(CONF_BRIGHTNESS) is None: + self._scenes = SCENE_LIST_RGB_1000 + else: + self._scenes = SCENE_LIST_RGBW_1000 + self._effect_list = list(self._scenes.keys()) + + if self._config.get(CONF_MUSIC_MODE): + self._effect_list.append(SCENE_MUSIC) + + @property + def is_on(self): + """Check if Tuya light is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of the light.""" + if self.is_color_mode or self.is_white_mode: + return map_range( + self._brightness, self._lower_brightness, self._upper_brightness, 0, 255 + ) + return None + + @property + def hs_color(self): + """Return the hs color value.""" + if self.is_color_mode: + return self._hs + if ( + ColorMode.HS in self.supported_color_modes + and not ColorMode.COLOR_TEMP in self.supported_color_modes + ): + return [0, 0] + return None + + @property + def color_temp(self): + """Return the color_temp of the light.""" + if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: + color_temp_value = ( + self._upper_color_temp - self._color_temp + if self._color_temp_reverse + else self._color_temp + ) + return int( + self._max_mired + - ( + ((self._max_mired - self._min_mired) / self._upper_color_temp) + * color_temp_value + ) + ) + return None + + @property + def min_mireds(self): + """Return color temperature min mireds.""" + return self._min_mired + + @property + def max_mireds(self): + """Return color temperature max mireds.""" + return self._max_mired + + @property + def effect(self): + """Return the current effect for this light.""" + if self.is_scene_mode or self.is_music_mode: + return self._effect + return None + + @property + def effect_list(self): + """Return the list of supported effects for this light.""" + if self.is_scene_mode or self.is_music_mode: + return self._effect + elif (color_mode := self.__get_color_mode()) in self._scenes.values(): + return self.__find_scene_by_scene_data(color_mode) + return None + + @property + def supported_color_modes(self) -> set[ColorMode] | set[str] | None: + """Flag supported color modes.""" + color_modes: set[ColorMode] = set() + + if self.has_config(CONF_COLOR_TEMP): + color_modes.add(ColorMode.COLOR_TEMP) + if self.has_config(CONF_COLOR): + color_modes.add(ColorMode.HS) + + if not color_modes and self.has_config(CONF_BRIGHTNESS): + return {ColorMode.BRIGHTNESS} + + if not color_modes: + return {ColorMode.ONOFF} + + return color_modes + + @property + def supported_features(self) -> LightEntityFeature: + """Flag supported features.""" + supports = LightEntityFeature(0) + if self.has_config(CONF_SCENE) or self.has_config(CONF_MUSIC_MODE): + supports |= LightEntityFeature.EFFECT + return supports + + @property + def color_mode(self) -> ColorMode: + """Return the color_mode of the light.""" + if len(self.supported_color_modes) == 1: + return next(iter(self.supported_color_modes)) + + if self.is_color_mode: + return ColorMode.HS + if self.is_white_mode: + return ColorMode.COLOR_TEMP + if self._brightness: + return ColorMode.BRIGHTNESS + + return ColorMode.ONOFF + + @property + def is_white_mode(self): + """Return true if the light is in white mode.""" + color_mode = self.__get_color_mode() + return color_mode is None or color_mode == self._modes.white + + @property + def is_color_mode(self): + """Return true if the light is in color mode.""" + color_mode = self.__get_color_mode() + return color_mode is not None and color_mode == self._modes.color + + @property + def is_scene_mode(self): + """Return true if the light is in scene mode.""" + color_mode = self.__get_color_mode() + return color_mode is not None and color_mode.startswith(self._modes.scene) + + @property + def is_music_mode(self): + """Return true if the light is in music mode.""" + color_mode = self.__get_color_mode() + return color_mode is not None and color_mode == self._modes.music + + def __is_color_rgb_encoded(self): + return len(self.dps_conf(CONF_COLOR)) > 12 + + def __find_scene_by_scene_data(self, data): + return next( + (item for item in self._effect_list if self._scenes.get(item) == data), + SCENE_CUSTOM, + ) + + def __get_color_mode(self): + return ( + self.dps_conf(CONF_COLOR_MODE) + if self.has_config(CONF_COLOR_MODE) + else self._modes.white + ) + + async def async_turn_on(self, **kwargs): + """Turn on or control the light.""" + states = {} + if not self.is_on: + states[self._dp_id] = True + features = self.supported_features + brightness = None + if ATTR_EFFECT in kwargs and (features & LightEntityFeature.EFFECT): + scene = self._scenes.get(kwargs[ATTR_EFFECT]) + if scene is not None: + if scene.startswith(MODE_SCENE): + states[self._config.get(CONF_COLOR_MODE)] = scene + else: + states[self._config.get(CONF_COLOR_MODE)] = MODE_SCENE + states[self._config.get(CONF_SCENE)] = scene + elif kwargs[ATTR_EFFECT] == SCENE_MUSIC: + states[self._config.get(CONF_COLOR_MODE)] = MODE_MUSIC + + if ATTR_BRIGHTNESS in kwargs and ( + ColorMode.BRIGHTNESS in self.supported_color_modes + or self.has_config(CONF_BRIGHTNESS) + or self.has_config(CONF_COLOR) + ): + brightness = map_range( + int(kwargs[ATTR_BRIGHTNESS]), + 0, + 255, + self._lower_brightness, + self._upper_brightness, + ) + if self.is_white_mode: + states[self._config.get(CONF_BRIGHTNESS)] = brightness + else: + if self.__is_color_rgb_encoded(): + rgb = color_util.color_hsv_to_RGB( + self._hs[0], + self._hs[1], + int(brightness * 100 / self._upper_brightness), + ) + color = "{:02x}{:02x}{:02x}{:04x}{:02x}{:02x}".format( + round(rgb[0]), + round(rgb[1]), + round(rgb[2]), + round(self._hs[0]), + round(self._hs[1] * 255 / 100), + brightness, + ) + else: + color = "{:04x}{:04x}{:04x}".format( + round(self._hs[0]), round(self._hs[1] * 10.0), brightness + ) + states[self._config.get(CONF_COLOR)] = color + states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR + + if ATTR_HS_COLOR in kwargs and ColorMode.HS in self.supported_color_modes: + if brightness is None: + brightness = self._brightness + hs = kwargs[ATTR_HS_COLOR] + if hs[1] == 0 and self.has_config(CONF_BRIGHTNESS): + states[self._config.get(CONF_BRIGHTNESS)] = brightness + states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE + else: + if self.__is_color_rgb_encoded(): + rgb = color_util.color_hsv_to_RGB( + hs[0], hs[1], int(brightness * 100 / self._upper_brightness) + ) + color = "{:02x}{:02x}{:02x}{:04x}{:02x}{:02x}".format( + round(rgb[0]), + round(rgb[1]), + round(rgb[2]), + round(hs[0]), + round(hs[1] * 255 / 100), + brightness, + ) + else: + color = "{:04x}{:04x}{:04x}".format( + round(hs[0]), round(hs[1] * 10.0), brightness + ) + states[self._config.get(CONF_COLOR)] = color + states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR + + if ColorMode.COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in self.supported_color_modes: + if brightness is None: + brightness = self._brightness + mired = int(kwargs[ColorMode.COLOR_TEMP]) + if self._color_temp_reverse: + mired = self._max_mired - (mired - self._min_mired) + if mired < self._min_mired: + mired = self._min_mired + elif mired > self._max_mired: + mired = self._max_mired + color_temp = int( + self._upper_color_temp + - (self._upper_color_temp / (self._max_mired - self._min_mired)) + * (mired - self._min_mired) + ) + states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE + states[self._config.get(CONF_BRIGHTNESS)] = brightness + states[self._config.get(CONF_COLOR_TEMP)] = color_temp + await self._device.set_dps(states) + + async def async_turn_off(self, **kwargs): + """Turn Tuya light off.""" + await self._device.set_dp(False, self._dp_id) + + def status_updated(self): + """Device status was updated.""" + self._state = self.dps(self._dp_id) + supported = self.supported_features + self._effect = None + + if (ColorMode.BRIGHTNESS in self.supported_color_modes + or self.has_config(CONF_BRIGHTNESS) + or self.has_config(CONF_COLOR) + ): + self._brightness = self.dps_conf(CONF_BRIGHTNESS) + + if ColorMode.HS in self.supported_color_modes: + color = self.dps_conf(CONF_COLOR) + if color is not None and not self.is_white_mode: + if self.__is_color_rgb_encoded(): + hue = int(color[6:10], 16) + sat = int(color[10:12], 16) + value = int(color[12:14], 16) + self._hs = [hue, (sat * 100 / 255)] + self._brightness = value + else: + hue, sat, value = [ + int(value, 16) for value in textwrap.wrap(color, 4) + ] + self._hs = [hue, sat / 10.0] + self._brightness = value + + if ColorMode.COLOR_TEMP in self.supported_color_modes: + self._color_temp = self.dps_conf(CONF_COLOR_TEMP) + + if self.is_scene_mode and supported & LightEntityFeature.EFFECT: + if self.dps_conf(CONF_COLOR_MODE) != MODE_SCENE: + self._effect = self.__find_scene_by_scene_data( + self.dps_conf(CONF_COLOR_MODE) + ) + else: + self._effect = self.__find_scene_by_scene_data( + self.dps_conf(CONF_SCENE) + ) + if self._effect == SCENE_CUSTOM: + if SCENE_CUSTOM not in self._effect_list: + self._effect_list.append(SCENE_CUSTOM) + elif SCENE_CUSTOM in self._effect_list: + self._effect_list.remove(SCENE_CUSTOM) + + if self.is_music_mode and supported & LightEntityFeature.EFFECT: + self._effect = SCENE_MUSIC + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaLight, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/manifest.json b/homeassistant/config/custom_components/localtuya/manifest.json new file mode 100644 index 0000000..95f34fb --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "localtuya", + "name": "LocalTuya integration", + "codeowners": [ + "@rospogrigio", "@postlund" + ], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/rospogrigio/localtuya/", + "iot_class": "local_push", + "issue_tracker": "https://github.com/rospogrigio/localtuya/issues", + "requirements": [], + "version": "5.2.3" +} diff --git a/homeassistant/config/custom_components/localtuya/number.py b/homeassistant/config/custom_components/localtuya/number.py new file mode 100644 index 0000000..917d3d0 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/number.py @@ -0,0 +1,113 @@ +"""Platform to present any Tuya DP as a number.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_DEFAULT_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_PASSIVE_ENTITY, + CONF_RESTORE_ON_RECONNECT, + CONF_STEPSIZE_VALUE, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_MIN = 0 +DEFAULT_MAX = 100000 +DEFAULT_STEP = 1.0 + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), + ), + vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), + ), + vol.Required(CONF_STEPSIZE_VALUE, default=DEFAULT_STEP): vol.All( + vol.Coerce(float), + vol.Range(min=0.0, max=1000000.0), + ), + vol.Required(CONF_RESTORE_ON_RECONNECT): bool, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, + } + + +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): + """Representation of a Tuya Number.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + + self._min_value = DEFAULT_MIN + if CONF_MIN_VALUE in self._config: + self._min_value = self._config.get(CONF_MIN_VALUE) + + self._max_value = DEFAULT_MAX + if CONF_MAX_VALUE in self._config: + self._max_value = self._config.get(CONF_MAX_VALUE) + + self._step_size = DEFAULT_STEP + if CONF_STEPSIZE_VALUE in self._config: + self._step_size = self._config.get(CONF_STEPSIZE_VALUE) + + # Override standard default value handling to cast to a float + default_value = self._config.get(CONF_DEFAULT_VALUE) + if default_value is not None: + self._default_value = float(default_value) + + @property + def native_value(self) -> float: + """Return sensor state.""" + return self._state + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + return self._min_value + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + return self._max_value + + @property + def native_step(self) -> float: + """Return the maximum value.""" + return self._step_size + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self._device.set_dp(value, self._dp_id) + + # Default value is the minimum value + def entity_default_value(self): + """Return the minimum value as the default for this entity type.""" + return self._min_value + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/pytuya/__init__.py b/homeassistant/config/custom_components/localtuya/pytuya/__init__.py new file mode 100644 index 0000000..67aabb2 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/pytuya/__init__.py @@ -0,0 +1,1196 @@ +# PyTuya Module +# -*- coding: utf-8 -*- +""" +Python module to interface with Tuya WiFi smart devices. + +Author: clach04, postlund +Maintained by: rospogrigio + +For more information see https://github.com/clach04/python-tuya + +Classes + TuyaInterface(dev_id, address, local_key=None) + dev_id (str): Device ID e.g. 01234567891234567890 + address (str): Device Network IP Address e.g. 10.0.1.99 + local_key (str, optional): The encryption key. Defaults to None. + +Functions + json = status() # returns json payload + set_version(version) # 3.1 [default], 3.2, 3.3 or 3.4 + detect_available_dps() # returns a list of available dps provided by the device + update_dps(dps) # sends update dps command + add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the + # device (to be queried in the payload) + set_dp(on, dp_index) # Set value of any dps index. + + + Credits + * TuyaAPI https://github.com/codetheweb/tuyapi by codetheweb and blackrozes + For protocol reverse engineering + * PyTuya https://github.com/clach04/python-tuya by clach04 + The origin of this python module (now abandoned) + * Tuya Protocol 3.4 Support by uzlonewolf + Enhancement to TuyaMessage logic for multi-payload messages and Tuya Protocol 3.4 support + * TinyTuya https://github.com/jasonacox/tinytuya by jasonacox + Several CLI tools and code for Tuya devices +""" + +import asyncio +import base64 +import binascii +import hmac +import json +import logging +import struct +import time +import weakref +from abc import ABC, abstractmethod +from collections import namedtuple +from hashlib import md5, sha256 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +version_tuple = (10, 0, 0) +version = version_string = __version__ = "%d.%d.%d" % version_tuple +__author__ = "rospogrigio" + +_LOGGER = logging.getLogger(__name__) + +# Tuya Packet Format +TuyaHeader = namedtuple("TuyaHeader", "prefix seqno cmd length") +MessagePayload = namedtuple("MessagePayload", "cmd payload") +try: + TuyaMessage = namedtuple( + "TuyaMessage", "seqno cmd retcode payload crc crc_good", defaults=(True,) + ) +except Exception: + TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc crc_good") + +# TinyTuya Error Response Codes +ERR_JSON = 900 +ERR_CONNECT = 901 +ERR_TIMEOUT = 902 +ERR_RANGE = 903 +ERR_PAYLOAD = 904 +ERR_OFFLINE = 905 +ERR_STATE = 906 +ERR_FUNCTION = 907 +ERR_DEVTYPE = 908 +ERR_CLOUDKEY = 909 +ERR_CLOUDRESP = 910 +ERR_CLOUDTOKEN = 911 +ERR_PARAMS = 912 +ERR_CLOUD = 913 + +error_codes = { + ERR_JSON: "Invalid JSON Response from Device", + ERR_CONNECT: "Network Error: Unable to Connect", + ERR_TIMEOUT: "Timeout Waiting for Device", + ERR_RANGE: "Specified Value Out of Range", + ERR_PAYLOAD: "Unexpected Payload from Device", + ERR_OFFLINE: "Network Error: Device Unreachable", + ERR_STATE: "Device in Unknown State", + ERR_FUNCTION: "Function Not Supported by Device", + ERR_DEVTYPE: "Device22 Detected: Retry Command", + ERR_CLOUDKEY: "Missing Tuya Cloud Key and Secret", + ERR_CLOUDRESP: "Invalid JSON Response from Cloud", + ERR_CLOUDTOKEN: "Unable to Get Cloud Token", + ERR_PARAMS: "Missing Function Parameters", + ERR_CLOUD: "Error Response from Tuya Cloud", + None: "Unknown Error", +} + + +class DecodeError(Exception): + """Specific Exception caused by decoding error.""" + + pass + + +# Tuya Command Types +# Reference: +# https://github.com/tuya/tuya-iotos-embeded-sdk-wifi-ble-bk7231n/blob/master/sdk/include/lan_protocol.h +AP_CONFIG = 0x01 # FRM_TP_CFG_WF # only used for ap 3.0 network config +ACTIVE = 0x02 # FRM_TP_ACTV (discard) # WORK_MODE_CMD +SESS_KEY_NEG_START = 0x03 # FRM_SECURITY_TYPE3 # negotiate session key +SESS_KEY_NEG_RESP = 0x04 # FRM_SECURITY_TYPE4 # negotiate session key response +SESS_KEY_NEG_FINISH = 0x05 # FRM_SECURITY_TYPE5 # finalize session key negotiation +UNBIND = 0x06 # FRM_TP_UNBIND_DEV # DATA_QUERT_CMD - issue command +CONTROL = 0x07 # FRM_TP_CMD # STATE_UPLOAD_CMD +STATUS = 0x08 # FRM_TP_STAT_REPORT # STATE_QUERY_CMD +HEART_BEAT = 0x09 # FRM_TP_HB +DP_QUERY = 0x0A # 10 # FRM_QUERY_STAT # UPDATE_START_CMD - get data points +QUERY_WIFI = 0x0B # 11 # FRM_SSID_QUERY (discard) # UPDATE_TRANS_CMD +TOKEN_BIND = 0x0C # 12 # FRM_USER_BIND_REQ # GET_ONLINE_TIME_CMD - system time (GMT) +CONTROL_NEW = 0x0D # 13 # FRM_TP_NEW_CMD # FACTORY_MODE_CMD +ENABLE_WIFI = 0x0E # 14 # FRM_ADD_SUB_DEV_CMD # WIFI_TEST_CMD +WIFI_INFO = 0x0F # 15 # FRM_CFG_WIFI_INFO +DP_QUERY_NEW = 0x10 # 16 # FRM_QUERY_STAT_NEW +SCENE_EXECUTE = 0x11 # 17 # FRM_SCENE_EXEC +UPDATEDPS = 0x12 # 18 # FRM_LAN_QUERY_DP # Request refresh of DPS +UDP_NEW = 0x13 # 19 # FR_TYPE_ENCRYPTION +AP_CONFIG_NEW = 0x14 # 20 # FRM_AP_CFG_WF_V40 +BOARDCAST_LPV34 = 0x23 # 35 # FR_TYPE_BOARDCAST_LPV34 +LAN_EXT_STREAM = 0x40 # 64 # FRM_LAN_EXT_STREAM + + +PROTOCOL_VERSION_BYTES_31 = b"3.1" +PROTOCOL_VERSION_BYTES_33 = b"3.3" +PROTOCOL_VERSION_BYTES_34 = b"3.4" + +PROTOCOL_3x_HEADER = 12 * b"\x00" +PROTOCOL_33_HEADER = PROTOCOL_VERSION_BYTES_33 + PROTOCOL_3x_HEADER +PROTOCOL_34_HEADER = PROTOCOL_VERSION_BYTES_34 + PROTOCOL_3x_HEADER +MESSAGE_HEADER_FMT = ">4I" # 4*uint32: prefix, seqno, cmd, length [, retcode] +MESSAGE_RECV_HEADER_FMT = ">5I" # 4*uint32: prefix, seqno, cmd, length, retcode +MESSAGE_RETCODE_FMT = ">I" # retcode for received messages +MESSAGE_END_FMT = ">2I" # 2*uint32: crc, suffix +MESSAGE_END_FMT_HMAC = ">32sI" # 32s:hmac, uint32:suffix +PREFIX_VALUE = 0x000055AA +PREFIX_BIN = b"\x00\x00U\xaa" +SUFFIX_VALUE = 0x0000AA55 +SUFFIX_BIN = b"\x00\x00\xaaU" +NO_PROTOCOL_HEADER_CMDS = [ + DP_QUERY, + DP_QUERY_NEW, + UPDATEDPS, + HEART_BEAT, + SESS_KEY_NEG_START, + SESS_KEY_NEG_RESP, + SESS_KEY_NEG_FINISH, +] + +HEARTBEAT_INTERVAL = 10 + +# DPS that are known to be safe to use with update_dps (0x12) command +UPDATE_DPS_WHITELIST = [18, 19, 20] # Socket (Wi-Fi) + +# Tuya Device Dictionary - Command and Payload Overrides +# This is intended to match requests.json payload at +# https://github.com/codetheweb/tuyapi : +# 'type_0a' devices require the 0a command for the DP_QUERY request +# 'type_0d' devices require the 0d command for the DP_QUERY request and a list of +# dps used set to Null in the request payload +# prefix: # Next byte is command byte ("hexByte") some zero padding, then length +# of remaining payload, i.e. command + suffix (unclear if multiple bytes used for +# length, zero padding implies could be more than one byte) + +# Any command not defined in payload_dict will be sent as-is with a +# payload of {"gwId": "", "devId": "", "uid": "", "t": ""} + +payload_dict = { + # Default Device + "type_0a": { + AP_CONFIG: { # [BETA] Set Control Values on Device + "command": {"gwId": "", "devId": "", "uid": "", "t": ""}, + }, + CONTROL: { # Set Control Values on Device + "command": {"devId": "", "uid": "", "t": ""}, + }, + STATUS: { # Get Status from Device + "command": {"gwId": "", "devId": ""}, + }, + HEART_BEAT: {"command": {"gwId": "", "devId": ""}}, + DP_QUERY: { # Get Data Points from Device + "command": {"gwId": "", "devId": "", "uid": "", "t": ""}, + }, + CONTROL_NEW: {"command": {"devId": "", "uid": "", "t": ""}}, + DP_QUERY_NEW: {"command": {"devId": "", "uid": "", "t": ""}}, + UPDATEDPS: {"command": {"dpId": [18, 19, 20]}}, + }, + # Special Case Device "0d" - Some of these devices + # Require the 0d command as the DP_QUERY status request and the list of + # dps requested payload + "type_0d": { + DP_QUERY: { # Get Data Points from Device + "command_override": CONTROL_NEW, # Uses CONTROL_NEW command for some reason + "command": {"devId": "", "uid": "", "t": ""}, + }, + }, + "v3.4": { + CONTROL: { + "command_override": CONTROL_NEW, # Uses CONTROL_NEW command + "command": {"protocol": 5, "t": "int", "data": ""}, + }, + DP_QUERY: {"command_override": DP_QUERY_NEW}, + }, +} + + +class TuyaLoggingAdapter(logging.LoggerAdapter): + """Adapter that adds device id to all log points.""" + + def process(self, msg, kwargs): + """Process log point and return output.""" + dev_id = self.extra["device_id"] + return f"[{dev_id[0:3]}...{dev_id[-3:]}] {msg}", kwargs + + +class ContextualLogger: + """Contextual logger adding device id to log points.""" + + def __init__(self): + """Initialize a new ContextualLogger.""" + self._logger = None + self._enable_debug = False + + def set_logger(self, logger, device_id, enable_debug=False): + """Set base logger to use.""" + self._enable_debug = enable_debug + self._logger = TuyaLoggingAdapter(logger, {"device_id": device_id}) + + def debug(self, msg, *args): + """Debug level log.""" + if not self._enable_debug: + return + return self._logger.log(logging.DEBUG, msg, *args) + + def info(self, msg, *args): + """Info level log.""" + return self._logger.log(logging.INFO, msg, *args) + + def warning(self, msg, *args): + """Warning method log.""" + return self._logger.log(logging.WARNING, msg, *args) + + def error(self, msg, *args): + """Error level log.""" + return self._logger.log(logging.ERROR, msg, *args) + + def exception(self, msg, *args): + """Exception level log.""" + return self._logger.exception(msg, *args) + + +def pack_message(msg, hmac_key=None): + """Pack a TuyaMessage into bytes.""" + end_fmt = MESSAGE_END_FMT_HMAC if hmac_key else MESSAGE_END_FMT + # Create full message excluding CRC and suffix + buffer = ( + struct.pack( + MESSAGE_HEADER_FMT, + PREFIX_VALUE, + msg.seqno, + msg.cmd, + len(msg.payload) + struct.calcsize(end_fmt), + ) + + msg.payload + ) + if hmac_key: + crc = hmac.new(hmac_key, buffer, sha256).digest() + else: + crc = binascii.crc32(buffer) & 0xFFFFFFFF + # Calculate CRC, add it together with suffix + buffer += struct.pack(end_fmt, crc, SUFFIX_VALUE) + return buffer + + +def unpack_message(data, hmac_key=None, header=None, no_retcode=False, logger=None): + """Unpack bytes into a TuyaMessage.""" + end_fmt = MESSAGE_END_FMT_HMAC if hmac_key else MESSAGE_END_FMT + # 4-word header plus return code + header_len = struct.calcsize(MESSAGE_HEADER_FMT) + retcode_len = 0 if no_retcode else struct.calcsize(MESSAGE_RETCODE_FMT) + end_len = struct.calcsize(end_fmt) + headret_len = header_len + retcode_len + + if len(data) < headret_len + end_len: + logger.debug( + "unpack_message(): not enough data to unpack header! need %d but only have %d", + headret_len + end_len, + len(data), + ) + raise DecodeError("Not enough data to unpack header") + + if header is None: + header = parse_header(data) + + if len(data) < header_len + header.length: + logger.debug( + "unpack_message(): not enough data to unpack payload! need %d but only have %d", + header_len + header.length, + len(data), + ) + raise DecodeError("Not enough data to unpack payload") + + retcode = ( + 0 + if no_retcode + else struct.unpack(MESSAGE_RETCODE_FMT, data[header_len:headret_len])[0] + ) + # the retcode is technically part of the payload, but strip it as we do not want it here + payload = data[header_len + retcode_len : header_len + header.length] + crc, suffix = struct.unpack(end_fmt, payload[-end_len:]) + + if hmac_key: + have_crc = hmac.new( + hmac_key, data[: (header_len + header.length) - end_len], sha256 + ).digest() + else: + have_crc = ( + binascii.crc32(data[: (header_len + header.length) - end_len]) & 0xFFFFFFFF + ) + + if suffix != SUFFIX_VALUE: + logger.debug("Suffix prefix wrong! %08X != %08X", suffix, SUFFIX_VALUE) + + if crc != have_crc: + if hmac_key: + logger.debug( + "HMAC checksum wrong! %r != %r", + binascii.hexlify(have_crc), + binascii.hexlify(crc), + ) + else: + logger.debug("CRC wrong! %08X != %08X", have_crc, crc) + + return TuyaMessage( + header.seqno, header.cmd, retcode, payload[:-end_len], crc, crc == have_crc + ) + + +def parse_header(data): + """Unpack bytes into a TuyaHeader.""" + header_len = struct.calcsize(MESSAGE_HEADER_FMT) + + if len(data) < header_len: + raise DecodeError("Not enough data to unpack header") + + prefix, seqno, cmd, payload_len = struct.unpack( + MESSAGE_HEADER_FMT, data[:header_len] + ) + + if prefix != PREFIX_VALUE: + # self.debug('Header prefix wrong! %08X != %08X', prefix, PREFIX_VALUE) + raise DecodeError("Header prefix wrong! %08X != %08X" % (prefix, PREFIX_VALUE)) + + # sanity check. currently the max payload length is somewhere around 300 bytes + if payload_len > 1000: + raise DecodeError( + "Header claims the packet size is over 1000 bytes! It is most likely corrupt. Claimed size: %d bytes" + % payload_len + ) + + return TuyaHeader(prefix, seqno, cmd, payload_len) + + +class AESCipher: + """Cipher module for Tuya communication.""" + + def __init__(self, key): + """Initialize a new AESCipher.""" + self.block_size = 16 + self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend()) + + def encrypt(self, raw, use_base64=True, pad=True): + """Encrypt data to be sent to device.""" + encryptor = self.cipher.encryptor() + if pad: + raw = self._pad(raw) + crypted_text = encryptor.update(raw) + encryptor.finalize() + return base64.b64encode(crypted_text) if use_base64 else crypted_text + + def decrypt(self, enc, use_base64=True, decode_text=True): + """Decrypt data from device.""" + if use_base64: + enc = base64.b64decode(enc) + + decryptor = self.cipher.decryptor() + raw = self._unpad(decryptor.update(enc) + decryptor.finalize()) + return raw.decode("utf-8") if decode_text else raw + + def _pad(self, data): + padnum = self.block_size - len(data) % self.block_size + return data + padnum * chr(padnum).encode() + + @staticmethod + def _unpad(data): + return data[: -ord(data[len(data) - 1 :])] + + +class MessageDispatcher(ContextualLogger): + """Buffer and dispatcher for Tuya messages.""" + + # Heartbeats on protocols < 3.3 respond with sequence number 0, + # so they can't be waited for like other messages. + # This is a hack to allow waiting for heartbeats. + HEARTBEAT_SEQNO = -100 + RESET_SEQNO = -101 + SESS_KEY_SEQNO = -102 + + def __init__(self, dev_id, listener, protocol_version, local_key, enable_debug): + """Initialize a new MessageBuffer.""" + super().__init__() + self.buffer = b"" + self.listeners = {} + self.listener = listener + self.version = protocol_version + self.local_key = local_key + self.set_logger(_LOGGER, dev_id, enable_debug) + + def abort(self): + """Abort all waiting clients.""" + for key in self.listeners: + sem = self.listeners[key] + self.listeners[key] = None + + # TODO: Received data and semahore should be stored separately + if isinstance(sem, asyncio.Semaphore): + sem.release() + + async def wait_for(self, seqno, cmd, timeout=5): + """Wait for response to a sequence number to be received and return it.""" + if seqno in self.listeners: + raise Exception(f"listener exists for {seqno}") + + self.debug("Command %d waiting for seq. number %d", cmd, seqno) + self.listeners[seqno] = asyncio.Semaphore(0) + try: + await asyncio.wait_for(self.listeners[seqno].acquire(), timeout=timeout) + except asyncio.TimeoutError: + self.debug( + "Command %d timed out waiting for sequence number %d", cmd, seqno + ) + del self.listeners[seqno] + raise + + return self.listeners.pop(seqno) + + def add_data(self, data): + """Add new data to the buffer and try to parse messages.""" + self.buffer += data + header_len = struct.calcsize(MESSAGE_RECV_HEADER_FMT) + + while self.buffer: + # Check if enough data for measage header + if len(self.buffer) < header_len: + break + + header = parse_header(self.buffer) + hmac_key = self.local_key if self.version == 3.4 else None + msg = unpack_message( + self.buffer, header=header, hmac_key=hmac_key, logger=self + ) + self.buffer = self.buffer[header_len - 4 + header.length :] + self._dispatch(msg) + + def _dispatch(self, msg): + """Dispatch a message to someone that is listening.""" + self.debug("Dispatching message CMD %r %s", msg.cmd, msg) + if msg.seqno in self.listeners: + # self.debug("Dispatching sequence number %d", msg.seqno) + sem = self.listeners[msg.seqno] + if isinstance(sem, asyncio.Semaphore): + self.listeners[msg.seqno] = msg + sem.release() + else: + self.debug("Got additional message without request - skipping: %s", sem) + elif msg.cmd == HEART_BEAT: + self.debug("Got heartbeat response") + if self.HEARTBEAT_SEQNO in self.listeners: + sem = self.listeners[self.HEARTBEAT_SEQNO] + self.listeners[self.HEARTBEAT_SEQNO] = msg + sem.release() + elif msg.cmd == UPDATEDPS: + self.debug("Got normal updatedps response") + if self.RESET_SEQNO in self.listeners: + sem = self.listeners[self.RESET_SEQNO] + self.listeners[self.RESET_SEQNO] = msg + sem.release() + elif msg.cmd == SESS_KEY_NEG_RESP: + self.debug("Got key negotiation response") + if self.SESS_KEY_SEQNO in self.listeners: + sem = self.listeners[self.SESS_KEY_SEQNO] + self.listeners[self.SESS_KEY_SEQNO] = msg + sem.release() + elif msg.cmd == STATUS: + if self.RESET_SEQNO in self.listeners: + self.debug("Got reset status update") + sem = self.listeners[self.RESET_SEQNO] + self.listeners[self.RESET_SEQNO] = msg + sem.release() + else: + self.debug("Got status update") + self.listener(msg) + else: + if msg.cmd == CONTROL_NEW: + self.debug("Got ACK message for command %d: will ignore it", msg.cmd) + else: + self.debug( + "Got message type %d for unknown listener %d: %s", + msg.cmd, + msg.seqno, + msg, + ) + + +class TuyaListener(ABC): + """Listener interface for Tuya device changes.""" + + @abstractmethod + def status_updated(self, status): + """Device updated status.""" + + @abstractmethod + def disconnected(self): + """Device disconnected.""" + + +class EmptyListener(TuyaListener): + """Listener doing nothing.""" + + def status_updated(self, status): + """Device updated status.""" + + def disconnected(self): + """Device disconnected.""" + + +class TuyaProtocol(asyncio.Protocol, ContextualLogger): + """Implementation of the Tuya protocol.""" + + def __init__( + self, dev_id, local_key, protocol_version, enable_debug, on_connected, listener + ): + """ + Initialize a new TuyaInterface. + + Args: + dev_id (str): The device id. + address (str): The network address. + local_key (str, optional): The encryption key. Defaults to None. + + Attributes: + port (int): The port to connect to. + """ + super().__init__() + self.loop = asyncio.get_running_loop() + self.set_logger(_LOGGER, dev_id, enable_debug) + self.id = dev_id + self.local_key = local_key.encode("latin1") + self.real_local_key = self.local_key + self.dev_type = "type_0a" + self.dps_to_request = {} + + if protocol_version: + self.set_version(float(protocol_version)) + else: + # make sure we call our set_version() and not a subclass since some of + # them (such as BulbDevice) make connections when called + TuyaProtocol.set_version(self, 3.1) + + self.cipher = AESCipher(self.local_key) + self.seqno = 1 + self.transport = None + self.listener = weakref.ref(listener) + self.dispatcher = self._setup_dispatcher(enable_debug) + self.on_connected = on_connected + self.heartbeater = None + self.dps_cache = {} + self.local_nonce = b"0123456789abcdef" # not-so-random random key + self.remote_nonce = b"" + + def set_version(self, protocol_version): + """Set the device version and eventually start available DPs detection.""" + self.version = protocol_version + self.version_bytes = str(protocol_version).encode("latin1") + self.version_header = self.version_bytes + PROTOCOL_3x_HEADER + if protocol_version == 3.2: # 3.2 behaves like 3.3 with type_0d + # self.version = 3.3 + self.dev_type = "type_0d" + elif protocol_version == 3.4: + self.dev_type = "v3.4" + + def error_json(self, number=None, payload=None): + """Return error details in JSON.""" + try: + spayload = json.dumps(payload) + # spayload = payload.replace('\"','').replace('\'','') + except Exception: + spayload = '""' + + vals = (error_codes[number], str(number), spayload) + self.debug("ERROR %s - %s - payload: %s", *vals) + + return json.loads('{ "Error":"%s", "Err":"%s", "Payload":%s }' % vals) + + def _setup_dispatcher(self, enable_debug): + def _status_update(msg): + if msg.seqno > 0: + self.seqno = msg.seqno + 1 + decoded_message = self._decode_payload(msg.payload) + if "dps" in decoded_message: + self.dps_cache.update(decoded_message["dps"]) + + listener = self.listener and self.listener() + if listener is not None: + listener.status_updated(self.dps_cache) + + return MessageDispatcher( + self.id, _status_update, self.version, self.local_key, enable_debug + ) + + def connection_made(self, transport): + """Did connect to the device.""" + self.transport = transport + self.on_connected.set_result(True) + + def start_heartbeat(self): + """Start the heartbeat transmissions with the device.""" + + async def heartbeat_loop(): + """Continuously send heart beat updates.""" + self.debug("Started heartbeat loop") + while True: + try: + await self.heartbeat() + await asyncio.sleep(HEARTBEAT_INTERVAL) + except asyncio.CancelledError: + self.debug("Stopped heartbeat loop") + raise + except asyncio.TimeoutError: + self.debug("Heartbeat failed due to timeout, disconnecting") + break + except Exception as ex: # pylint: disable=broad-except + self.exception("Heartbeat failed (%s), disconnecting", ex) + break + + transport = self.transport + self.transport = None + transport.close() + + self.heartbeater = self.loop.create_task(heartbeat_loop()) + + def data_received(self, data): + """Received data from device.""" + # self.debug("received data=%r", binascii.hexlify(data)) + self.dispatcher.add_data(data) + + def connection_lost(self, exc): + """Disconnected from device.""" + self.debug("Connection lost: %s", exc) + self.real_local_key = self.local_key + try: + listener = self.listener and self.listener() + if listener is not None: + listener.disconnected() + except Exception: # pylint: disable=broad-except + self.exception("Failed to call disconnected callback") + + async def close(self): + """Close connection and abort all outstanding listeners.""" + self.debug("Closing connection") + self.real_local_key = self.local_key + if self.heartbeater is not None: + self.heartbeater.cancel() + try: + await self.heartbeater + except asyncio.CancelledError: + pass + self.heartbeater = None + if self.dispatcher is not None: + self.dispatcher.abort() + self.dispatcher = None + if self.transport is not None: + transport = self.transport + self.transport = None + transport.close() + + async def exchange_quick(self, payload, recv_retries): + """Similar to exchange() but never retries sending and does not decode the response.""" + if not self.transport: + self.debug( + "[" + self.id + "] send quick failed, could not get socket: %s", payload + ) + return None + enc_payload = ( + self._encode_message(payload) + if isinstance(payload, MessagePayload) + else payload + ) + # self.debug("Quick-dispatching message %s, seqno %s", binascii.hexlify(enc_payload), self.seqno) + + try: + self.transport.write(enc_payload) + except Exception: + # self._check_socket_close(True) + self.close() + return None + while recv_retries: + try: + seqno = MessageDispatcher.SESS_KEY_SEQNO + msg = await self.dispatcher.wait_for(seqno, payload.cmd) + # for 3.4 devices, we get the starting seqno with the SESS_KEY_NEG_RESP message + self.seqno = msg.seqno + except Exception: + msg = None + if msg and len(msg.payload) != 0: + return msg + recv_retries -= 1 + if recv_retries == 0: + self.debug( + "received null payload (%r) but out of recv retries, giving up", msg + ) + else: + self.debug( + "received null payload (%r), fetch new one - %s retries remaining", + msg, + recv_retries, + ) + return None + + async def exchange(self, command, dps=None): + """Send and receive a message, returning response from device.""" + if self.version == 3.4 and self.real_local_key == self.local_key: + self.debug("3.4 device: negotiating a new session key") + await self._negotiate_session_key() + + self.debug( + "Sending command %s (device type: %s)", + command, + self.dev_type, + ) + payload = self._generate_payload(command, dps) + real_cmd = payload.cmd + dev_type = self.dev_type + # self.debug("Exchange: payload %r %r", payload.cmd, payload.payload) + + # Wait for special sequence number if heartbeat or reset + seqno = self.seqno + + if payload.cmd == HEART_BEAT: + seqno = MessageDispatcher.HEARTBEAT_SEQNO + elif payload.cmd == UPDATEDPS: + seqno = MessageDispatcher.RESET_SEQNO + + enc_payload = self._encode_message(payload) + self.transport.write(enc_payload) + msg = await self.dispatcher.wait_for(seqno, payload.cmd) + if msg is None: + self.debug("Wait was aborted for seqno %d", seqno) + return None + + # TODO: Verify stuff, e.g. CRC sequence number? + if real_cmd in [HEART_BEAT, CONTROL, CONTROL_NEW] and len(msg.payload) == 0: + # device may send messages with empty payload in response + # to a HEART_BEAT or CONTROL or CONTROL_NEW command: consider them an ACK + self.debug("ACK received for command %d: ignoring it", real_cmd) + return None + payload = self._decode_payload(msg.payload) + + # Perform a new exchange (once) if we switched device type + if dev_type != self.dev_type: + self.debug( + "Re-send %s due to device type change (%s -> %s)", + command, + dev_type, + self.dev_type, + ) + return await self.exchange(command, dps) + return payload + + async def status(self): + """Return device status.""" + status = await self.exchange(DP_QUERY) + if status and "dps" in status: + self.dps_cache.update(status["dps"]) + return self.dps_cache + + async def heartbeat(self): + """Send a heartbeat message.""" + return await self.exchange(HEART_BEAT) + + async def reset(self, dpIds=None): + """Send a reset message (3.3 only).""" + if self.version == 3.3: + self.dev_type = "type_0a" + self.debug("reset switching to dev_type %s", self.dev_type) + return await self.exchange(UPDATEDPS, dpIds) + + return True + + async def update_dps(self, dps=None): + """ + Request device to update index. + + Args: + dps([int]): list of dps to update, default=detected&whitelisted + """ + if self.version in [3.2, 3.3, 3.4]: # 3.2 behaves like 3.3 with type_0d + if dps is None: + if not self.dps_cache: + await self.detect_available_dps() + if self.dps_cache: + dps = [int(dp) for dp in self.dps_cache] + # filter non whitelisted dps + dps = list(set(dps).intersection(set(UPDATE_DPS_WHITELIST))) + self.debug("updatedps() entry (dps %s, dps_cache %s)", dps, self.dps_cache) + payload = self._generate_payload(UPDATEDPS, dps) + enc_payload = self._encode_message(payload) + self.transport.write(enc_payload) + return True + + async def set_dp(self, value, dp_index): + """ + Set value (may be any type: bool, int or string) of any dps index. + + Args: + dp_index(int): dps index to set + value: new value for the dps index + """ + return await self.exchange(CONTROL, {str(dp_index): value}) + + async def set_dps(self, dps): + """Set values for a set of datapoints.""" + return await self.exchange(CONTROL, dps) + + async def detect_available_dps(self): + """Return which datapoints are supported by the device.""" + # type_0d devices need a sort of bruteforce querying in order to detect the + # list of available dps experience shows that the dps available are usually + # in the ranges [1-25] and [100-110] need to split the bruteforcing in + # different steps due to request payload limitation (max. length = 255) + self.dps_cache = {} + ranges = [(2, 11), (11, 21), (21, 31), (100, 111)] + + for dps_range in ranges: + # dps 1 must always be sent, otherwise it might fail in case no dps is found + # in the requested range + self.dps_to_request = {"1": None} + self.add_dps_to_request(range(*dps_range)) + try: + data = await self.status() + except Exception as ex: + self.exception("Failed to get status: %s", ex) + raise + if "dps" in data: + self.dps_cache.update(data["dps"]) + + if self.dev_type == "type_0a": + return self.dps_cache + self.debug("Detected dps: %s", self.dps_cache) + return self.dps_cache + + def add_dps_to_request(self, dp_indicies): + """Add a datapoint (DP) to be included in requests.""" + if isinstance(dp_indicies, int): + self.dps_to_request[str(dp_indicies)] = None + else: + self.dps_to_request.update({str(index): None for index in dp_indicies}) + + def _decode_payload(self, payload): + cipher = AESCipher(self.local_key) + + if self.version == 3.4: + # 3.4 devices encrypt the version header in addition to the payload + try: + # self.debug("decrypting=%r", payload) + payload = cipher.decrypt(payload, False, decode_text=False) + except Exception as ex: + self.debug( + "incomplete payload=%r with len:%d (%s)", payload, len(payload), ex + ) + return self.error_json(ERR_PAYLOAD) + + # self.debug("decrypted 3.x payload=%r", payload) + + if payload.startswith(PROTOCOL_VERSION_BYTES_31): + # Received an encrypted payload + # Remove version header + payload = payload[len(PROTOCOL_VERSION_BYTES_31) :] + # Decrypt payload + # Remove 16-bytes of MD5 hexdigest of payload + payload = cipher.decrypt(payload[16:]) + elif self.version >= 3.2: # 3.2 or 3.3 or 3.4 + # Trim header for non-default device type + if payload.startswith(self.version_bytes): + payload = payload[len(self.version_header) :] + # self.debug("removing 3.x=%r", payload) + elif self.dev_type == "type_0d" and (len(payload) & 0x0F) != 0: + payload = payload[len(self.version_header) :] + # self.debug("removing type_0d 3.x header=%r", payload) + + if self.version != 3.4: + try: + # self.debug("decrypting=%r", payload) + payload = cipher.decrypt(payload, False) + except Exception as ex: + self.debug( + "incomplete payload=%r with len:%d (%s)", + payload, + len(payload), + ex, + ) + return self.error_json(ERR_PAYLOAD) + + # self.debug("decrypted 3.x payload=%r", payload) + # Try to detect if type_0d found + + if not isinstance(payload, str): + try: + payload = payload.decode() + except Exception as ex: + self.debug("payload was not string type and decoding failed") + raise DecodeError("payload was not a string: %s" % ex) + # return self.error_json(ERR_JSON, payload) + + if "data unvalid" in payload: + self.dev_type = "type_0d" + self.debug( + "'data unvalid' error detected: switching to dev_type %r", + self.dev_type, + ) + return None + elif not payload.startswith(b"{"): + self.debug("Unexpected payload=%r", payload) + return self.error_json(ERR_PAYLOAD, payload) + + if not isinstance(payload, str): + payload = payload.decode() + self.debug("Deciphered data = %r", payload) + try: + json_payload = json.loads(payload) + except Exception as ex: + raise DecodeError( + "could not decrypt data: wrong local_key? (exception: %s)" % ex + ) + # json_payload = self.error_json(ERR_JSON, payload) + + # v3.4 stuffs it into {"data":{"dps":{"1":true}}, ...} + if ( + "dps" not in json_payload + and "data" in json_payload + and "dps" in json_payload["data"] + ): + json_payload["dps"] = json_payload["data"]["dps"] + + return json_payload + + async def _negotiate_session_key(self): + self.local_key = self.real_local_key + + rkey = await self.exchange_quick( + MessagePayload(SESS_KEY_NEG_START, self.local_nonce), 2 + ) + if not rkey or not isinstance(rkey, TuyaMessage) or len(rkey.payload) < 48: + # error + self.debug("session key negotiation failed on step 1") + return False + + if rkey.cmd != SESS_KEY_NEG_RESP: + self.debug( + "session key negotiation step 2 returned wrong command: %d", rkey.cmd + ) + return False + + payload = rkey.payload + try: + # self.debug("decrypting %r using %r", payload, self.real_local_key) + cipher = AESCipher(self.real_local_key) + payload = cipher.decrypt(payload, False, decode_text=False) + except Exception as ex: + self.debug( + "session key step 2 decrypt failed, payload=%r with len:%d (%s)", + payload, + len(payload), + ex, + ) + return False + + self.debug("decrypted session key negotiation step 2: payload=%r", payload) + + if len(payload) < 48: + self.debug("session key negotiation step 2 failed, too short response") + return False + + self.remote_nonce = payload[:16] + hmac_check = hmac.new(self.local_key, self.local_nonce, sha256).digest() + + if hmac_check != payload[16:48]: + self.debug( + "session key negotiation step 2 failed HMAC check! wanted=%r but got=%r", + binascii.hexlify(hmac_check), + binascii.hexlify(payload[16:48]), + ) + + # self.debug("session local nonce: %r remote nonce: %r", self.local_nonce, self.remote_nonce) + rkey_hmac = hmac.new(self.local_key, self.remote_nonce, sha256).digest() + await self.exchange_quick(MessagePayload(SESS_KEY_NEG_FINISH, rkey_hmac), None) + + self.local_key = bytes( + [a ^ b for (a, b) in zip(self.local_nonce, self.remote_nonce)] + ) + # self.debug("Session nonce XOR'd: %r" % self.local_key) + + cipher = AESCipher(self.real_local_key) + self.local_key = self.dispatcher.local_key = cipher.encrypt( + self.local_key, False, pad=False + ) + self.debug("Session key negotiate success! session key: %r", self.local_key) + return True + + # adds protocol header (if needed) and encrypts + def _encode_message(self, msg): + hmac_key = None + payload = msg.payload + self.cipher = AESCipher(self.local_key) + if self.version == 3.4: + hmac_key = self.local_key + if msg.cmd not in NO_PROTOCOL_HEADER_CMDS: + # add the 3.x header + payload = self.version_header + payload + self.debug("final payload for cmd %r: %r", msg.cmd, payload) + payload = self.cipher.encrypt(payload, False) + elif self.version >= 3.2: + # expect to connect and then disconnect to set new + payload = self.cipher.encrypt(payload, False) + if msg.cmd not in NO_PROTOCOL_HEADER_CMDS: + # add the 3.x header + payload = self.version_header + payload + elif msg.cmd == CONTROL: + # need to encrypt + payload = self.cipher.encrypt(payload) + preMd5String = ( + b"data=" + + payload + + b"||lpv=" + + PROTOCOL_VERSION_BYTES_31 + + b"||" + + self.local_key + ) + m = md5() + m.update(preMd5String) + hexdigest = m.hexdigest() + # some tuya libraries strip 8: to :24 + payload = ( + PROTOCOL_VERSION_BYTES_31 + + hexdigest[8:][:16].encode("latin1") + + payload + ) + + self.cipher = None + msg = TuyaMessage(self.seqno, msg.cmd, 0, payload, 0, True) + self.seqno += 1 # increase message sequence number + buffer = pack_message(msg, hmac_key=hmac_key) + # self.debug("payload encrypted with key %r => %r", self.local_key, binascii.hexlify(buffer)) + return buffer + + def _generate_payload(self, command, data=None, gwId=None, devId=None, uid=None): + """ + Generate the payload to send. + + Args: + command(str): The type of command. + This is one of the entries from payload_dict + data(dict, optional): The data to be send. + This is what will be passed via the 'dps' entry + gwId(str, optional): Will be used for gwId + devId(str, optional): Will be used for devId + uid(str, optional): Will be used for uid + """ + json_data = command_override = None + + if command in payload_dict[self.dev_type]: + if "command" in payload_dict[self.dev_type][command]: + json_data = payload_dict[self.dev_type][command]["command"] + if "command_override" in payload_dict[self.dev_type][command]: + command_override = payload_dict[self.dev_type][command][ + "command_override" + ] + + if self.dev_type != "type_0a": + if ( + json_data is None + and command in payload_dict["type_0a"] + and "command" in payload_dict["type_0a"][command] + ): + json_data = payload_dict["type_0a"][command]["command"] + if ( + command_override is None + and command in payload_dict["type_0a"] + and "command_override" in payload_dict["type_0a"][command] + ): + command_override = payload_dict["type_0a"][command]["command_override"] + + if command_override is None: + command_override = command + if json_data is None: + # I have yet to see a device complain about included but unneeded attribs, but they *will* + # complain about missing attribs, so just include them all unless otherwise specified + json_data = {"gwId": "", "devId": "", "uid": "", "t": ""} + + if "gwId" in json_data: + if gwId is not None: + json_data["gwId"] = gwId + else: + json_data["gwId"] = self.id + if "devId" in json_data: + if devId is not None: + json_data["devId"] = devId + else: + json_data["devId"] = self.id + if "uid" in json_data: + if uid is not None: + json_data["uid"] = uid + else: + json_data["uid"] = self.id + if "t" in json_data: + if json_data["t"] == "int": + json_data["t"] = int(time.time()) + else: + json_data["t"] = str(int(time.time())) + + if data is not None: + if "dpId" in json_data: + json_data["dpId"] = data + elif "data" in json_data: + json_data["data"] = {"dps": data} + else: + json_data["dps"] = data + elif self.dev_type == "type_0d" and command == DP_QUERY: + json_data["dps"] = self.dps_to_request + + if json_data == "": + payload = "" + else: + payload = json.dumps(json_data) + # if spaces are not removed device does not respond! + payload = payload.replace(" ", "").encode("utf-8") + self.debug("Sending payload: %s", payload) + + return MessagePayload(command_override, payload) + + def __repr__(self): + """Return internal string representation of object.""" + return self.id + + +async def connect( + address, + device_id, + local_key, + protocol_version, + enable_debug, + listener=None, + port=6668, + timeout=5, +): + """Connect to a device.""" + loop = asyncio.get_running_loop() + on_connected = loop.create_future() + _, protocol = await loop.create_connection( + lambda: TuyaProtocol( + device_id, + local_key, + protocol_version, + enable_debug, + on_connected, + listener or EmptyListener(), + ), + address, + port, + ) + + await asyncio.wait_for(on_connected, timeout=timeout) + return protocol diff --git a/homeassistant/config/custom_components/localtuya/select.py b/homeassistant/config/custom_components/localtuya/select.py new file mode 100644 index 0000000..c9b1d1c --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/select.py @@ -0,0 +1,123 @@ +"""Platform to present any Tuya DP as an enumeration.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_DEFAULT_VALUE, + CONF_OPTIONS, + CONF_OPTIONS_FRIENDLY, + CONF_PASSIVE_ENTITY, + CONF_RESTORE_ON_RECONNECT, +) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Required(CONF_OPTIONS): str, + vol.Optional(CONF_OPTIONS_FRIENDLY): str, + vol.Required(CONF_RESTORE_ON_RECONNECT): bool, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, + } + + +_LOGGER = logging.getLogger(__name__) + + +class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): + """Representation of a Tuya Enumeration.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + self._state_friendly = "" + self._valid_options = self._config.get(CONF_OPTIONS).split(";") + + # Set Display options + self._display_options = [] + display_options_str = "" + if CONF_OPTIONS_FRIENDLY in self._config: + display_options_str = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: %s", display_options_str) + + if display_options_str.find(";") >= 0: + self._display_options = display_options_str.split(";") + elif len(display_options_str.strip()) > 0: + self._display_options.append(display_options_str) + else: + # Default display string to raw string + _LOGGER.debug("No Display options configured - defaulting to raw values") + self._display_options = self._valid_options + + _LOGGER.debug( + "Total Raw Options: %s - Total Display Options: %s", + str(len(self._valid_options)), + str(len(self._display_options)), + ) + if len(self._valid_options) > len(self._display_options): + # If list of display items smaller than list of valid items, + # then default remaining items to be the raw value + _LOGGER.debug( + "Valid options is larger than display options - \ + filling up with raw values" + ) + for i in range(len(self._display_options), len(self._valid_options)): + self._display_options.append(self._valid_options[i]) + + @property + def current_option(self) -> str: + """Return the current value.""" + return self._state_friendly + + @property + def options(self) -> list: + """Return the list of values.""" + return self._display_options + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + option_value = self._valid_options[self._display_options.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + option_value) + await self._device.set_dp(option_value, self._dp_id) + + def status_updated(self): + """Device status was updated.""" + super().status_updated() + + state = self.dps(self._dp_id) + + # Check that received status update for this entity. + if state is not None: + try: + self._state_friendly = self._display_options[ + self._valid_options.index(state) + ] + except Exception: # pylint: disable=broad-except + # Friendly value couldn't be mapped + self._state_friendly = state + + # Default value is the first option + def entity_default_value(self): + """Return the first option as the default value for this entity type.""" + return self._valid_options[0] + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/sensor.py b/homeassistant/config/custom_components/localtuya/sensor.py new file mode 100644 index 0000000..0eb0ae4 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/sensor.py @@ -0,0 +1,75 @@ +"""Platform to present any Tuya DP as a sensor.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import CONF_SCALING + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PRECISION = 2 + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_DEVICE_CLASS): vol.In(DEVICE_CLASSES), + vol.Optional(CONF_SCALING): vol.All( + vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0) + ), + } + + +class LocaltuyaSensor(LocalTuyaEntity): + """Representation of a Tuya sensor.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + + @property + def state(self): + """Return sensor state.""" + return self._state + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + scale_factor = self._config.get(CONF_SCALING) + if scale_factor is not None and isinstance(state, (int, float)): + state = round(state * scale_factor, DEFAULT_PRECISION) + self._state = state + + # No need to restore state for a sensor + async def restore_state_when_connected(self): + """Do nothing for a sensor.""" + return + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSensor, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/services.yaml b/homeassistant/config/custom_components/localtuya/services.yaml new file mode 100644 index 0000000..f10af4a --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/services.yaml @@ -0,0 +1,15 @@ +reload: + description: Reload localtuya and reconnect to all devices. + +set_dp: + description: Change the value of a datapoint (DP) + fields: + device_id: + description: Device ID of device to change datapoint value for + example: 11100118278aab4de001 + dp: + description: Datapoint index + example: 1 + value: + description: New value to set + example: False diff --git a/homeassistant/config/custom_components/localtuya/strings.json b/homeassistant/config/custom_components/localtuya/strings.json new file mode 100644 index 0000000..32f6040 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/strings.json @@ -0,0 +1,139 @@ +{ + "config": { + "abort": { + "already_configured": "Device has already been configured.", + "unsupported_device_type": "Unsupported device type!" + }, + "error": { + "cannot_connect": "Cannot connect to device. Verify that address is correct.", + "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", + "unknown": "An unknown error occurred. See log for details.", + "switch_already_configured": "Switch with this ID has already been configured." + }, + "step": { + "user": { + "title": "Main Configuration", + "description": "Input the credentials for Tuya Cloud API.", + "data": { + "region": "API server region", + "client_id": "Client ID", + "client_secret": "Secret", + "user_id": "User ID" + } + }, + "power_outlet": { + "title": "Add subswitch", + "description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.", + "data": { + "id": "ID", + "name": "Name", + "friendly_name": "Friendly name", + "current": "Current", + "current_consumption": "Current Consumption", + "voltage": "Voltage", + "add_another_switch": "Add another switch" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "LocalTuya Configuration", + "description": "Please select the desired actionSSSS.", + "data": { + "add_device": "Add a new device", + "edit_device": "Edit a device", + "delete_device": "Delete a device", + "setup_cloud": "Reconfigure Cloud API account" + } + }, + "entity": { + "title": "Entity Configuration", + "description": "Editing entity with DPS `{id}` and platform `{platform}`.", + "data": { + "id": "ID", + "friendly_name": "Friendly name", + "current": "Current", + "current_consumption": "Current Consumption", + "voltage": "Voltage", + "commands_set": "Open_Close_Stop Commands Set", + "positioning_mode": "Positioning mode", + "current_position_dp": "Current Position (for *position* mode only)", + "set_position_dp": "Set Position (for *position* mode only)", + "position_inverted": "Invert 0-100 position (for *position* mode only)", + "span_time": "Full opening time, in secs. (for *timed* mode only)", + "unit_of_measurement": "Unit of Measurement", + "device_class": "Device Class", + "scaling": "Scaling Factor", + "state_on": "On Value", + "state_off": "Off Value", + "powergo_dp": "Power DP (Usually 25 or 2)", + "idle_status_value": "Idle Status (comma-separated)", + "returning_status_value": "Returning Status", + "docked_status_value": "Docked Status (comma-separated)", + "fault_dp": "Fault DP (Usually 11)", + "battery_dp": "Battery status DP (Usually 14)", + "mode_dp": "Mode DP (Usually 27)", + "modes": "Modes list", + "return_mode": "Return home mode", + "fan_speed_dp": "Fan speeds DP (Usually 30)", + "fan_speeds": "Fan speeds list (comma-separated)", + "clean_time_dp": "Clean Time DP (Usually 33)", + "clean_area_dp": "Clean Area DP (Usually 32)", + "clean_record_dp": "Clean Record DP (Usually 34)", + "locate_dp": "Locate DP (Usually 31)", + "paused_state": "Pause state (pause, paused, etc)", + "stop_status": "Stop status", + "brightness": "Brightness (only for white color)", + "brightness_lower": "Brightness Lower Value", + "brightness_upper": "Brightness Upper Value", + "color_temp": "Color Temperature", + "color_temp_reverse": "Color Temperature Reverse", + "color": "Color", + "color_mode": "Color Mode", + "color_temp_min_kelvin": "Minimum Color Temperature in K", + "color_temp_max_kelvin": "Maximum Color Temperature in K", + "music_mode": "Music mode available", + "scene": "Scene", + "fan_speed_control": "Fan Speed Control dps", + "fan_oscillating_control": "Fan Oscillating Control dps", + "fan_speed_min": "minimum fan speed integer", + "fan_speed_max": "maximum fan speed integer", + "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", + "fan_direction": "fan direction dps", + "fan_direction_forward": "forward dps string", + "fan_direction_reverse": "reverse dps string", + "fan_dps_type": "DP value type", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature (optional)", + "min_temperature_dp": "Min Temperature (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "heuristic_action": "Enable heuristic action (optional)", + "dps_default_value": "Default value when un-initialised (optional)", + "restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection", + "min_value": "Minimum Value", + "max_value": "Maximum Value", + "step_size": "Minimum increment between numbers" + } + }, + "yaml_import": { + "title": "Not Supported", + "description": "Options cannot be edited when configured via YAML." + } + } + }, + "title": "LocalTuya" +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/localtuya/switch.py b/homeassistant/config/custom_components/localtuya/switch.py new file mode 100644 index 0000000..3776836 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/switch.py @@ -0,0 +1,91 @@ +"""Platform to locally control Tuya-based switch devices.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.switch import DOMAIN, SwitchEntity + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + ATTR_CURRENT, + ATTR_CURRENT_CONSUMPTION, + ATTR_STATE, + ATTR_VOLTAGE, + CONF_CURRENT, + CONF_CURRENT_CONSUMPTION, + CONF_DEFAULT_VALUE, + CONF_PASSIVE_ENTITY, + CONF_RESTORE_ON_RECONNECT, + CONF_VOLTAGE, +) + +_LOGGER = logging.getLogger(__name__) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_CURRENT): vol.In(dps), + vol.Optional(CONF_CURRENT_CONSUMPTION): vol.In(dps), + vol.Optional(CONF_VOLTAGE): vol.In(dps), + vol.Required(CONF_RESTORE_ON_RECONNECT): bool, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, + } + + +class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): + """Representation of a Tuya switch.""" + + def __init__( + self, + device, + config_entry, + switchid, + **kwargs, + ): + """Initialize the Tuya switch.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + self._state = None + _LOGGER.debug("Initialized switch [%s]", self.name) + + @property + def is_on(self): + """Check if Tuya switch is on.""" + return self._state + + @property + def extra_state_attributes(self): + """Return device state attributes.""" + attrs = {} + if self.has_config(CONF_CURRENT): + attrs[ATTR_CURRENT] = self.dps(self._config[CONF_CURRENT]) + if self.has_config(CONF_CURRENT_CONSUMPTION): + attrs[ATTR_CURRENT_CONSUMPTION] = ( + self.dps(self._config[CONF_CURRENT_CONSUMPTION]) / 10 + ) + if self.has_config(CONF_VOLTAGE): + attrs[ATTR_VOLTAGE] = self.dps(self._config[CONF_VOLTAGE]) / 10 + + # Store the state + if self._state is not None: + attrs[ATTR_STATE] = self._state + elif self._last_state is not None: + attrs[ATTR_STATE] = self._last_state + return attrs + + async def async_turn_on(self, **kwargs): + """Turn Tuya switch on.""" + await self._device.set_dp(True, self._dp_id) + + async def async_turn_off(self, **kwargs): + """Turn Tuya switch off.""" + await self._device.set_dp(False, self._dp_id) + + # Default value is the "OFF" state + def entity_default_value(self): + """Return False as the default value for this entity type.""" + return False + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSwitch, flow_schema) diff --git a/homeassistant/config/custom_components/localtuya/translations/en.json b/homeassistant/config/custom_components/localtuya/translations/en.json new file mode 100644 index 0000000..8fdbb60 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/translations/en.json @@ -0,0 +1,238 @@ +{ + "config": { + "abort": { + "already_configured": "Device has already been configured.", + "device_updated": "Device configuration has been updated!" + }, + "error": { + "authentication_failed": "Failed to authenticate.\n{msg}", + "cannot_connect": "Cannot connect to device. Verify that address is correct and try again.", + "device_list_failed": "Failed to retrieve device list.\n{msg}", + "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", + "unknown": "An unknown error occurred. See log for details.", + "entity_already_configured": "Entity with this ID has already been configured.", + "address_in_use": "Address used for discovery is already in use. Make sure no other application is using it (TCP port 6668).", + "discovery_failed": "Something failed when discovering devices. See log for details.", + "empty_dps": "Connection to device succeeded but no datapoints found, please try again. Create a new issue and include debug logs if problem persists." + }, + "step": { + "user": { + "title": "Cloud API account configuration", + "description": "Input the credentials for Tuya Cloud API.", + "data": { + "region": "API server region", + "client_id": "Client ID", + "client_secret": "Secret", + "user_id": "User ID", + "user_name": "Username", + "no_cloud": "Do not configure a Cloud API account" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Device has already been configured.", + "device_success": "Device {dev_name} successfully {action}.", + "no_entities": "Cannot remove all entities from a device.\nIf you want to delete a device, enter it in the Devices menu, click the 3 dots in the 'Device info' frame, and press the Delete button." + }, + "error": { + "authentication_failed": "Failed to authenticate.\n{msg}", + "cannot_connect": "Cannot connect to device. Verify that address is correct and try again.", + "device_list_failed": "Failed to retrieve device list.\n{msg}", + "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", + "unknown": "An unknown error occurred. See log for details.", + "entity_already_configured": "Entity with this ID has already been configured.", + "address_in_use": "Address used for discovery is already in use. Make sure no other application is using it (TCP port 6668).", + "discovery_failed": "Something failed when discovering devices. See log for details.", + "empty_dps": "Connection to device succeeded but no datapoints found, please try again. Create a new issue and include debug logs if problem persists." + }, + "step": { + "yaml_import": { + "title": "Not Supported", + "description": "Options cannot be edited when configured via YAML." + }, + "init": { + "title": "LocalTuya Configuration", + "description": "Please select the desired action.", + "data": { + "add_device": "Add a new device", + "edit_device": "Edit a device", + "setup_cloud": "Reconfigure Cloud API account" + } + }, + "add_device": { + "title": "Add a new device", + "description": "Pick one of the automatically discovered devices or `...` to manually to add a device.", + "data": { + "selected_device": "Discovered Devices" + } + }, + "edit_device": { + "title": "Edit a new device", + "description": "Pick the configured device you wish to edit.", + "data": { + "selected_device": "Configured Devices", + "max_temperature_const": "Max Temperature Constant (optional)", + "min_temperature_const": "Min Temperature Constant (optional)", + "hvac_fan_mode_dp": "HVAC Fan Mode DP (optional)", + "hvac_fan_mode_set": "HVAC Fan Mode Set (optional)", + "hvac_swing_mode_dp": "HVAC Swing Mode DP (optional)", + "hvac_swing_mode_set": "HVAC Swing Mode Set (optional)" + } + }, + "cloud_setup": { + "title": "Cloud API account configuration", + "description": "Input the credentials for Tuya Cloud API.", + "data": { + "region": "API server region", + "client_id": "Client ID", + "client_secret": "Secret", + "user_id": "User ID", + "user_name": "Username", + "no_cloud": "Do not configure Cloud API account" + } + }, + "configure_device": { + "title": "Configure Tuya device", + "description": "Fill in the device details{for_device}.", + "data": { + "friendly_name": "Name", + "host": "Host", + "device_id": "Device ID", + "local_key": "Local key", + "protocol_version": "Protocol Version", + "enable_debug": "Enable debugging for this device (debug must be enabled also in configuration.yaml)", + "scan_interval": "Scan interval (seconds, only when not updating automatically)", + "entities": "Entities (uncheck an entity to remove it)", + "add_entities": "Add more entities in 'edit device' mode", + "manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (optional)", + "reset_dpids": "DPIDs to send in RESET command (separated by commas ',')- Used when device does not respond to status requests after turning on (optional)" + } + }, + "pick_entity_type": { + "title": "Entity type selection", + "description": "Please pick the type of entity you want to add.", + "data": { + "platform_to_add": "Platform", + "no_additional_entities": "Do not add any more entities" + } + }, + "configure_entity": { + "title": "Configure entity", + "description": "Please fill out the details for {entity} with type `{platform}`. All settings except for `ID` can be changed from the Options page later.", + "data": { + "id": "ID", + "friendly_name": "Friendly name", + "current": "Current", + "current_consumption": "Current Consumption", + "voltage": "Voltage", + "commands_set": "Open_Close_Stop Commands Set", + "positioning_mode": "Positioning mode", + "current_position_dp": "Current Position (for *position* mode only)", + "set_position_dp": "Set Position (for *position* mode only)", + "position_inverted": "Invert 0-100 position (for *position* mode only)", + "span_time": "Full opening time, in secs. (for *timed* mode only)", + "unit_of_measurement": "Unit of Measurement", + "device_class": "Device Class", + "scaling": "Scaling Factor", + "state_on": "On Value", + "state_off": "Off Value", + "powergo_dp": "Power DP (Usually 25 or 2)", + "idle_status_value": "Idle Status (comma-separated)", + "returning_status_value": "Returning Status", + "docked_status_value": "Docked Status (comma-separated)", + "fault_dp": "Fault DP (Usually 11)", + "battery_dp": "Battery status DP (Usually 14)", + "mode_dp": "Mode DP (Usually 27)", + "modes": "Modes list", + "return_mode": "Return home mode", + "fan_speed_dp": "Fan speeds DP (Usually 30)", + "fan_speeds": "Fan speeds list (comma-separated)", + "clean_time_dp": "Clean Time DP (Usually 33)", + "clean_area_dp": "Clean Area DP (Usually 32)", + "clean_record_dp": "Clean Record DP (Usually 34)", + "locate_dp": "Locate DP (Usually 31)", + "paused_state": "Pause state (pause, paused, etc)", + "stop_status": "Stop status", + "brightness": "Brightness (only for white color)", + "brightness_lower": "Brightness Lower Value", + "brightness_upper": "Brightness Upper Value", + "color_temp": "Color Temperature", + "color_temp_reverse": "Color Temperature Reverse", + "color": "Color", + "color_mode": "Color Mode", + "color_temp_min_kelvin": "Minimum Color Temperature in K", + "color_temp_max_kelvin": "Maximum Color Temperature in K", + "music_mode": "Music mode available", + "scene": "Scene", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;", + "fan_speed_control": "Fan Speed Control dps", + "fan_oscillating_control": "Fan Oscillating Control dps", + "fan_speed_min": "minimum fan speed integer", + "fan_speed_max": "maximum fan speed integer", + "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", + "fan_direction": "fan direction dps", + "fan_direction_forward": "forward dps string", + "fan_direction_reverse": "reverse dps string", + "fan_dps_type": "DP value type", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature DP (optional)", + "min_temperature_dp": "Min Temperature DP (optional)", + "max_temperature_const": "Max Temperature Constant (optional)", + "min_temperature_const": "Min Temperature Constant (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_fan_mode_dp": "HVAC Fan Mode DP (optional)", + "hvac_fan_mode_set": "HVAC Fan Mode Set (optional)", + "hvac_swing_mode_dp": "HVAC Swing Mode DP (optional)", + "hvac_swing_mode_set": "HVAC Swing Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "heuristic_action": "Enable heuristic action (optional)", + "dps_default_value": "Default value when un-initialised (optional)", + "restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection", + "min_value": "Minimum Value", + "max_value": "Maximum Value", + "step_size": "Minimum increment between numbers", + "is_passive_entity": "Passive entity - requires integration to send initialisation value" + } + } + } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reload localtuya and reconnect to all devices." + }, + "set_dp": { + "name": "Set datapoint", + "description": "Change the value of a datapoint (DP)", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Device ID of device to change datapoint value for" + }, + "dp": { + "name": "DP", + "description": "Datapoint index" + }, + "value": { + "name": "Value", + "description": "New value to set" + } + } + } + }, + "title": "LocalTuya" +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/localtuya/translations/it.json b/homeassistant/config/custom_components/localtuya/translations/it.json new file mode 100644 index 0000000..264bb97 --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/translations/it.json @@ -0,0 +1,216 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo è già stato configurato.", + "device_updated": "La configurazione del dispositivo è stata aggiornata." + }, + "error": { + "authentication_failed": "Autenticazione fallita. Errore:\n{msg}", + "cannot_connect": "Impossibile connettersi al dispositivo. Verifica che l'indirizzo sia corretto e riprova.", + "device_list_failed": "Impossibile recuperare l'elenco dei dispositivi.\n{msg}", + "invalid_auth": "Impossibile autenticarsi con il dispositivo. Verificare che device_id e local_key siano corretti.", + "unknown": "Si è verificato un errore sconosciuto. Vedere registro per i dettagli.", + "entity_already_configured": "L'entity con questo ID è già stata configurata.", + "address_in_use": "L'indirizzo utilizzato per il discovery è già in uso. Assicurarsi che nessun'altra applicazione lo stia utilizzando (porta TCP 6668).", + "discovery_failed": "Qualcosa è fallito nella discovery dei dispositivi. Vedi registro per i dettagli.", + "empty_dps": "La connessione al dispositivo è riuscita ma non sono stati trovati i datapoint, riprova. Crea un nuovo Issue e includi i log di debug se il problema persiste." + }, + "step": { + "user": { + "title": "Configurazione dell'account Cloud API", + "description": "Inserisci le credenziali per l'account Cloud API Tuya.", + "data": { + "region": "Regione del server API", + "client_id": "Client ID", + "client_secret": "Secret", + "user_id": "User ID", + "user_name": "Username", + "no_cloud": "Non configurare un account Cloud API" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Il dispositivo è già stato configurato.", + "device_success": "Dispositivo {dev_name} {action} con successo.", + "no_entities": "Non si possono rimuovere tutte le entities da un device.\nPer rimuovere un device, entrarci nel menu Devices, premere sui 3 punti nel riquadro 'Device info', e premere il pulsante Delete." + }, + "error": { + "authentication_failed": "Autenticazione fallita. Errore:\n{msg}", + "cannot_connect": "Impossibile connettersi al dispositivo. Verifica che l'indirizzo sia corretto e riprova.", + "device_list_failed": "Impossibile recuperare l'elenco dei dispositivi.\n{msg}", + "invalid_auth": "Impossibile autenticarsi con il dispositivo. Verificare che device_id e local_key siano corretti.", + "unknown": "Si è verificato un errore sconosciuto. Vedere registro per i dettagli.", + "entity_already_configured": "L'entity con questo ID è già stata configurata.", + "address_in_use": "L'indirizzo utilizzato per il discovery è già in uso. Assicurarsi che nessun'altra applicazione lo stia utilizzando (porta TCP 6668).", + "discovery_failed": "Qualcosa è fallito nella discovery dei dispositivi. Vedi registro per i dettagli.", + "empty_dps": "La connessione al dispositivo è riuscita ma non sono stati trovati i datapoint, riprova. Crea un nuovo Issue e includi i log di debug se il problema persiste." + }, + "step": { + "yaml_import": { + "title": "Non supportato", + "description": "Le impostazioni non possono essere configurate tramite file YAML." + }, + "init": { + "title": "Configurazione LocalTuya", + "description": "Seleziona l'azione desiderata.", + "data": { + "add_device": "Aggiungi un nuovo dispositivo", + "edit_device": "Modifica un dispositivo", + "setup_cloud": "Riconfigurare l'account Cloud API" + } + }, + "add_device": { + "title": "Aggiungi un nuovo dispositivo", + "description": "Scegli uno dei dispositivi trovati automaticamente o `...` per aggiungere manualmente un dispositivo.", + "data": { + "selected_device": "Dispositivi trovati" + } + }, + "edit_device": { + "title": "Modifica un dispositivo", + "description": "Scegli il dispositivo configurato che si desidera modificare.", + "data": { + "selected_device": "Dispositivi configurati" + } + }, + "cloud_setup": { + "title": "Configurazione dell'account Cloud API", + "description": "Inserisci le credenziali per l'account Cloud API Tuya.", + "data": { + "region": "Regione del server API", + "client_id": "Client ID", + "client_secret": "Secret", + "user_id": "User ID", + "user_name": "Username", + "no_cloud": "Non configurare l'account Cloud API" + } + }, + "configure_device": { + "title": "Configura il dispositivo", + "description": "Compila i dettagli del dispositivo {for_device}.", + "data": { + "friendly_name": "Nome", + "host": "Host", + "device_id": "ID del dispositivo", + "local_key": "Chiave locale", + "protocol_version": "Versione del protocollo", + "enable_debug": "Abilita il debugging per questo device (il debug va abilitato anche in configuration.yaml)", + "scan_interval": "Intervallo di scansione (secondi, solo quando non si aggiorna automaticamente)", + "entities": "Entities (deseleziona un'entity per rimuoverla)" + } + }, + "pick_entity_type": { + "title": "Selezione del tipo di entity", + "description": "Scegli il tipo di entity che desideri aggiungere.", + "data": { + "platform_to_add": "piattaforma", + "no_additional_entities": "Non aggiungere altre entity" + } + }, + "configure_entity": { + "title": "Configurare entity", + "description": "Compila i dettagli per {entity} con tipo `{platform}`.Tutte le impostazioni ad eccezione di `id` possono essere modificate dalla pagina delle opzioni in seguito.", + "data": { + "id": "ID", + "friendly_name": "Nome amichevole", + "current": "Corrente", + "current_consumption": "Potenza", + "voltage": "Tensione", + "commands_set": "Set di comandi Aperto_Chiuso_Stop", + "positioning_mode": "Modalità di posizionamento", + "current_position_dp": "Posizione attuale (solo per la modalità *posizione*)", + "set_position_dp": "Imposta posizione (solo per modalità *posizione*)", + "position_inverted": "Inverti posizione 0-100 (solo per modalità *posizione*)", + "span_time": "Tempo di apertura totale, in sec. (solo per modalità *a tempo*)", + "unit_of_measurement": "Unità di misura", + "device_class": "Classe del dispositivo", + "scaling": "Fattore di scala", + "state_on": "Valore di ON", + "state_off": "Valore di OFF", + "powergo_dp": "Potenza DP (di solito 25 o 2)", + "idle_status_value": "Stato di inattività (separato da virgole)", + "returning_status_value": "Stato di ritorno alla base", + "docked_status_value": "Stato di tornato alla base (separato da virgole)", + "fault_dp": "DP di guasto (di solito 11)", + "battery_dp": "DP di stato batteria (di solito 14)", + "mode_dp": "DP di modalità (di solito 27)", + "modes": "Elenco delle modalità", + "return_mode": "Ritorno in modalità home", + "fan_speed_dp": "DP di velocità del ventilatore (di solito 30)", + "fan_speeds": "DP di elenco delle velocità del ventilatore (separato da virgola)", + "clean_time_dp": "DP di tempo di pulizia (di solito 33)", + "clean_area_dp": "DP di area pulita (di solito 32)", + "clean_record_dp": "DP di record delle pulizie (di solito 34)", + "locate_dp": "DP di individuazione (di solito 31)", + "paused_state": "Stato di pausa (pausa, pausa, ecc.)", + "stop_status": "Stato di stop", + "brightness": "Luminosità (solo per il colore bianco)", + "brightness_lower": "Limite inferiore per la luminosità", + "brightness_upper": "Limite superiore per la luminosità", + "color_temp": "Temperatura di colore", + "color_temp_reverse": "Temperatura di colore invertita", + "color": "Colore", + "color_mode": "Modalità colore", + "color_temp_min_kelvin": "Minima temperatura di colore in K", + "color_temp_max_kelvin": "Massima temperatura di colore in k", + "music_mode": "Modalità musicale disponibile", + "scene": "Scena", + "select_options": "Opzioni valide, voci separate da una vigola (;)", + "select_options_friendly": "Opzioni intuitive, voci separate da una virgola", + "fan_speed_control": "DP di controllo di velocità del ventilatore", + "fan_oscillating_control": "DP di controllo dell'oscillazione del ventilatore", + "fan_speed_min": "Velocità del ventilatore minima", + "fan_speed_max": "Velocità del ventilatore massima", + "fan_speed_ordered_list": "Elenco delle modalità di velocità del ventilatore (sovrascrive velocità min/max)", + "fan_direction":"DP di direzione del ventilatore", + "fan_direction_forward": "Stringa del DP per avanti", + "fan_direction_reverse": "Stringa del DP per indietro", + "current_temperature_dp": "Temperatura attuale", + "target_temperature_dp": "Temperatura target", + "temperature_step": "Intervalli di temperatura (facoltativo)", + "max_temperature_dp": "Temperatura massima (opzionale)", + "min_temperature_dp": "Temperatura minima (opzionale)", + "precision": "Precisione (opzionale, per valori DP)", + "target_precision": "Precisione del target (opzionale, per valori DP)", + "temperature_unit": "Unità di temperatura (opzionale)", + "hvac_mode_dp": "Modalità HVAC attuale (opzionale)", + "hvac_mode_set": "Impostazione modalità HVAC (opzionale)", + "hvac_action_dp": "Azione HVAC attuale (opzionale)", + "hvac_action_set": "Impostazione azione HVAC (opzionale)", + "preset_dp": "Preset DP (opzionale)", + "preset_set": "Set di preset (opzionale)", + "eco_dp": "DP per Eco (opzionale)", + "eco_value": "Valore Eco (opzionale)", + "heuristic_action": "Abilita azione euristica (opzionale)" + } + } + } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reload localtuya and reconnect to all devices." + }, + "set_dp": { + "name": "Set datapoint", + "description": "Change the value of a datapoint (DP)", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Device ID of device to change datapoint value for" + }, + "dp": { + "name": "DP", + "description": "Datapoint index" + }, + "value": { + "name": "Value", + "description": "New value to set" + } + } + } + }, + "title": "LocalTuya" +} diff --git a/homeassistant/config/custom_components/localtuya/translations/pt-BR.json b/homeassistant/config/custom_components/localtuya/translations/pt-BR.json new file mode 100644 index 0000000..74884ee --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/translations/pt-BR.json @@ -0,0 +1,216 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo já foi configurado.", + "device_updated": "A configuração do dispositivo foi atualizada!" + }, + "error": { + "authentication_failed": "Falha ao autenticar.\n{msg}", + "cannot_connect": "Não é possível se conectar ao dispositivo. Verifique se o endereço está correto e tente novamente", + "device_list_failed": "Falha ao recuperar a lista de dispositivos.\n{msg}", + "invalid_auth": "Falha ao autenticar com o dispositivo. Verifique se o ID do dispositivo e a chave local estão corretos.", + "unknown": "Ocorreu um erro desconhecido. Consulte o registro para obter detalhes.", + "entity_already_configured": "A entidade com este ID já foi configurada.", + "address_in_use": "AddresO endereço usado para descoberta já está em uso. Certifique-se de que nenhum outro aplicativo o esteja usando (porta TCP 6668).s used for discovery is already in use. Make sure no other application is using it (TCP port 6668).", + "discovery_failed": "Algo falhou ao descobrir dispositivos. Consulte o registro para obter detalhes.", + "empty_dps": "A conexão com o dispositivo foi bem-sucedida, mas nenhum ponto de dados foi encontrado. Tente novamente. Crie um novo issue e inclua os logs de depuração se o problema persistir." + }, + "step": { + "user": { + "title": "Configuração da conta da API do Cloud", + "description": "Insira as credenciais para a API Tuya Cloud.", + "data": { + "region": "Região do servidor de API", + "client_id": "ID do cliente", + "client_secret": "Secret", + "user_id": "ID de usuário", + "user_name": "Nome de usuário", + "no_cloud": "Não configure uma conta de API da Cloud" + } + } + } + }, + "options": { + "abort": { + "already_configured": "O dispositivo já foi configurado.", + "device_success": "Dispositivo {dev_name} {action} com sucesso.", + "no_entities": "Não é possível remover todas as entidades de um dispositivo.\nSe você deseja excluir um dispositivo, insira-o no menu Dispositivos, clique nos 3 pontos no quadro 'Informações do dispositivo' e pressione o botão Excluir." + }, + "error": { + "authentication_failed": "Falha ao autenticar.\n{msg}", + "cannot_connect": "Não é possível se conectar ao dispositivo. Verifique se o endereço está correto e tente novamente", + "device_list_failed": "Falha ao recuperar a lista de dispositivos.\n{msg}", + "invalid_auth": "Falha ao autenticar com o dispositivo. Verifique se o ID do dispositivo e a chave local estão corretos.", + "unknown": "Ocorreu um erro desconhecido. Consulte o registro para obter detalhes.", + "entity_already_configured": "A entidade com este ID já foi configurada.", + "address_in_use": "O endereço usado para descoberta já está em uso. Certifique-se de que nenhum outro aplicativo o esteja usando (porta TCP 6668).", + "discovery_failed": "Algo falhou ao descobrir dispositivos. Consulte o registro para obter detalhes.", + "empty_dps": "A conexão com o dispositivo foi bem-sucedida, mas nenhum ponto de dados foi encontrado. Tente novamente. Crie um novo issue e inclua os logs de depuração se o problema persistir." + }, + "step": { + "yaml_import": { + "title": "Não suportado", + "description": "As opções não podem ser editadas quando configuradas via YAML." + }, + "init": { + "title": "Configuração LocalTuya", + "description": "Selecione a ação desejada.", + "data": { + "add_device": "Adicionar um novo dispositivo", + "edit_device": "Editar um dispositivo", + "setup_cloud": "Reconfigurar a conta da API da Cloud" + } + }, + "add_device": { + "title": "Adicionar um novo dispositivo", + "description": "Escolha um dos dispositivos descobertos automaticamente ou `...` para adicionar um dispositivo manualmente.", + "data": { + "selected_device": "Dispositivos descobertos" + } + }, + "edit_device": { + "title": "Editar um novo dispositivo", + "description": "Escolha o dispositivo configurado que você deseja editar.", + "data": { + "selected_device": "Dispositivos configurados" + } + }, + "cloud_setup": { + "title": "Configuração da conta da API da Cloud", + "description": "Insira as credenciais para a API Tuya Cloud.", + "data": { + "region": "Região do servidor de API", + "client_id": "ID do Cliente", + "client_secret": "Secret", + "user_id": "ID do usuário", + "user_name": "Nome de usuário", + "no_cloud": "Não configure a conta da API da Cloud" + } + }, + "configure_device": { + "title": "Configurar dispositivo Tuya", + "description": "Preencha os detalhes do dispositivo {for_device}.", + "data": { + "friendly_name": "Nome", + "host": "Host", + "device_id": "ID do dispositivo", + "local_key": "Local key", + "protocol_version": "Versão do protocolo", + "enable_debug": "Ative a depuração para este dispositivo (a depuração também deve ser ativada em configuration.yaml)", + "scan_interval": "Intervalo de escaneamento (segundos, somente quando não estiver atualizando automaticamente)", + "entities": "Entidades (desmarque uma entidade para removê-la)" + } + }, + "pick_entity_type": { + "title": "Seleção do tipo de entidade", + "description": "Escolha o tipo de entidade que deseja adicionar.", + "data": { + "platform_to_add": "Plataforma", + "no_additional_entities": "Não adicione mais entidades" + } + }, + "configure_entity": { + "title": "Configurar entidade", + "description": "Por favor, preencha os detalhes de {entity} com o tipo `{platform}`. Todas as configurações, exceto `ID`, podem ser alteradas na página Opções posteriormente.", + "data": { + "id": "ID", + "friendly_name": "Nome fantasia", + "current": "Atual", + "current_consumption": "Consumo atual", + "voltage": "Voltagem", + "commands_set": "Conjunto de comandos Abrir_Fechar_Parar", + "positioning_mode": "Modo de posicionamento", + "current_position_dp": "Posição atual (somente para o modo *posição*)", + "set_position_dp": "Definir posição (somente para o modo *posição*)", + "position_inverted": "Inverter 0-100 posição (somente para o modo *posição*)", + "span_time": "Tempo de abertura completo, em segundos. (somente para o modo *temporizado*)", + "unit_of_measurement": "Unidade de medida", + "device_class": "Classe do dispositivo", + "scaling": "Fator de escala", + "state_on": "Valor ligado", + "state_off": "Valor desligado", + "powergo_dp": "Potência DP (Geralmente 25 ou 2)", + "idle_status_value": "Status ocioso (separado por vírgula)", + "returning_status_value": "Status de retorno", + "docked_status_value": "Status encaixado (separado por vírgula)", + "fault_dp": "Falha DP (Geralmente 11)", + "battery_dp": "Status da bateria DP (normalmente 14)", + "mode_dp": "Modo DP (Geralmente 27)", + "modes": "Lista de modos", + "return_mode": "Modo de retorno para casa", + "fan_speed_dp": "Velocidades do ventilador DP (normalmente 30)", + "fan_speeds": "Lista de velocidades do ventilador (separadas por vírgulas)", + "clean_time_dp": "Tempo Limpo DP (Geralmente 33)", + "clean_area_dp": "Área Limpa DP (Geralmente 32)", + "clean_record_dp": "Limpar Registro DP (Geralmente 34)", + "locate_dp": "Localize DP (Geralmente 31)", + "paused_state": "Estado de pausa (pausa, pausado, etc)", + "stop_status": "Status de parada", + "brightness": "Brilho (somente para cor branca)", + "brightness_lower": "Valor mais baixo de brilho", + "brightness_upper": "Valor superior de brilho", + "color_temp": "Temperatura da cor", + "color_temp_reverse": "Temperatura da cor reversa", + "color": "Cor", + "color_mode": "Modo de cor", + "color_temp_min_kelvin": "Temperatura de cor mínima em K", + "color_temp_max_kelvin": "Temperatura máxima de cor em K", + "music_mode": "Modo de música disponível", + "scene": "Cena", + "select_options": "Entradas válidas, entradas separadas por um ;", + "select_options_friendly": "Opções fantasia ​​ao usuário, entradas separadas por um ;", + "fan_speed_control": "Dps de controle de velocidade do ventilador", + "fan_oscillating_control": "Dps de controle oscilante do ventilador", + "fan_speed_min": "Velocidade mínima do ventilador inteiro", + "fan_speed_max": "Velocidade máxima do ventilador inteiro", + "fan_speed_ordered_list": "Lista de modos de velocidade do ventilador (substitui a velocidade min/max)", + "fan_direction":"Direção do ventilador dps", + "fan_direction_forward": "Seqüência de dps para frente", + "fan_direction_reverse": "String dps reversa", + "current_temperature_dp": "Temperatura atual", + "target_temperature_dp": "Temperatura alvo", + "temperature_step": "Etapa de temperatura (opcional)", + "max_temperature_dp": "Temperatura máxima (opcional)", + "min_temperature_dp": "Temperatura mínima (opcional)", + "precision": "Precisão (opcional, para valores de DPs)", + "target_precision": "Precisão do alvo (opcional, para valores de DPs)", + "temperature_unit": "Unidade de Temperatura (opcional)", + "hvac_mode_dp": "Modo HVAC DP (opcional)", + "hvac_mode_set": "Conjunto de modo HVAC (opcional)", + "hvac_action_dp": "Ação atual de HVAC DP (opcional)", + "hvac_action_set": "Conjunto de ação atual HVAC (opcional)", + "preset_dp": "Predefinições DP (opcional)", + "preset_set": "Conjunto de predefinições (opcional)", + "eco_dp": "Eco DP (opcional)", + "eco_value": "Valor eco (opcional)", + "heuristic_action": "Ativar ação heurística (opcional)" + } + } + } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reload localtuya and reconnect to all devices." + }, + "set_dp": { + "name": "Set datapoint", + "description": "Change the value of a datapoint (DP)", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Device ID of device to change datapoint value for" + }, + "dp": { + "name": "DP", + "description": "Datapoint index" + }, + "value": { + "name": "Value", + "description": "New value to set" + } + } + } + }, + "title": "LocalTuya" +} diff --git a/homeassistant/config/custom_components/localtuya/vacuum.py b/homeassistant/config/custom_components/localtuya/vacuum.py new file mode 100644 index 0000000..0ac0b1e --- /dev/null +++ b/homeassistant/config/custom_components/localtuya/vacuum.py @@ -0,0 +1,241 @@ +"""Platform to locally control Tuya-based vacuum devices.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.vacuum import ( + DOMAIN, + StateVacuumEntity, VacuumActivity, VacuumEntityFeature, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_BATTERY_DP, + CONF_CLEAN_AREA_DP, + CONF_CLEAN_RECORD_DP, + CONF_CLEAN_TIME_DP, + CONF_DOCKED_STATUS_VALUE, + CONF_FAN_SPEED_DP, + CONF_FAN_SPEEDS, + CONF_FAULT_DP, + CONF_IDLE_STATUS_VALUE, + CONF_LOCATE_DP, + CONF_MODE_DP, + CONF_MODES, + CONF_PAUSED_STATE, + CONF_POWERGO_DP, + CONF_RETURN_MODE, + CONF_RETURNING_STATUS_VALUE, + CONF_STOP_STATUS, +) + +_LOGGER = logging.getLogger(__name__) + +CLEAN_TIME = "clean_time" +CLEAN_AREA = "clean_area" +CLEAN_RECORD = "clean_record" +MODES_LIST = "cleaning_mode_list" +MODE = "cleaning_mode" +FAULT = "fault" + +DEFAULT_IDLE_STATUS = "standby,sleep" +DEFAULT_RETURNING_STATUS = "docking" +DEFAULT_DOCKED_STATUS = "charging,chargecompleted" +DEFAULT_MODES = "smart,wall_follow,spiral,single" +DEFAULT_FAN_SPEEDS = "low,normal,high" +DEFAULT_PAUSED_STATE = "paused" +DEFAULT_RETURN_MODE = "chargego" +DEFAULT_STOP_STATUS = "standby" + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str, + vol.Required(CONF_POWERGO_DP): vol.In(dps), + vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str, + vol.Optional( + CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS + ): str, + vol.Optional(CONF_BATTERY_DP): vol.In(dps), + vol.Optional(CONF_MODE_DP): vol.In(dps), + vol.Optional(CONF_MODES, default=DEFAULT_MODES): str, + vol.Optional(CONF_RETURN_MODE, default=DEFAULT_RETURN_MODE): str, + vol.Optional(CONF_FAN_SPEED_DP): vol.In(dps), + vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, + vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), + vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps), + vol.Optional(CONF_CLEAN_RECORD_DP): vol.In(dps), + vol.Optional(CONF_LOCATE_DP): vol.In(dps), + vol.Optional(CONF_FAULT_DP): vol.In(dps), + vol.Optional(CONF_PAUSED_STATE, default=DEFAULT_PAUSED_STATE): str, + vol.Optional(CONF_STOP_STATUS, default=DEFAULT_STOP_STATUS): str, + } + + +class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): + """Tuya vacuum device.""" + + def __init__(self, device, config_entry, switchid, **kwargs): + """Initialize a new LocaltuyaVacuum.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + self._state = None + self._battery_level = None + self._attrs = {} + + self._idle_status_list = [] + if self.has_config(CONF_IDLE_STATUS_VALUE): + self._idle_status_list = self._config[CONF_IDLE_STATUS_VALUE].split(",") + + self._modes_list = [] + if self.has_config(CONF_MODES): + self._modes_list = self._config[CONF_MODES].split(",") + self._attrs[MODES_LIST] = self._modes_list + + self._docked_status_list = [] + if self.has_config(CONF_DOCKED_STATUS_VALUE): + self._docked_status_list = self._config[CONF_DOCKED_STATUS_VALUE].split(",") + + self._fan_speed_list = [] + if self.has_config(CONF_FAN_SPEEDS): + self._fan_speed_list = self._config[CONF_FAN_SPEEDS].split(",") + + self._fan_speed = "" + self._cleaning_mode = "" + _LOGGER.debug("Initialized vacuum [%s]", self.name) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ( + VacuumEntityFeature.START + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.STOP + | VacuumEntityFeature.STATUS + | VacuumEntityFeature.STATE + ) + + if self.has_config(CONF_RETURN_MODE): + supported_features = supported_features | VacuumEntityFeature.RETURN_HOME + if self.has_config(CONF_FAN_SPEED_DP): + supported_features = supported_features | VacuumEntityFeature.FAN_SPEED + if self.has_config(CONF_BATTERY_DP): + supported_features = supported_features | VacuumEntityFeature.BATTERY + if self.has_config(CONF_LOCATE_DP): + supported_features = supported_features | VacuumEntityFeature.LOCATE + + return supported_features + + @property + def state(self): + """Return the vacuum state.""" + return self._state + + @property + def battery_level(self): + """Return the current battery level.""" + return self._battery_level + + @property + def extra_state_attributes(self): + """Return the specific state attributes of this vacuum cleaner.""" + return self._attrs + + @property + def fan_speed(self): + """Return the current fan speed.""" + return self._fan_speed + + @property + def fan_speed_list(self) -> list: + """Return the list of available fan speeds.""" + return self._fan_speed_list + + async def async_start(self, **kwargs): + """Turn the vacuum on and start cleaning.""" + await self._device.set_dp(True, self._config[CONF_POWERGO_DP]) + + async def async_pause(self, **kwargs): + """Stop the vacuum cleaner, do not return to base.""" + await self._device.set_dp(False, self._config[CONF_POWERGO_DP]) + + async def async_return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + if self.has_config(CONF_RETURN_MODE): + await self._device.set_dp( + self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP] + ) + else: + _LOGGER.error("Missing command for return home in commands set.") + + async def async_stop(self, **kwargs): + """Turn the vacuum off stopping the cleaning.""" + if self.has_config(CONF_STOP_STATUS): + await self._device.set_dp( + self._config[CONF_STOP_STATUS], self._config[CONF_MODE_DP] + ) + else: + _LOGGER.error("Missing command for stop in commands set.") + + async def async_clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + return None + + async def async_locate(self, **kwargs): + """Locate the vacuum cleaner.""" + if self.has_config(CONF_LOCATE_DP): + await self._device.set_dp("", self._config[CONF_LOCATE_DP]) + + async def async_set_fan_speed(self, fan_speed, **kwargs): + """Set the fan speed.""" + await self._device.set_dp(fan_speed, self._config[CONF_FAN_SPEED_DP]) + + async def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + if command == "set_mode" and "mode" in params: + mode = params["mode"] + await self._device.set_dp(mode, self._config[CONF_MODE_DP]) + + def status_updated(self): + """Device status was updated.""" + state_value = str(self.dps(self._dp_id)) + + if state_value in self._idle_status_list: + self._state = VacuumActivity.IDLE + elif state_value in self._docked_status_list: + self._state = VacuumActivity.DOCKED + elif state_value == self._config[CONF_RETURNING_STATUS_VALUE]: + self._state = VacuumActivity.RETURNING + elif state_value == self._config[CONF_PAUSED_STATE]: + self._state = VacuumActivity.PAUSED + else: + self._state = VacuumActivity.CLEANING + + if self.has_config(CONF_BATTERY_DP): + self._battery_level = self.dps_conf(CONF_BATTERY_DP) + + self._cleaning_mode = "" + if self.has_config(CONF_MODES): + self._cleaning_mode = self.dps_conf(CONF_MODE_DP) + self._attrs[MODE] = self._cleaning_mode + + self._fan_speed = "" + if self.has_config(CONF_FAN_SPEEDS): + self._fan_speed = self.dps_conf(CONF_FAN_SPEED_DP) + + if self.has_config(CONF_CLEAN_TIME_DP): + self._attrs[CLEAN_TIME] = self.dps_conf(CONF_CLEAN_TIME_DP) + + if self.has_config(CONF_CLEAN_AREA_DP): + self._attrs[CLEAN_AREA] = self.dps_conf(CONF_CLEAN_AREA_DP) + + if self.has_config(CONF_CLEAN_RECORD_DP): + self._attrs[CLEAN_RECORD] = self.dps_conf(CONF_CLEAN_RECORD_DP) + + if self.has_config(CONF_FAULT_DP): + self._attrs[FAULT] = self.dps_conf(CONF_FAULT_DP) + if self._attrs[FAULT] != 0: + self._state = VacuumActivity.ERROR + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/__init__.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/__init__.py new file mode 100644 index 0000000..94ace24 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/__init__.py @@ -0,0 +1,285 @@ +""" +The custom component for local network access to Midea appliances +""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_VERSION, + CONF_BROADCAST_ADDRESS, + CONF_DEVICES, + CONF_DISCOVERY, + CONF_EXCLUDE, + CONF_ID, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_TTL, + CONF_TYPE, + CONF_UNIQUE_ID, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get +from midea_beautiful.cloud import MideaCloud +from midea_beautiful.exceptions import MideaError +from midea_beautiful.lan import LanDevice +from midea_beautiful.midea import SUPPORTED_APPS, DEFAULT_APP_ID, DEFAULT_APPKEY + +from custom_components.midea_dehumidifier_lan.const import ( + CONF_MOBILE_APP, + CONF_TOKEN_KEY, + CONF_USE_CLOUD_OBSOLETE, + DEFAULT_APP, + DEFAULT_TTL, + DISCOVERY_CLOUD, + DISCOVERY_IGNORE, + DISCOVERY_LAN, + DISCOVERY_WAIT, + DOMAIN, + LOCAL_BROADCAST, + NAME, + CURRENT_CONFIG_VERSION, + OBSOLETE_CONF_APPID, + OBSOLETE_CONF_APPKEY, + PLATFORMS, + UNKNOWN_IP, +) +from custom_components.midea_dehumidifier_lan.hub import Hub +from custom_components.midea_dehumidifier_lan.util import MideaClient, address_ok + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + + if (hub := hass.data[DOMAIN].get(config_entry.entry_id)) is None: + hub = Hub(hass, config_entry) + hass.data[DOMAIN][config_entry.entry_id] = hub + await hub.async_setup() + await _async_migrate_names(hass, config_entry) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def _async_migrate_names(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + entity_registry = async_get(hass) + + conf = config_entry.data + if devices := conf.get(CONF_DEVICES): + old_entites = [ + entry + for _, entry in entity_registry.entities.items() + if entry.platform == DOMAIN + ] + for reg_entry in old_entites: + for device in devices: + old_suffix = f"_{device[CONF_ID]}" + new_suffix = f"_{device[CONF_UNIQUE_ID]}" + if reg_entry.unique_id.endswith(old_suffix): + prefix = reg_entry.unique_id[: -len(old_suffix)] + old_unique_id = reg_entry.unique_id + new_unique_id = f"{prefix}{new_suffix}" + try: + entity_registry.async_update_entity( + reg_entry.entity_id, + new_unique_id=new_unique_id, + ) + _LOGGER.warning( + "Changed unique id of %s from %s to %s", + reg_entry.entity_id, + old_unique_id, + new_unique_id, + ) + except ValueError as ex: + _LOGGER.error( + "Unable to change unique id of %s: %s", + reg_entry.entity_id, + ex, + ) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hub: Hub = hass.data[DOMAIN].pop(entry.entry_id) + await hub.async_unload() + + return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old config entry to new version.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version < CURRENT_CONFIG_VERSION: + old_conf = config_entry.data + old_broadcast = old_conf.get(CONF_BROADCAST_ADDRESS, []) + if not old_broadcast: + old_broadcast = [LOCAL_BROADCAST] + new_conf = { + CONF_MOBILE_APP: old_conf.get(CONF_MOBILE_APP), + CONF_BROADCAST_ADDRESS: old_broadcast, + CONF_USERNAME: old_conf.get(CONF_USERNAME), + CONF_PASSWORD: old_conf.get(CONF_PASSWORD), + } + if not old_conf.get(OBSOLETE_CONF_APPID) or not old_conf.get( + OBSOLETE_CONF_APPKEY + ): + new_conf[CONF_MOBILE_APP] = DEFAULT_APP + else: + appkey = old_conf.get(OBSOLETE_CONF_APPKEY, DEFAULT_APPKEY) + if appkey: + for appname, appconf in SUPPORTED_APPS.items(): + if appconf["appkey"] == appkey: + new_conf[CONF_MOBILE_APP] = appname + break + else: + appid = old_conf.get(OBSOLETE_CONF_APPID, DEFAULT_APP_ID) + for appname, appconf in SUPPORTED_APPS.items(): + if appconf["appid"] == appid: + new_conf[CONF_MOBILE_APP] = appname + break + + new_devices = [] + new_conf[CONF_DEVICES] = new_devices + + id_resolver = _ApplianceIdResolver(hass) + + old: dict[str, Any] + for old in config_entry.data[CONF_DEVICES]: + new = { + CONF_API_VERSION: old.get(CONF_API_VERSION), + CONF_DISCOVERY: old.get(CONF_DISCOVERY), + CONF_ID: old.get(CONF_ID), + CONF_IP_ADDRESS: old.get(CONF_IP_ADDRESS, UNKNOWN_IP), + CONF_NAME: old.get(CONF_NAME), + CONF_TOKEN: old.get(CONF_TOKEN), + CONF_TOKEN_KEY: old.get(CONF_TOKEN_KEY), + CONF_TYPE: old.get(CONF_TYPE), + CONF_UNIQUE_ID: old.get(CONF_UNIQUE_ID), + CONF_TTL: old.get(CONF_TTL, DEFAULT_TTL), + } + + discovery_mode = new.get(CONF_DISCOVERY) + if discovery_mode not in [ + DISCOVERY_WAIT, + DISCOVERY_LAN, + DISCOVERY_IGNORE, + DISCOVERY_CLOUD, + ]: + if old.get(CONF_USE_CLOUD_OBSOLETE): + new[CONF_DISCOVERY] = DISCOVERY_CLOUD + elif old.get(CONF_EXCLUDE): + new[CONF_DISCOVERY] = DISCOVERY_IGNORE + elif not address_ok(old.get(CONF_IP_ADDRESS)): + new[CONF_DISCOVERY] = DISCOVERY_WAIT + else: + new[CONF_DISCOVERY] = DISCOVERY_LAN + + await id_resolver.async_get_unique_id_if_missing(new_conf, new) + new_devices.append(new) + + config_entry.version = CURRENT_CONFIG_VERSION + _LOGGER.debug( + "Migrating configuration from %s to %s", config_entry.data, new_conf + ) + if hass.config_entries.async_update_entry( + config_entry, data=new_conf, title=NAME + ): + _LOGGER.info("Configuration migrated to version %s", config_entry.version) + else: + _LOGGER.debug( + "Configuration didn't change during migration to version %s", + config_entry.version, + ) + return id_resolver.success + + return True + + +# pylint: disable=too-few-public-methods +class _ApplianceIdResolver: + def __init__(self, hass: HomeAssistant) -> None: + self.hass = hass + self.client = MideaClient(hass) + self.cloud: MideaCloud | None = None + self.descriptors: list[dict] | None = None + self.success = True + + async def _start(self, conf: dict[str, Any]) -> None: + try: + self.cloud = await self.client.async_connect_to_cloud(conf) + self.descriptors = await self.client.async_list_appliances(self.cloud) + except MideaError as ex: + _LOGGER.error( + "Unable to get list of appliances during configuration migration %s.", + ex, + exc_info=True, + ) + + async def async_get_unique_id_if_missing( + self, + conf: dict[str, Any], + device_conf: dict[str, Any], + ) -> None: + """If there is no unique_id assigned, try to find serial number""" + if device_conf[CONF_UNIQUE_ID] is None: + if device_conf[CONF_DISCOVERY] == DISCOVERY_LAN: + appliance = await self._get_appliance_state(device_conf) + device_conf[CONF_UNIQUE_ID] = appliance and appliance.serial_number + if device_conf[CONF_UNIQUE_ID] is None: + if self.cloud is None: + await self._start(conf) + self._find_unique_id_in_appliance_list(device_conf) + if device_conf[CONF_UNIQUE_ID] is None: + _LOGGER.error( + "Unable to find serial number for appliance %s." + "Please re-install %s integration.", + device_conf[CONF_NAME], + NAME, + ) + self.success = False + + def _find_unique_id_in_appliance_list(self, device_conf) -> None: + if self.descriptors is not None: + for app in self.descriptors: + if app["id"] == device_conf[CONF_ID]: + if app["sn"] and app["sn"] != "Unknown": + device_conf[CONF_UNIQUE_ID] = app["sn"] + else: + _LOGGER.warning("Unable to get serial number for %s", app) + break + + async def _get_appliance_state( + self, + device_conf: dict[str, Any], + cloud: MideaCloud = None, + use_cloud: bool = False, + ) -> LanDevice | None: + try: + return await self.hass.async_add_executor_job( + self.client.appliance_state, + device_conf[CONF_IP_ADDRESS], + device_conf[CONF_TOKEN], + device_conf[CONF_TOKEN_KEY], + cloud, + use_cloud, + device_conf[CONF_ID], + ) + except MideaError as ex: + _LOGGER.error( + "Unable to poll appliance during configuration migration %s.", + ex, + exc_info=True, + ) + return None diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_coordinator.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_coordinator.py new file mode 100644 index 0000000..e98d09f --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_coordinator.py @@ -0,0 +1,294 @@ +"""Update coordinator for Midea devices""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timedelta +import logging +from time import monotonic +from typing import Any, cast, final + +from homeassistant.const import CONF_DISCOVERY, CONF_TOKEN, CONF_TTL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +from homeassistant.util import slugify +from midea_beautiful.appliance import AirConditionerAppliance, DehumidifierAppliance +from midea_beautiful.cloud import MideaCloud +from midea_beautiful.exceptions import MideaError +from midea_beautiful.lan import LanDevice + +from custom_components.midea_dehumidifier_lan.const import ( + APPLIANCE_REFRESH_COOLDOWN, + APPLIANCE_REFRESH_INTERVAL, + CONF_TOKEN_KEY, + DEFAULT_TTL, + DISCOVERY_CLOUD, + DISCOVERY_IGNORE, + DOMAIN, + ENTITY_DISABLED_BY_DEFAULT, + ENTITY_ENABLED_BY_DEFAULT, + UNIQUE_DEHUMIDIFIER_PREFIX, +) +from custom_components.midea_dehumidifier_lan.util import ( + AbstractHub, + ApplianceCoordinator, + RedactedConf, +) + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=too-many-instance-attributes +class ApplianceUpdateCoordinator(DataUpdateCoordinator, ApplianceCoordinator): + """Single class to retrieve data from an appliance""" + + def __init__( # pylint: disable=too-many-arguments + self, + hass: HomeAssistant, + hub: AbstractHub, + appliance: LanDevice, + device: dict[str, Any], + available: bool, + ): + super().__init__( + hass, + _LOGGER, + name=appliance.name, + update_method=self._async_appliance_refresh, + update_interval=timedelta(seconds=APPLIANCE_REFRESH_INTERVAL), + request_refresh_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=APPLIANCE_REFRESH_COOLDOWN, + immediate=True, + function=self.async_refresh, + ), + ) + self.hub = hub + self.appliance = appliance + self.updating = {} + self.wait_for_update = False + self.device = device + self.discovery_mode = device.get(CONF_DISCOVERY, DISCOVERY_IGNORE) + self.use_cloud: bool = self.discovery_mode == DISCOVERY_CLOUD + self.available = available + # TTL is in minutes + self.time_to_leave = 60 * int(device.get(CONF_TTL, DEFAULT_TTL)) + self.has_failure = False + self.first_failure_time: float = 0 + + def _cloud(self) -> MideaCloud | None: + if self.use_cloud: + if not self.hub.cloud: + raise UpdateFailed( + f"Midea cloud API was not initialized, {self.appliance}" + f" configuration={RedactedConf(self.hub.config)}" + ) + return self.hub.cloud + return None + + async def _async_appliance_refresh(self) -> LanDevice: + """Called to refresh appliance state""" + + if not self.available: + await self._async_try_to_detect() + + if self.wait_for_update: + return self.appliance + + try: + if self.updating: + await self._async_do_update() + + await self.hass.async_add_executor_job( + self.appliance.refresh, self._cloud() + ) + self.has_failure = False + except MideaError as ex: + if not self.has_failure: + self.has_failure = True + self.first_failure_time = monotonic() + if (monotonic() - self.first_failure_time) >= self.time_to_leave: + raise UpdateFailed(str(ex)) from ex + _LOGGER.warning( + "Error fetching %s data: %s, will be trying again.", self.name, ex + ) + finally: + self.wait_for_update = False + return self.appliance + + async def _async_do_update(self): + self.wait_for_update = True + _LOGGER.debug("Updating attributes for %s: %s", self.appliance, self.updating) + for attr in self.updating: + setattr(self.appliance.state, attr, self.updating[attr]) + self.updating.clear() + await self.hass.async_add_executor_job(self.appliance.apply, self._cloud()) + + async def _async_try_to_detect(self): + _LOGGER.debug("Trying to find appliance %s", self.appliance) + need_token, appliance = await self.hub.async_discover_device(self.device) + if not appliance: + raise UpdateFailed(self.hub.errors.get(str(self.appliance.serial_number))) + if need_token: + self.device[CONF_TOKEN] = appliance.token + self.device[CONF_TOKEN_KEY] = appliance.key + + self.appliance = appliance + await self.hub.async_update_config() + self.available = True + + async def async_apply(self, args: dict) -> None: + """Applies changes to device""" + for key, value in args.items(): + self.updating[key] = value + await self.async_request_refresh() + + +class ApplianceEntity(CoordinatorEntity): + """Represents an appliance that gets data from a coordinator""" + + _unique_id_prefx = UNIQUE_DEHUMIDIFIER_PREFIX + _name_suffix = "" + _capability_attr = "" + _add_extra_attrs = False + _was_online_registered = False + + def __init__(self, coordinator: ApplianceUpdateCoordinator) -> None: + self.coordinator = coordinator + self.appliance = coordinator.appliance + self._set_enabled_for_capability() + super().__init__(coordinator) + self._attr_unique_id = f"{self.unique_id_prefix}{self.appliance.serial_number}" + self._attr_name = str(self.appliance.name or self.unique_id) + self.name_suffix + if self._add_extra_attrs: + self._attr_extra_state_attributes = { + "last_error_code": 0, + "last_error_time": datetime.now(), + } + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + # Override parent, we will handle state + self.async_on_remove(self.coordinator.async_add_listener(self._updated_data)) + if self.coordinator.available: + self.on_online(True) + self._was_online_registered = True + + @callback + def _updated_data(self) -> None: + """Called when data has been updated by coordinator""" + + self.appliance = self.coordinator.appliance + self._attr_available = self.appliance.online + if not self.coordinator.available: + self.on_online(False) + elif not self._was_online_registered: + self.on_online(True) + + if self.appliance.online: + self.on_update() + self.async_write_ha_state() + + def _set_enabled_for_capability(self) -> None: + capability = self._capability_attr + if not capability: + return + if capability == ENTITY_ENABLED_BY_DEFAULT: + enabled = True + elif capability == ENTITY_DISABLED_BY_DEFAULT: + enabled = False + elif capabilities := self.appliance.state.capabilities: + enabled = capabilities.get(capability, False) + elif hasattr(self, "_attr_entity_registry_enabled_default"): + return + else: + enabled = False + self._attr_entity_registry_enabled_default = enabled + + def on_update(self) -> None: + """Allows additional processing after the coordinator updates data""" + if self._add_extra_attrs: + state = self.appliance.state + _error_code = state.error_code + + self._attr_extra_state_attributes |= { + "capabilities": str(state.capabilities), + "capabilities_data": state.capabilities_data.hex(), + "error_code": _error_code, + "last_data": state.latest_data.hex(), + } + if _error_code: + self._attr_extra_state_attributes |= { + "last_error_code": _error_code, + "last_error_time": datetime.now(), + } + + def on_online(self, update: bool) -> None: + """To be called when appliance comes online for the first time""" + + if update: + self.on_update() + self.async_write_ha_state() + + @final + def dehumidifier(self) -> DehumidifierAppliance: + """Returns state as dehumidifier""" + return cast(DehumidifierAppliance, self.appliance.state) + + @final + def airconditioner(self) -> AirConditionerAppliance: + """Returns state as air conditioner""" + return cast(AirConditionerAppliance, self.appliance.state) + + @property + def available(self) -> bool: + """Return if entity is available.""" + if not self.coordinator.available: + return False + return super().available + + @property + def name_suffix(self) -> str: + """Suffix to append to entity name""" + return self._name_suffix + + @property + def unique_id_prefix(self) -> str: + """Prefix for entity id""" + strip = self.name_suffix.strip() + if len(strip) == 0: + return self._unique_id_prefx + slug = slugify(strip) + return f"{self._unique_id_prefx}{slug}_" + + @property + def device_info(self) -> DeviceInfo: + identifier = str(self.appliance.serial_number or self.appliance.serial_number) + mac = self.appliance.mac + return DeviceInfo( + identifiers={(DOMAIN, str(identifier))}, + name=self.appliance.name, + manufacturer="Midea", + model=str(self.appliance.model), + sw_version=self.appliance.firmware_version, + ) + + def apply(self, *args, **kwargs) -> None: + """Applies changes to device""" + if len(args) % 2 != 0: + raise ValueError(f"Expecting attribute/value pairs, had {len(args)} items") + aargs = {} + for i in range(0, len(args), 2): + aargs[args[i]] = args[i + 1] + for key, value in kwargs.items(): + aargs[key] = value + asyncio.run_coroutine_threadsafe( + self.coordinator.async_apply(aargs), self.hass.loop + ).result() diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_discovery.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_discovery.py new file mode 100644 index 0000000..1410419 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/appliance_discovery.py @@ -0,0 +1,402 @@ +"""The custom component for local network access to Midea appliances""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +import ipaddress +from itertools import chain, cycle +import logging +from typing import Any, Iterator, cast + +from homeassistant.core import CALLBACK_TYPE +from homeassistant.components.network import async_get_ipv4_broadcast_addresses +from homeassistant.const import ( + CONF_API_VERSION, + CONF_BROADCAST_ADDRESS, + CONF_DEVICES, + CONF_DISCOVERY, + CONF_ID, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_TYPE, + CONF_UNIQUE_ID, +) +from homeassistant.helpers.event import async_track_time_interval +from midea_beautiful.lan import LanDevice + +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.const import ( + CONF_TOKEN_KEY, + DEFAULT_DISCOVERY_MODE, + DEFAULT_SCAN_INTERVAL, + DISCOVERY_BATCH_SIZE, + DISCOVERY_IGNORE, + DISCOVERY_LAN, + DISCOVERY_MODE_EXPLANATION, + DISCOVERY_WAIT, + LOCAL_BROADCAST, + NAME, + UNKNOWN_IP, +) +from custom_components.midea_dehumidifier_lan.util import ( + AbstractHub, + RedactedConf, + address_ok, + supported_appliance, +) + +_LOGGER = logging.getLogger(__name__) + + +def empty_address_iterator(): + """No addresses to iterate""" + yield from () + + +def _add_if_discoverable(conf_addresses: list[str], device: dict[str, Any]): + if device.get(CONF_DISCOVERY) != DISCOVERY_LAN: + if address_ok(device[CONF_IP_ADDRESS]): + conf_addresses.append(device[CONF_IP_ADDRESS]) + + +@dataclass +class _ChangedDevice: + device: LanDevice + coordinator: ApplianceUpdateCoordinator + + +class ApplianceDiscoveryHelper: # pylint: disable=too-many-instance-attributes + """Utility class to discover Midea appliances on local network""" + + def __init__( + self, + hub: AbstractHub, + ) -> None: + self.hass = hub.hass + self.hub = hub + self.new_devices: list[LanDevice] = [] + self.changed_devices: list[_ChangedDevice] = [] + self.broadcast_addresses: list[str] = [] + self.address_iterator: Iterator[list[str]] = empty_address_iterator() + self.notifed_addresses: set[str] = set() + self.remove_discovery: CALLBACK_TYPE | None = None + self.conf_addresses: list[str] = [] + + def _admit_new(self) -> bool: + """Admits new devices into configurations""" + need_reload = False + added_devices: list[dict[str, Any]] = [] + dev_confs = self.hub.config[CONF_DEVICES] + for new in self.new_devices: + for known in dev_confs: + if self._admitted_known_device(known, new): + need_reload = True + break + else: + added_devices.append(self._admit_not_known_device(new)) + need_reload = True + + if added_devices: + dev_confs += added_devices + return need_reload + + def _admit_not_known_device(self, new: LanDevice) -> dict[str, Any]: + name = f"{new.model} {new.mac[-4] if new.mac else new.serial_number}" + new_device = { + CONF_DISCOVERY: DISCOVERY_IGNORE, + CONF_API_VERSION: new.version, + CONF_ID: new.appliance_id, + CONF_IP_ADDRESS: new.address, + CONF_NAME: name, + CONF_TOKEN_KEY: new.key, + CONF_TOKEN: new.token, + CONF_TYPE: new.type, + CONF_UNIQUE_ID: new.serial_number, + } + + _LOGGER.debug("Found unknown device %s at %s.", name, new.address) + msg = ( + f"Found previously unknown device {name} found on {new.address}." + f" [Check it out.](/config/integrations)" + ) + self.hass.components.persistent_notification.async_create( + title=NAME, + message=msg, + notification_id=f"midea_unknown_{new.serial_number}", + ) + return new_device + + def _admitted_known_device(self, known: dict[str, Any], new: LanDevice) -> bool: + need_reload = False + if known[CONF_UNIQUE_ID] == new.serial_number: + if known[CONF_DISCOVERY] == DISCOVERY_WAIT: + update = { + CONF_DISCOVERY: DISCOVERY_LAN, + CONF_API_VERSION: new.version, + CONF_ID: new.appliance_id, + CONF_IP_ADDRESS: new.address, + CONF_TOKEN_KEY: new.key, + CONF_TOKEN: new.token, + CONF_TYPE: new.type, + CONF_UNIQUE_ID: new.serial_number, + } + _LOGGER.debug( + "Updating discovered device %s, previous conf %s, conf update %s", + new, + known, + update, + ) + + msg = ( + "Device %(name)s," + " which was waiting to be discovered," + " was found on address %(address)s." + " It will now be activated." + ) % { + "name": known[CONF_NAME], + "address": new.address, + } + self.hass.components.persistent_notification.async_create( + title=NAME, + message=msg, + notification_id=f"midea_wait_discovery_{new.serial_number}", + ) + known |= update + need_reload = True + elif new.address and known[CONF_DISCOVERY] != DISCOVERY_LAN: + self._possible_lan_notification(new, known, new.address) + + return need_reload + + def _possible_lan_notification( + self, device: LanDevice, known: dict[str, Any], address: str + ): + if address not in self.notifed_addresses: + _LOGGER.warning( + "Device %s in mode %s found on address %s. " + " It can be configured for local network access.", + known[CONF_NAME], + known[CONF_DISCOVERY], + address, + ) + self.notifed_addresses.add(address) + + discovery_label = DISCOVERY_MODE_EXPLANATION.get( + known[CONF_DISCOVERY], known[CONF_DISCOVERY] + ) + msg = ( + "Device %(name)s," + " which is %(discovery_label)s," + " was found on address %(address)s." + " It can be configured for local network access." + " [Check it out.](/config/integrations)" + ) % { + "name": known[CONF_NAME], + "discovery_label": discovery_label, + "address": address, + } + + self.hass.components.persistent_notification.async_create( + title=NAME, + message=msg, + notification_id=f"midea_non_lan_discovery_{device.serial_number}", + ) + + def _address_generator(self, batch_size: int = DISCOVERY_BATCH_SIZE): + """Generator for one batch of ip addresses to scan""" + net_addrs = [] + addr_count = 0 + for addr in self.conf_addresses: + # If local broadcast address we don't need to expand it + if addr == LOCAL_BROADCAST: + continue + # Get network corresponding to address + net = ipaddress.IPv4Network(addr) + # If network references a block: + if net.num_addresses > 1: + _LOGGER.debug("Block %s with %d addresses", net, net.num_addresses) + # collect all hosts from the block + net_addrs.append(net.hosts()) + addr_count += net.num_addresses + + # If we do have addresses to scan + if net_addrs: + # we will iterate over all of available addresses in batches + # having batch_size items + all_addrs = chain(*net_addrs) + for _ in range(0, addr_count, batch_size): + yield list( + # We use filter to remove empty addresses + filter( + None, + map( + (lambda _: (x := next(all_addrs)) and str(x)), + range(batch_size), + ), + ) + ) + + async def _async_run_discovery(self, devices: list[LanDevice]) -> None: + """Trigger config flows for discovered devices.""" + + dev_confs: list[dict[str, Any]] = self.hub.config[CONF_DEVICES] + for dev_conf in dev_confs: + dev_conf.setdefault(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE) + dev_conf.setdefault(CONF_IP_ADDRESS, UNKNOWN_IP) + + self._iterate_devices(devices) + + need_reload = self._admit_new() + devices_changed = self._merge_with_configuration() + + if devices_changed or need_reload: + _LOGGER.debug("Config entry needs to be updated") + self.hass.config_entries.async_update_entry( + entry=self.hub.config_entry, + data=self.hub.config, + ) + if need_reload: + _LOGGER.debug("Config entry needs to be reloaded") + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.hub.config_entry.entry_id) + ) + + def _iterate_devices(self, devices: list[LanDevice]): + self.new_devices.clear() + self.changed_devices.clear() + for device in devices: + if not device.address: + continue + coordinator = next( + ( + cast(ApplianceUpdateCoordinator, coord) + for coord in self.hub.coordinators + if coord.appliance.serial_number == device.serial_number + ), + None, + ) + if coordinator: + # If address changed, we need to handle it + if device.address and device.address != coordinator.appliance.address: + _LOGGER.debug( + "Device %s changed address to %s", + coordinator.name, + device.address, + ) + self.changed_devices.append(_ChangedDevice(device, coordinator)) + elif supported_appliance(self.hub.config, device): + _LOGGER.debug("Discovered new device %s", device) + self.new_devices.append(device) + + def _merge_with_configuration(self: ApplianceDiscoveryHelper) -> bool: + """Merges list of changed devices with existing config entry configuration""" + dev_confs: list[dict[str, Any]] = self.hub.config[CONF_DEVICES] + updated_conf = False + for changed in self.changed_devices: + for known in dev_confs: + coordinator = changed.coordinator + device = changed.device + if known[CONF_UNIQUE_ID] == coordinator.appliance.serial_number: + coordinator.appliance.address = device.address + known[CONF_IP_ADDRESS] = device.address + updated_conf = True + if device.address and known[CONF_DISCOVERY] != DISCOVERY_LAN: + self._possible_lan_notification( + coordinator.appliance, + known, + device.address, + ) + break + + return updated_conf + + def _setup(self) -> None: + """Initializes address iterator. + Address iterator allows iterating over adresses to broadcast to. + It will iterate over all addresses in specified ranges. + """ + self.notifed_addresses.clear() + self.conf_addresses.clear() + has_discoverable = False + device: dict[str, Any] + for device in self.hub.config[CONF_DEVICES]: + if _add_if_discoverable(self.conf_addresses, device): + has_discoverable = True + for coordinator in self.hub.coordinators: + if not coordinator.available: + if _add_if_discoverable(self.conf_addresses, coordinator.device): + has_discoverable = True + self.conf_addresses += [ + item + for item in self.hub.config.get(CONF_BROADCAST_ADDRESS, []) or [] + if item and item != LOCAL_BROADCAST + ] + self.broadcast_addresses = [LOCAL_BROADCAST] + for addr in self.conf_addresses: + net = ipaddress.IPv4Network(addr) + self.broadcast_addresses.append(str(net.broadcast_address)) + + if has_discoverable and self.conf_addresses: + _LOGGER.debug("Discovery via configured addresses %s", self.conf_addresses) + self.address_iterator = cycle(self._address_generator()) + else: + self.address_iterator = empty_address_iterator() + + def start(self) -> None: + """Starts periodic disovery of devices""" + self.stop() + try: + self._setup() + scan_interval = self.hub.config.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + if scan_interval: + _LOGGER.debug( + "Starting periodic discovery with interval %s minute(s)," + " broadcast %s, configured %s", + scan_interval, + self.broadcast_addresses, + self.conf_addresses, + ) + self.remove_discovery = async_track_time_interval( + self.hass, self._async_discover, timedelta(minutes=scan_interval) + ) + except Exception as ex: + _LOGGER.error( + "Unable to setup up periodic discovery." + " Please remove integration and then reinstall it to check if problem" + " can be fixed." + " Cause: %s" + " Configuration: %s", + ex, + RedactedConf(self.hub.config), + ) + self.stop() + raise ex + + def stop(self) -> None: + """Stops periodic disovery of devices""" + if self.remove_discovery: + _LOGGER.debug("Stopping periodic discovery") + + self.remove_discovery() + self.remove_discovery = None + + async def _async_discover(self, _: datetime) -> None: + """Discover Midea appliances on configured network interfaces.""" + + addresses = list(address for address in self.broadcast_addresses) + if new_addresses := next(self.address_iterator, None): + addresses += new_addresses + if not addresses: + iface_broadcast = await async_get_ipv4_broadcast_addresses(self.hass) + addresses += [str(address) for address in iface_broadcast] + _LOGGER.debug("Initiated discovery via %s", addresses) + result = self.hub.client.find_appliances(None, addresses, retries=1, timeout=1) + if result: + await self._async_run_discovery(result) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/binary_sensor.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/binary_sensor.py new file mode 100644 index 0000000..ec277fb --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/binary_sensor.py @@ -0,0 +1,118 @@ +"""Adds binary sensors for appliances.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from midea_beautiful.midea import ERROR_CODE_BUCKET_FULL, ERROR_CODE_BUCKET_REMOVED + +from custom_components.midea_dehumidifier_lan.const import ( + DOMAIN, + UNIQUE_DEHUMIDIFIER_PREFIX, +) +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.hub import Hub +from custom_components.midea_dehumidifier_lan.util import is_enabled_by_capabilities + + +def _is_enabled(coordinator: ApplianceUpdateCoordinator, capability: str) -> bool: + return is_enabled_by_capabilities( + coordinator.appliance.state.capabilities, capability + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up appliance binary sensors""" + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + # Dehumidifier sensors + async_add_entities( + TankFullSensor(c) for c in hub.coordinators if c.is_dehumidifier() + ) + # Add tank removed sensor if pump is supported + async_add_entities( + TankRemovedSensor(c) + for c in hub.coordinators + if c.is_dehumidifier() and _is_enabled(c, "pump") + ) + async_add_entities( + FilterReplacementSensor(c) + for c in hub.coordinators + if c.is_dehumidifier() and _is_enabled(c, "filter") + ) + async_add_entities( + DefrostingSensor(c) for c in hub.coordinators if c.is_dehumidifier() + ) + + +class TankFullSensor(ApplianceEntity, BinarySensorEntity): + """ + Describes full tank binary sensors (indicated as problem as it prevents + dehumidifier from operating) + """ + + _attr_device_class = BinarySensorDeviceClass.PROBLEM + _name_suffix = " Tank Full" + + def on_update(self) -> None: + self._attr_is_on = ( + self.dehumidifier().tank_full + or self.dehumidifier().error_code == ERROR_CODE_BUCKET_FULL + ) + + +class TankRemovedSensor(ApplianceEntity, BinarySensorEntity): + """ + Shows that tank has been removed binary sensors (indicated as problem as it prevents + dehumidifier from operating) + """ + + _attr_device_class = BinarySensorDeviceClass.PROBLEM + _name_suffix = " Tank Removed" + _capability_attr = "pump" + + def on_update(self) -> None: + self._attr_is_on = self.dehumidifier().error_code == ERROR_CODE_BUCKET_REMOVED + + +class FilterReplacementSensor(ApplianceEntity, BinarySensorEntity): + """ + Describes filter replacement binary sensors (indicated as problem) + """ + + _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_registry_enabled_default = False + _name_suffix = " Replace Filter" + _capability_attr = "filter" + + @property + def unique_id_prefix(self) -> str: + """Prefix for entity id""" + return f"{UNIQUE_DEHUMIDIFIER_PREFIX}filter_" + + def on_update(self) -> None: + self._attr_is_on = self.dehumidifier().filter_indicator + + +class DefrostingSensor(ApplianceEntity, BinarySensorEntity): + """ + Describes defrosting mode binary sensors (indicated as cold) + """ + + _attr_device_class = BinarySensorDeviceClass.COLD + _attr_entity_registry_enabled_default = False + _name_suffix = " Defrosting" + + def on_update(self) -> None: + self._attr_is_on = self.dehumidifier().defrosting diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/climate.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/climate.py new file mode 100644 index 0000000..7c80cf5 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/climate.py @@ -0,0 +1,254 @@ +"""Adds climate entity for each air conditioner appliance.""" + +import logging +from typing import Final + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_SWING_MODE, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + PRESET_SLEEP, + PRESET_AWAY, + PRESET_COMFORT, + ClimateEntityFeature, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, +) +from custom_components.midea_dehumidifier_lan.const import ( + ATTR_RUNNING, + DOMAIN, + MAX_TARGET_TEMPERATURE, + MIN_TARGET_TEMPERATURE, +) +from custom_components.midea_dehumidifier_lan.hub import Hub + +_LOGGER = logging.getLogger(__name__) + + +FAN_SILENT = "Silent" +FAN_FULL = "Full" + +HVAC_MODES: Final = [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.FAN_ONLY, +] +FAN_MODES: Final = [ + FAN_SILENT, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_FULL, + FAN_AUTO, +] + +SWING_MODES: Final = [SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL, SWING_BOTH] + +PRESET_MODES: Final = [PRESET_NONE, PRESET_ECO, PRESET_BOOST, PRESET_SLEEP, PRESET_AWAY, PRESET_COMFORT] + +_FAN_SPEEDS = { + FAN_AUTO: 102, + FAN_FULL: 100, + FAN_HIGH: 80, + FAN_MEDIUM: 60, + FAN_LOW: 40, + FAN_SILENT: 20, +} + +_MODES_TO_MIDEA = { + HVACMode.AUTO: 1, + HVACMode.COOL: 2, + HVACMode.DRY: 3, + HVACMode.HEAT: 4, + HVACMode.FAN_ONLY: 5, +} + +_MIDEA_TO_MODES = { + 1: HVACMode.AUTO, + 2: HVACMode.COOL, + 3: HVACMode.DRY, + 4: HVACMode.HEAT, + 5: HVACMode.FAN_ONLY, +} + +_HVAC_ACTIONS = { + HVACMode.OFF: HVACAction.OFF, + HVACMode.AUTO: None, + HVACMode.COOL: HVACAction.COOLING, + HVACMode.DRY: HVACAction.DRYING, + HVACMode.HEAT: HVACAction.HEATING, + HVACMode.FAN_ONLY: HVACAction.FAN, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up air conditioner entites""" + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + AirConditionerEntity(c) for c in hub.coordinators if c.is_climate() + ) + + +class AirConditionerEntity(ApplianceEntity, ClimateEntity): + """Climate entity for Midea air conditioner""" + + _attr_hvac_modes = HVAC_MODES + _attr_fan_modes = FAN_MODES + _attr_preset_modes = PRESET_MODES + _attr_swing_modes = SWING_MODES + _attr_max_temp = MAX_TARGET_TEMPERATURE + _attr_min_temp = MIN_TARGET_TEMPERATURE + _attr_precision = PRECISION_HALVES + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) + + _name_suffix = "" + _add_extra_attrs = True + + def on_update(self) -> None: + aircon = self.airconditioner() + self._attr_current_temperature = aircon.indoor_temperature + self._attr_target_temperature = aircon.target_temperature + self._attr_fan_mode = self._fan_mode() + self._attr_preset_mode = self._preset_mode() + self._attr_swing_mode = self._swing_mode() + self._attr_hvac_mode = self._hvac_mode() + self._attr_hvac_action = _HVAC_ACTIONS.get(self._attr_hvac_mode) + super().on_update() + + def _fan_mode(self) -> str: + fan_speed = self.airconditioner().fan_speed + for mode, mode_speed in _FAN_SPEEDS.items(): + if fan_speed <= mode_speed: + return mode + return FAN_AUTO + + def _preset_mode(self) -> str: + if self.airconditioner().turbo: + return PRESET_BOOST + if self.airconditioner().eco_mode: + return PRESET_ECO + if self.airconditioner().comfort_sleep: + return PRESET_SLEEP + if self.airconditioner().frost_protect: + return PRESET_AWAY + if self.airconditioner().comfort_mode: + return PRESET_COMFORT + return PRESET_NONE + + def _swing_mode(self) -> str: + if self.airconditioner().vertical_swing: + if self.airconditioner().horizontal_swing: + return SWING_BOTH + return SWING_VERTICAL + if self.airconditioner().horizontal_swing: + return SWING_HORIZONTAL + return SWING_OFF + + def _hvac_mode(self) -> str: + if not self.airconditioner().running: + return HVACMode.OFF + + curr_mode = self.airconditioner().mode + mode = _MIDEA_TO_MODES.get(curr_mode) + if mode is None: + mode = HVACMode.AUTO + _LOGGER.warning("Unknown mode %d, reporting %s", curr_mode, mode) + + return mode + + def turn_on(self, **kwargs) -> None: # pylint: disable=unused-argument + """Turn the entity on.""" + self.apply(ATTR_RUNNING, True) + + def turn_off(self, **kwargs) -> None: # pylint: disable=unused-argument + """Turn the entity off.""" + self.apply(ATTR_RUNNING, False) + + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVACMode.OFF: + self.turn_off() + return + midea_mode = _MODES_TO_MIDEA.get(hvac_mode) + if midea_mode is None: + _LOGGER.warning("Unsupported climate mode %s", hvac_mode) + return + # Make sure we are running + if not self.airconditioner().running: + self.turn_on() + self.apply("mode", midea_mode) + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if kwargs.get(ATTR_TEMPERATURE): + self.apply("target_temperature", kwargs.get(ATTR_TEMPERATURE)) + if kwargs.get(ATTR_HVAC_MODE): + self.set_hvac_mode(kwargs.get(ATTR_HVAC_MODE)) + if kwargs.get(ATTR_SWING_MODE): + self.set_swing_mode(kwargs.get(ATTR_SWING_MODE)) + if kwargs.get(ATTR_FAN_MODE): + self.set_fan_mode(kwargs.get(ATTR_FAN_MODE)) + + def set_swing_mode(self, swing_mode: str) -> None: + if swing_mode == SWING_VERTICAL: + self.apply(vertical_swing=True, horizontal_swing=False) + elif swing_mode == SWING_HORIZONTAL: + self.apply(vertical_swing=False, horizontal_swing=True) + elif swing_mode == SWING_BOTH: + self.apply(vertical_swing=True, horizontal_swing=True) + else: + self.apply(vertical_swing=False, horizontal_swing=False) + + def set_fan_mode(self, fan_mode: str) -> None: + self.apply(fan_speed=_FAN_SPEEDS.get(fan_mode, 20)) + + def set_preset_mode(self, preset_mode: str) -> None: + if preset_mode == PRESET_BOOST: + self.apply(turbo=True, eco_mode=False, comfort_sleep=False, frost_protect=False, comfort_mode=False) + elif preset_mode == PRESET_ECO: + self.apply(turbo=False, eco_mode=True, comfort_sleep=False, frost_protect=False, comfort_mode=False) + elif preset_mode == PRESET_SLEEP: + self.apply(turbo=False, eco_mode=False, comfort_sleep=True, frost_protect=False, comfort_mode=False) + elif preset_mode == PRESET_AWAY: + self.apply(turbo=False, eco_mode=False, comfort_sleep=False, frost_protect=True, comfort_mode=False) + elif preset_mode == PRESET_SLEEP: + self.apply(turbo=False, eco_mode=False, comfort_sleep=False, frost_protect=False, comfort_mode=True) + else: + self.apply(turbo=False, eco_mode=False, comfort_sleep=False, frost_protect=False, comfort_mode=False) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/config_flow.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/config_flow.py new file mode 100644 index 0000000..4130440 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/config_flow.py @@ -0,0 +1,727 @@ +"""Config flow for Midea Air Appliance (Local) integration.""" +from __future__ import annotations + +from ipaddress import IPv4Address, IPv4Network +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import ( + ATTR_ID, + ATTR_NAME, + CONF_API_VERSION, + CONF_BROADCAST_ADDRESS, + CONF_DEVICES, + CONF_DISCOVERY, + CONF_ID, + CONF_INCLUDE, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_TTL, + CONF_TYPE, + CONF_UNIQUE_ID, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowHandler, FlowResult +from homeassistant.helpers import config_validation as cv +import voluptuous as vol + +from midea_beautiful.cloud import MideaCloud +from midea_beautiful.exceptions import ( + AuthenticationError, + CloudAuthenticationError, + CloudError, + MideaError, + MideaNetworkError, + ProtocolError, + RetryLaterError, +) +from midea_beautiful.lan import LanDevice +from midea_beautiful.midea import ( + APPLIANCE_TYPE_DEHUMIDIFIER, + SUPPORTED_APPS, +) + +from custom_components.midea_dehumidifier_lan import Hub +from custom_components.midea_dehumidifier_lan.const import ( + NAME, + CURRENT_CONFIG_VERSION, + SUPPORTED_APPLIANCES, + CONF_ADVANCED_SETTINGS, + CONF_DEBUG, + CONF_MOBILE_APP, + CONF_TOKEN_KEY, + DEFAULT_APP, + DEFAULT_DISCOVERY_MODE, + DEFAULT_PASSWORD, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TTL, + DEFAULT_USERNAME, + DISCOVERY_CLOUD, + DISCOVERY_IGNORE, + DISCOVERY_LAN, + DISCOVERY_MODE_LABELS, + DISCOVERY_WAIT, + DOMAIN, + LOCAL_BROADCAST, + UNKNOWN_IP, +) +from custom_components.midea_dehumidifier_lan.util import ( + MideaClient, + RedactedConf, + address_ok, + supported_appliance, +) + +_LOGGER = logging.getLogger(__name__) + + +def _appliance_schema( # pylint: disable=too-many-arguments + name: str, + address: str = UNKNOWN_IP, + ttl: int = DEFAULT_TTL, + token: str = "", + token_key: str = "", + discovery_mode=DISCOVERY_WAIT, +) -> vol.Schema: + return vol.Schema( + { + vol.Optional(CONF_DISCOVERY, default=str(discovery_mode)): vol.In( + DISCOVERY_MODE_LABELS + ), + vol.Optional( + CONF_IP_ADDRESS, + default=address or UNKNOWN_IP, + ): cv.string, + vol.Required(CONF_NAME, default=name): cv.string, + vol.Required( + CONF_TTL, + msg="Test", + default=ttl, + description={"suffix": "minutes"}, + ): cv.positive_int, + vol.Optional(CONF_TOKEN, default=token or ""): cv.string, + vol.Optional(CONF_TOKEN_KEY, default=token_key or ""): cv.string, + } + ) + + +# pylint: disable=too-many-arguments +def _advanced_settings_schema( + username: str = "", + password: str = "", + app: str = DEFAULT_APP, + broadcast_address: str = "", + appliances: list[str] = None, + debug: bool = False, +) -> vol.Schema: + appliances = appliances or [APPLIANCE_TYPE_DEHUMIDIFIER] + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=username): cv.string, + vol.Required(CONF_PASSWORD, default=password): cv.string, + vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()), + vol.Optional(CONF_BROADCAST_ADDRESS, default=broadcast_address): cv.string, + vol.Required( + CONF_SCAN_INTERVAL, + msg="Test", + default=DEFAULT_SCAN_INTERVAL, + description={"suffix": "minutes"}, + ): cv.positive_int, + vol.Required(CONF_INCLUDE, default=appliances): vol.All( + cv.multi_select(SUPPORTED_APPLIANCES), + vol.Length(min=1, msg="Must select at least one appliance category"), + ), + vol.Required(CONF_DEBUG, default=debug): bool, + } + ) + + +def _reauth_schema( + username: str, + password: str, +) -> vol.Schema: + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=username): cv.string, + vol.Required(CONF_PASSWORD, default=password): cv.string, + } + ) + + +def _user_schema(username: str, password: str, app: str) -> vol.Schema: + + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=username): cv.string, + vol.Required(CONF_PASSWORD, default=password): cv.string, + vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()), + vol.Required(CONF_ADVANCED_SETTINGS, default=False): bool, + } + ) + + +# pylint: disable=too-many-instance-attributes +class _MideaFlow(FlowHandler): + """Base class for Midea data flows""" + + def __init__(self) -> None: + super().__init__() + self.appliance_idx = -1 + self.appliances: list[LanDevice] = [] + self._client: MideaClient | None = None + self.cloud: MideaCloud | None = None # type: ignore + self.conf = {} + self.config_entry: ConfigEntry | None = None + self.devices_conf: list[dict[str, Any]] = [] + self.discovered_appliances: list[LanDevice | None] = [] + self.error_cause: str = "" + self.errors: dict[str, Any] = {} + self.indexes_to_process = [] + + @property + def client(self) -> MideaClient: + """Returns instance of MideaClient.""" + if not self._client: + self._client = MideaClient(self.hass) + return self._client + + def _process_exception(self: _MideaFlow, ex: Exception) -> None: + if isinstance(ex, _FlowException): + _LOGGER.warning( + "Caught flow exception during appliance step %s", ex, exc_info=True + ) + self.error_cause = str(ex.cause) + self.errors["base"] = ex.message + elif isinstance(ex, CloudAuthenticationError): + self.error_cause = f"{ex.error_code} - {ex.message}" + self.errors["base"] = "invalid_auth" + elif isinstance(ex, CloudError): + self.error_cause = f"{ex.error_code} - {ex.message}" + self.errors["base"] = "midea_client" + elif isinstance(ex, RetryLaterError): + self.error_cause = f"{ex.error_code} - {ex.message}" + self.errors["base"] = "retry_later" + elif isinstance(ex, MideaError): + self.error_cause = f"{ex.message}" + self.errors["base"] = "midea_client" + else: + raise ex + + def _connect_to_cloud(self: _MideaFlow, extra_conf: dict[str, Any] = None) -> None: + """Validates that cloud credentials are valid""" + cfg = self.conf | (extra_conf or {}) + try: + self.cloud = self.client.connect_to_cloud(cfg) + except MideaError as ex: + raise _FlowException("no_cloud", str(ex)) from ex + + def _validate_appliance( + self: _MideaFlow, appliance: LanDevice, device_conf: dict + ) -> LanDevice | None: + """ + Validates that appliance configuration is correct and matches physical + device + """ + discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE) + if discovery_mode == DISCOVERY_IGNORE: + _LOGGER.debug("Ignoring appliance %s", appliance) + return None + if discovery_mode == DISCOVERY_WAIT: + _LOGGER.debug( + "Attempt to discover appliance %s will be made later", + appliance, + ) + return None + try: + if discovery_mode == DISCOVERY_CLOUD: + discovered = self.client.appliance_state( + appliance_id=appliance.appliance_id, + cloud=self.cloud, + use_cloud=True, + ) + else: # DISCOVERY_LAN + ip_address = appliance.address + if not address_ok(ip_address): + raise _FlowException("invalid_ip_address", ip_address) + try: + IPv4Address(ip_address) + except Exception as ex: + _LOGGER.debug("Invalid appliance address %s: %s", ip_address, ex) + raise _FlowException("invalid_ip_address", ip_address) from ex + discovered = self.client.appliance_state( + address=ip_address, cloud=self.cloud + ) + except ProtocolError as ex: + raise _FlowException("connection_error", str(ex)) from ex + except AuthenticationError as ex: + raise _FlowException("invalid_auth", str(ex)) from ex + except MideaNetworkError as ex: + raise _FlowException("cannot_connect", str(ex)) from ex + except MideaError as ex: + raise _FlowException("not_discovered", str(ex)) from ex + if discovered is None: + raise _FlowException("not_discovered", appliance.address) + return discovered + + async def _async_add_entry(self: _MideaFlow) -> FlowResult: + supported_devices_conf = [] + for i, appliance in enumerate(self.appliances): + if not supported_appliance(self.conf, appliance): + continue + device_conf = self.devices_conf[i] + + if device_conf.get(CONF_DISCOVERY) != DISCOVERY_IGNORE: + device_conf |= { + CONF_API_VERSION: appliance.version, + CONF_ID: appliance.appliance_id, + CONF_IP_ADDRESS: ( + appliance.address or device_conf[CONF_IP_ADDRESS] or UNKNOWN_IP + ), + CONF_NAME: appliance.name, + CONF_TOKEN_KEY: appliance.key, + CONF_TOKEN: appliance.token, + CONF_TYPE: appliance.type, + CONF_UNIQUE_ID: appliance.serial_number, + } + suggested_discovery = ( + DISCOVERY_LAN + if address_ok(device_conf[CONF_IP_ADDRESS]) + else DISCOVERY_WAIT + ) + device_conf.get(CONF_DISCOVERY, suggested_discovery) + supported_devices_conf.append(device_conf) + self.devices_conf = supported_devices_conf + self.conf[CONF_DEVICES] = self.devices_conf + + # Remove not used elements + self.conf.pop(CONF_ADVANCED_SETTINGS, None) + if self.config_entry: + _LOGGER.debug("Updating configuration data %s", RedactedConf(self.conf)) + self.hass.config_entries.async_update_entry( + entry=self.config_entry, data=self.conf + ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + + if not self.devices_conf: + _LOGGER.debug("No configured appliances %s", RedactedConf(self.conf)) + return self.async_abort(reason="no_configured_devices") + _LOGGER.debug("Creating configuration data %s", RedactedConf(self.conf)) + return self.async_create_entry(title=NAME, data=self.conf) + + async def _async_step_appliance( # pylint: disable=too-many-locals + self: _MideaFlow, + step_id: str, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Manage an appliances""" + + self.errors.clear() + self.error_cause = "" + appliance = self.appliances[self.appliance_idx] + device_conf = self.devices_conf[self.appliance_idx] + discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE) + ttl = device_conf.get(CONF_TTL, DEFAULT_TTL) + ip_address = appliance.address or UNKNOWN_IP + if user_input is not None: + try: + + ip_address = user_input.get( + CONF_IP_ADDRESS, device_conf.get(CONF_IP_ADDRESS, UNKNOWN_IP) + ) + self._check_ip_address_unique(ip_address) + + discovery_mode = user_input.get(CONF_DISCOVERY, discovery_mode) + if discovery_mode not in [ + DISCOVERY_WAIT, + DISCOVERY_LAN, + DISCOVERY_IGNORE, + DISCOVERY_CLOUD, + ]: + discovery_mode = ( + DISCOVERY_LAN if address_ok(ip_address) else DISCOVERY_CLOUD + ) + device_conf[CONF_DISCOVERY] = discovery_mode + device_conf[CONF_TTL] = user_input.get(CONF_TTL, ttl) + appliance.address = ip_address + appliance.name = user_input.get(CONF_NAME, appliance.name) + appliance.token = user_input.get(CONF_TOKEN, "") + appliance.key = user_input.get(CONF_TOKEN_KEY, "") + + if not self.cloud: + await self.hass.async_add_executor_job(self._connect_to_cloud) + + discovered = await self.hass.async_add_executor_job( + self._validate_appliance, + appliance, + device_conf, + ) + self.discovered_appliances[self.appliance_idx] = discovered + + if not self.indexes_to_process: + self._update_appliances_after_flow() + + return await self._async_add_entry() + + self.appliance_idx = self.indexes_to_process.pop(0) + appliance = self.appliances[self.appliance_idx] + device_conf = self.devices_conf[self.appliance_idx] + ip_address = appliance.address or UNKNOWN_IP + user_input = None + discovery_mode = DEFAULT_DISCOVERY_MODE + ttl = DEFAULT_TTL + + except Exception as ex: # pylint: disable=broad-except + self._process_exception(ex) + + name = appliance.name + extra = { + "index": str(self.appliance_idx + 1), + "count": str(len(self.appliances)), + "serial_number": appliance.serial_number, + } + placeholders = self._placeholders(appliance, extra) + schema_arg = { + "name": name, + "address": device_conf.get(CONF_IP_ADDRESS, ip_address), + "token": device_conf.get(CONF_TOKEN, appliance.token), + "token_key": device_conf.get(CONF_TOKEN_KEY, appliance.key), + "ttl": device_conf.get(CONF_TTL, ttl), + "discovery_mode": device_conf.get(CONF_DISCOVERY, discovery_mode), + } + schema = _appliance_schema(**schema_arg) + return self.async_show_form( + step_id=step_id, + data_schema=schema, + description_placeholders=placeholders, + errors=self.errors, + last_step=len(self.indexes_to_process) == 0, + ) + + def _check_ip_address_unique(self, ip_address) -> None: + if address_ok(ip_address): + for i in range(self.appliance_idx): + if ( + self.devices_conf[i].get(CONF_IP_ADDRESS) == ip_address + or ip_address == self.appliances[i].address + ): + raise _FlowException( + "duplicate_ip_provided", self.appliances[i].name + ) + + def _update_appliances_after_flow(self) -> None: + for i, discovered in enumerate(self.discovered_appliances): + if discovered: + old_address = self.appliances[i].address + self.appliances[i].update(discovered) + if not discovered.address: + self.appliances[i].address = old_address + + def _placeholders( + self: _MideaFlow, appliance: LanDevice = None, extra: dict[str, str] = None + ) -> dict[str, str]: + extra = extra or {} + placeholders = { + "cause": self.error_cause or "", + **extra, + } + if appliance: + placeholders[ATTR_ID] = ( + appliance.serial_number or f"{appliance.appliance_id} (Missing S/N)" + ) + placeholders[ATTR_NAME] = appliance.name + + return placeholders + + +def _get_broadcast_addresses(user_input: dict[str, Any]) -> list[str]: + address_entry = str(user_input.get(CONF_BROADCAST_ADDRESS, "")) + addresses = [LOCAL_BROADCAST] + specified_addresses = [ + addr.strip() for addr in address_entry.split(",") if addr.strip() + ] + for addr in specified_addresses: + _LOGGER.debug("Trying IPv4 %s", addr) + try: + IPv4Network(addr) + addresses.append(addr) + except ValueError as ex: + raise _FlowException("invalid_ip_range", str(ex)) from ex + except Exception as ex: + _LOGGER.debug("Invalid IP address %s", addr, exc_info=True) + raise _FlowException("invalid_ip_range", addr) from ex + return addresses + + +class _FlowException(Exception): + def __init__(self, message, cause: str = None) -> None: + super().__init__() + self.message = message + self.cause = cause + + +# pylint: disable=too-many-instance-attributes +class MideaConfigFlow(ConfigFlow, _MideaFlow, domain=DOMAIN): + """Configuration flow for Midea dehumidifiers on local network uses + discovery based on Midea cloud, so it first requires credentials for it. + If some appliances are registered in the cloud, but not discovered, configuration + flow will prompt for additional information. + """ + + VERSION = CURRENT_CONFIG_VERSION + + def __init__(self) -> None: + super().__init__() + self.discovered_appliances: list[LanDevice | None] = [] + self.appliances: list[LanDevice] = [] + self.config_entry: ConfigEntry | None = None + self.advanced_settings = False + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Define the config flow to handle options.""" + return MideaOptionsFlow(config_entry) + + def _connect_and_discover(self: MideaConfigFlow) -> None: + """Validates that cloud credentials are valid and discovers local appliances""" + + self._connect_to_cloud() + conf_addresses = self.conf.get(CONF_BROADCAST_ADDRESS, []) + if isinstance(conf_addresses, str): + conf_addresses = [conf_addresses] + addresses = [ + str(IPv4Network(addr).broadcast_address) for addr in conf_addresses + ] + self.appliances.clear() + self.appliances += self.client.find_appliances(self.cloud, addresses) + self.devices_conf = [{} for _ in self.appliances] + + async def _validate_discovery_phase( + self, user_input: dict[str, Any] | None + ) -> FlowResult: + assert user_input is not None + self.conf[CONF_USERNAME] = user_input[CONF_USERNAME] + self.conf[CONF_PASSWORD] = user_input[CONF_PASSWORD] + + if self.advanced_settings: + assert self.conf is not None + self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP) + self.conf[CONF_INCLUDE] = user_input[CONF_INCLUDE] + self.conf[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL] + self.conf[CONF_DEBUG] = user_input[CONF_DEBUG] + self.conf[CONF_BROADCAST_ADDRESS] = _get_broadcast_addresses(user_input) + + else: + self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP) + if user_input.get(CONF_ADVANCED_SETTINGS): + return await self.async_step_advanced_settings() + + self.conf[CONF_BROADCAST_ADDRESS] = [] + self.conf[CONF_INCLUDE] = [APPLIANCE_TYPE_DEHUMIDIFIER] + self.conf[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL + + if self.conf.get(CONF_DEBUG, False): + await self.client.async_debug_mode(True) + await self.hass.async_add_executor_job(self._connect_and_discover) + + self.indexes_to_process = [ + index + for index, appliance in enumerate(self.appliances) + if supported_appliance(self.conf, appliance) + and not address_ok(appliance.address) + ] + if self.indexes_to_process: + self.appliance_idx = self.indexes_to_process.pop(0) + self.discovered_appliances = [None] * len(self.devices_conf) + return await self.async_step_unreachable_appliance() + + return await self._async_add_entry() + + async def _do_validate(self, user_input: dict[str, Any]) -> FlowResult | None: + try: + return await self._validate_discovery_phase(user_input) + except Exception as ex: # pylint: disable=broad-except + self._process_exception(ex) + return None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + self.advanced_settings = False + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + self.errors.clear() + self.error_cause = "" + + username = DEFAULT_USERNAME + password = DEFAULT_PASSWORD + app = DEFAULT_APP + if user_input is not None: + username = user_input.get(CONF_USERNAME, username) + password = user_input.get(CONF_PASSWORD, password) + app = user_input.get(CONF_MOBILE_APP, app) + res = await self._do_validate(user_input) + if res: + return res + + return self.async_show_form( + step_id="user", + data_schema=_user_schema(username=username, password=password, app=app), + description_placeholders=self._placeholders(), + errors=self.errors, + ) + + async def async_step_advanced_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Step for managing advanced settings""" + self.errors = {} + self.error_cause = "" + self.advanced_settings = True + if user_input is not None: + if res := await self._do_validate(user_input): + return res + else: + user_input = {} + + username = user_input.get( + CONF_USERNAME, self.conf.get(CONF_USERNAME, DEFAULT_USERNAME) + ) + password = user_input.get( + CONF_PASSWORD, self.conf.get(CONF_PASSWORD, DEFAULT_PASSWORD) + ) + app = user_input.get(CONF_MOBILE_APP, DEFAULT_APP) + broadcast_addresses = user_input.get( + CONF_BROADCAST_ADDRESS, ",".join(self.conf.get(CONF_BROADCAST_ADDRESS, [])) + ) + + return self.async_show_form( + step_id="advanced_settings", + data_schema=_advanced_settings_schema( + username=username, + password=password, + app=app, + broadcast_address=broadcast_addresses, + ), + description_placeholders=self._placeholders(), + errors=self.errors, + ) + + async def async_step_unreachable_appliance( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the appliances that were not discovered automatically on LAN.""" + + return await self._async_step_appliance( + step_id="unreachable_appliance", + user_input=user_input, + ) + + async def _async_add_entry(self) -> FlowResult: + assert self.conf is not None + self.config_entry = await self.async_set_unique_id(self.conf[CONF_USERNAME]) + return await super()._async_add_entry() + + async def async_step_reauth(self, config) -> FlowResult: + """Handle reauthorization request from Abode.""" + self.conf = {**config} + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauthorization flow.""" + self.errors.clear() + password = "" + username = self.conf.get(CONF_USERNAME, "") + app = self.conf.get(CONF_MOBILE_APP, DEFAULT_APP) + if user_input is not None: + extra_conf = { + CONF_USERNAME: user_input.get(CONF_USERNAME, ""), + CONF_PASSWORD: user_input.get(CONF_PASSWORD, ""), + CONF_MOBILE_APP: user_input.get(CONF_MOBILE_APP, app), + } + try: + await self.hass.async_add_executor_job( + self._connect_to_cloud, extra_conf + ) + except Exception as ex: # pylint: disable=broad-except + self._process_exception(ex) + else: + self.conf[CONF_USERNAME] = username + self.conf[CONF_PASSWORD] = password + self.conf[CONF_MOBILE_APP] = app + return await self._async_add_entry() + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=_reauth_schema( + username=username, + password=password, + ), + description_placeholders=self._placeholders(), + errors=self.errors, + ) + + +class MideaOptionsFlow(OptionsFlow, _MideaFlow): + """Handle Midea options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize Midea options flow.""" + super().__init__() + self.config_entry = config_entry + self.conf = {**config_entry.data} + self.devices_conf = self.conf.get(CONF_DEVICES, []) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Starts options flow""" + self._build_appliance_list() + return await self.async_step_appliance(user_input) + + async def async_step_appliance( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Options for an appliance""" + + return await self._async_step_appliance( + step_id="appliance", + user_input=user_input, + ) + + def _build_appliance_list(self) -> None: + assert self.config_entry + hub: Hub = self.hass.data[DOMAIN][self.config_entry.entry_id] + self.appliances.clear() + self.devices_conf = self.conf[CONF_DEVICES] + for device in self.devices_conf: + for coord in hub.coordinators: + if device[CONF_UNIQUE_ID] == coord.appliance.serial_number: + self.appliances.append(coord.appliance) + break + else: + appliance = LanDevice( + appliance_id=device[CONF_ID], + serial_number=device[CONF_UNIQUE_ID], + appliance_type=device[CONF_TYPE], + ) + appliance.name = device[CONF_NAME] + appliance.address = device.get(CONF_IP_ADDRESS, UNKNOWN_IP) + self.appliances.append(appliance) + self.indexes_to_process = list(range(len(self.appliances))) + self.appliance_idx = self.indexes_to_process.pop(0) + self.discovered_appliances = [None] * len(self.devices_conf) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/const.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/const.py new file mode 100644 index 0000000..6f1b654 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/const.py @@ -0,0 +1,111 @@ +"""Constants for Midea Air Appliance custom component""" +from __future__ import annotations + +from typing import Final + +from homeassistant.const import Platform + +from midea_beautiful.midea import ( + APPLIANCE_TYPE_AIRCON, + APPLIANCE_TYPE_DEHUMIDIFIER, + DEFAULT_APP as DEFAULT_APP_FROM_LIB, +) + +__version__ = "0.9.6" + +# Base component constants +NAME: Final = "Midea Air Appliance (LAN)" +UNIQUE_ID_PRE_PREFIX: Final = "midea_" +UNIQUE_DEHUMIDIFIER_PREFIX: Final = "midea_dehumidifier_" +UNIQUE_CLIMATE_PREFIX: Final = "midea_climate_" +DOMAIN: Final = f"{UNIQUE_DEHUMIDIFIER_PREFIX}lan" +# pylint: disable=line-too-long +ISSUE_URL: Final = "https://github.com/nbogojevic/homeassistant-midea-air-appliances-lan/issues/new/choose" # noqa: E501 + +CONF_ADVANCED_SETTINGS: Final = "advanced_settings" +OBSOLETE_CONF_APPID: Final = "appid" +OBSOLETE_CONF_APPKEY: Final = "appkey" +CONF_DEBUG: Final = "debug" +CONF_MOBILE_APP: Final = "mobile_app" +CONF_TOKEN_KEY: Final = "token_key" +CONF_USE_CLOUD_OBSOLETE: Final = "use_cloud" + +MAX_TARGET_HUMIDITY: Final = 85 +MIN_TARGET_HUMIDITY: Final = 35 + +MAX_TARGET_TEMPERATURE: Final = 32 +MIN_TARGET_TEMPERATURE: Final = 16 + +CURRENT_CONFIG_VERSION: Final = 3 + +# Wait half a second between successive refresh calls +APPLIANCE_REFRESH_COOLDOWN: Final = 0.5 +APPLIANCE_REFRESH_INTERVAL: Final = 60 +DEFAULT_SCAN_INTERVAL: Final = 15 +MIN_SCAN_INTERVAL: Final = 2 + +ATTR_FAN_SPEED: Final = "fan_speed" +ATTR_RUNNING: Final = "running" + +PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.SENSOR, + Platform.SWITCH, +] + +ENTITY_DISABLED_BY_DEFAULT: Final = ":disabled:" +ENTITY_ENABLED_BY_DEFAULT: Final = ":enabled:" +_ALWAYS_CREATE: Final = [ENTITY_DISABLED_BY_DEFAULT, ENTITY_ENABLED_BY_DEFAULT] + +UNKNOWN_IP: Final = "0.0.0.0" +LOCAL_BROADCAST: Final = "255.255.255.255" + +# What to do with configured appliance +DISCOVERY_IGNORE = "IGNORE" +DISCOVERY_LAN = "LAN" +DISCOVERY_CLOUD = "CLOUD" +DISCOVERY_WAIT = "WAIT" +DEFAULT_DISCOVERY_MODE = DISCOVERY_LAN + +DISCOVERY_BATCH_SIZE: Final = 64 + +DEFAULT_APP: Final = DEFAULT_APP_FROM_LIB + +DEFAULT_USERNAME: Final = "" +DEFAULT_PASSWORD: Final = "" + +STARTUP_MESSAGE: Final = f""" +------------------------------------------------------------------- +{NAME} +Version: {__version__} +This is a custom integration! +If you have any issues with this you need to open an issue here: +{ISSUE_URL} +------------------------------------------------------------------- +""" + +DISCOVERY_MODE_LABELS = { + DISCOVERY_IGNORE: "Exclude appliance", + DISCOVERY_LAN: "Provide appliance's IPv4 address", + DISCOVERY_WAIT: "Wait for appliance to come online", + DISCOVERY_CLOUD: "Use cloud API to poll appliance", +} + +DISCOVERY_MODE_EXPLANATION = { + DISCOVERY_IGNORE: "excluded from polling", + DISCOVERY_LAN: "assigned local network address", + DISCOVERY_WAIT: "waiting to be disovered", + DISCOVERY_CLOUD: "polled using cloud", +} + +SUPPORTED_APPLIANCES = { + APPLIANCE_TYPE_AIRCON: "Air conditioner (BETA)", + APPLIANCE_TYPE_DEHUMIDIFIER: "Dehumidifier", +} + +# Default period of failed updates before appliance is declared unavailable +# 5 minutes +DEFAULT_TTL: Final = 5 diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/fan.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/fan.py new file mode 100644 index 0000000..faaf1a1 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/fan.py @@ -0,0 +1,143 @@ +"""Adds fan entity for each dehumidifer appliance.""" + +import logging +from typing import Any, Final + +from homeassistant.components.fan import ( + FanEntityFeature, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.midea_dehumidifier_lan.const import ATTR_FAN_SPEED, DOMAIN +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.hub import Hub + +_LOGGER = logging.getLogger(__name__) + +MODE_NONE: Final = "None" +MODE_AUTO: Final = "Auto" +MODE_LOW: Final = "Low" +MODE_MEDIUM: Final = "Medium" +MODE_HIGH: Final = "High" + +PRESET_MODES_7: Final = [MODE_LOW, MODE_MEDIUM, MODE_HIGH] +PRESET_MODES_3: Final = [MODE_LOW, MODE_HIGH] +PRESET_MODES_2: Final = [MODE_AUTO] + +_FAN_SPEEDS = {2: PRESET_MODES_2, 3: PRESET_MODES_3, 7: PRESET_MODES_7} +_ON_SPEED = {2: MODE_AUTO, 3: MODE_HIGH, 7: MODE_HIGH} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up fan entity for dehumidifer""" + + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + DehumidiferFan(c) for c in hub.coordinators if c.is_dehumidifier() + ) + + +# pylint: disable=too-many-ancestors +class DehumidiferFan(ApplianceEntity, FanEntity): + """Entity for managing dehumidifer fan""" + + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE | + FanEntityFeature.TURN_OFF | + FanEntityFeature.TURN_ON + ) + + _attr_preset_modes = PRESET_MODES_7 + _attr_speed_count = len(PRESET_MODES_7) + _name_suffix = " Fan" + _on_speed = MODE_MEDIUM + + def __init__(self, coordinator: ApplianceUpdateCoordinator) -> None: + + super().__init__(coordinator) + self._fan_speeds = { + MODE_NONE: 0, + MODE_LOW: 40, + MODE_MEDIUM: 60, + MODE_HIGH: 80, + MODE_AUTO: 101, + } + + @property + def is_on(self): + # Override parent logic + return self._attr_is_on + + def on_online(self, update: bool) -> None: + supports = self.dehumidifier().capabilities + fan_capability = supports.get("fan_speed", 0) + self._attr_preset_modes = _FAN_SPEEDS.get(fan_capability, PRESET_MODES_7) + self._on_speed = _ON_SPEED.get(fan_capability, MODE_HIGH) + self._attr_speed_count = len(self._attr_preset_modes) + return super().on_online(update) + + def on_update(self) -> None: + fan_speed = self.dehumidifier().fan_speed + self._attr_percentage = fan_speed + self._attr_is_on = fan_speed > self._fan_speeds[MODE_LOW] + for mode, mode_speed in self._fan_speeds.items(): + if fan_speed <= mode_speed: + self._attr_preset_mode = mode + break + else: + self._attr_preset_mode = MODE_NONE + + def set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + speed = self._fan_speeds.get(preset_mode, None) + _LOGGER.debug("Setting speed to %s", speed) + if speed is not None: + self.apply(ATTR_FAN_SPEED, speed) + else: + _LOGGER.warning("Unsupported fan mode %s", preset_mode) + + def set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + _LOGGER.debug("Setting percentage to %s", percentage) + + self.apply(ATTR_FAN_SPEED, percentage) + + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turns fan to medium speed.""" + updated = False + if preset_mode is not None: + self.set_preset_mode(preset_mode) + updated = True + if percentage is not None: + self.set_percentage(percentage) + updated = True + if speed is not None: + self.set_speed(speed) + updated = True + # _LOGGER.debug("turn_on percentage=%s was_updated=%s", self._attr_percentage, updated) + if ( + not updated + and (self._attr_percentage or 0) < self._fan_speeds[self._on_speed] + ): + self.set_preset_mode(self._on_speed) + + def turn_off(self, **kwargs: Any) -> None: + """Turns fan to silent speed.""" + self.set_preset_mode(MODE_LOW) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/hub.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/hub.py new file mode 100644 index 0000000..e0bcdd7 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/hub.py @@ -0,0 +1,303 @@ +"""The custom component for local network access to Midea appliances +""" + +from __future__ import annotations + +import logging +from typing import Any, Tuple + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_VERSION, + CONF_DEVICES, + CONF_DISCOVERY, + CONF_ID, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_TYPE, + CONF_UNIQUE_ID, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from midea_beautiful.exceptions import AuthenticationError +from midea_beautiful.lan import LanDevice + +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.appliance_discovery import ( + ApplianceDiscoveryHelper, +) +from custom_components.midea_dehumidifier_lan.const import ( + CONF_TOKEN_KEY, + DISCOVERY_CLOUD, + DISCOVERY_IGNORE, + DISCOVERY_LAN, + DISCOVERY_WAIT, + NAME, + UNKNOWN_IP, +) +from custom_components.midea_dehumidifier_lan.util import ( + AbstractHub, + RedactedConf, + address_ok, +) + +_LOGGER = logging.getLogger(__name__) + + +def _assure_valid_device_configuration( + conf: dict[str, Any], device: dict[str, Any] +) -> bool: + """Checks device configuration. + If configuration is correct returns ``True``. + If it is not complete, updates it and returns ``False``. + For example, if discovery mode is not set-up corectly it will try to deduce + correct setting.""" + discovery_mode = device.get(CONF_DISCOVERY) + if discovery_mode in [ + DISCOVERY_IGNORE, + DISCOVERY_WAIT, + DISCOVERY_LAN, + DISCOVERY_CLOUD, + ]: + return True + ip_address = device.get(CONF_IP_ADDRESS) + token = device.get(CONF_TOKEN) + key = device.get(CONF_TOKEN_KEY) + if address_ok(ip_address): + device[CONF_DISCOVERY] = DISCOVERY_LAN if token and key else DISCOVERY_WAIT + elif token and key: + device[CONF_DISCOVERY] = DISCOVERY_WAIT + else: + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + device[CONF_DISCOVERY] = ( + DISCOVERY_CLOUD if username and password else DISCOVERY_IGNORE + ) + _LOGGER.warning( + "Updated discovery mode for device %s.", + RedactedConf(device), + ) + return False + + +def _get_placeholder_appliance(device: dict[str, Any]) -> LanDevice: + appliance = LanDevice( + appliance_id=device[CONF_ID], + serial_number=device[CONF_UNIQUE_ID], + appliance_type=device[CONF_TYPE], + token=device.get(CONF_TOKEN), + key=device.get(CONF_TOKEN_KEY) or "", + address=device.get(CONF_IP_ADDRESS, UNKNOWN_IP), + version=device.get(CONF_API_VERSION, 3), + ) + appliance.name = device[CONF_NAME] + return appliance + + +class Hub(AbstractHub): # pylint: disable=too-many-instance-attributes + """Central class for interacting with appliances""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + super().__init__(hass, config_entry) + self.discovery = ApplianceDiscoveryHelper(self) + self.coordinators: list[ApplianceUpdateCoordinator] = [] + self.updated_conf = False + + async def async_unload(self) -> None: + """Stops discovery and coordinators""" + _LOGGER.debug("Unloading hub") + + self.discovery.stop() + for coordinator in self.coordinators: + # Stop coordinators + coordinator.update_interval = None + + async def async_update_config(self) -> None: + """Updates config entry from Hub's data""" + self.hass.config_entries.async_update_entry(self.config_entry, data=self.config) + + async def async_setup(self) -> None: + """Sets up appliances and creates an update coordinator for + each one + """ + self.discovery.stop() + self.config = {**self.config_entry.data} + devices = [{**device} for device in self.config.get(CONF_DEVICES, [])] + self.config[CONF_DEVICES] = devices + self.errors = {} + self.updated_conf = False + + devices = [] + for device in self.config[CONF_DEVICES]: + if not _assure_valid_device_configuration(self.config, device): + self.updated_conf = True + coordinator = await self._process_appliance(device) + if coordinator and coordinator.available: + await coordinator.async_config_entry_first_refresh() + devices.append(device) + + if self.updated_conf: + await self.async_update_config() + + self.discovery.start() + + self._notify_setup_errors() + + def _notify_setup_errors(self): + if self.errors: + if not self.coordinators: + raise ConfigEntryNotReady(str(self.errors)) + for unique_id, error in self.errors.items(): + _LOGGER.warning("Device may be offline or unreachable, trying again later. %s", error) + + async def _process_appliance( + self, device: dict[str, Any] + ) -> ApplianceUpdateCoordinator | None: + discovery_mode = device.get(CONF_DISCOVERY) + # We are waiting for appliance to come online + if discovery_mode == DISCOVERY_IGNORE: + _LOGGER.debug("Ignored appliance for discovery %s", device) + return None + if discovery_mode == DISCOVERY_WAIT: + _LOGGER.debug("Waiting for appliance discovery %s", device) + return None + need_token, appliance = await self.async_discover_device( + device, initial_discovery=True + ) + return self._create_coordinator(appliance, device, need_token) + + async def async_discover_device( + self, device: dict[str, Any], initial_discovery=False + ) -> Tuple[bool, LanDevice | None]: + """Finds device on local network or cloud""" + discovery_mode = device.get(CONF_DISCOVERY) + + use_cloud = discovery_mode == DISCOVERY_CLOUD + need_cloud = use_cloud + lan_mode = discovery_mode == DISCOVERY_LAN + version = device.get(CONF_API_VERSION, 3) + need_token = ( + discovery_mode == DISCOVERY_LAN + and version >= 3 + and (not device.get(CONF_TOKEN) or not device.get(CONF_TOKEN_KEY)) + ) + if need_token: + _LOGGER.debug( + "Appliance %s %s has no token," + " trying to obtain it from Midea cloud API", + device.get(CONF_NAME), + device.get(CONF_UNIQUE_ID), + ) + need_cloud = True + if not await self._async_get_cloud_if_needed(device, need_cloud, need_token): + return need_token, None + ip_address = device[CONF_IP_ADDRESS] if lan_mode else None + if not ip_address and not use_cloud: + _LOGGER.error( + "Missing ip_address and cloud discovery is not used for %s." + "Will fall-back to cloud discovery, full configuration is %s", + device.get(CONF_UNIQUE_ID), + RedactedConf(self.config), + ) + use_cloud = True + appliance = None + try: + appliance = await self.hass.async_add_executor_job( + self.client.appliance_state, + device[CONF_IP_ADDRESS] if lan_mode else None, + device.get(CONF_TOKEN), + device.get(CONF_TOKEN_KEY), + self.cloud, + use_cloud, + device[CONF_ID], + ) + + except Exception as ex: # pylint: disable=broad-except + self.errors[ + device[CONF_UNIQUE_ID] + ] = f"Unable to get state of device {device[CONF_NAME]}: {ex}" + if initial_discovery: + _LOGGER.error( + "Error '%s' while setting up appliance %s," + " full configuration %s", + ex, + device.get(CONF_UNIQUE_ID), + RedactedConf(self.config), + exc_info=True, + ) + else: + _LOGGER.debug( + "Error '%s' while setting up appliance %s", + ex, + RedactedConf(device), + ) + return need_token, appliance + + async def _async_get_cloud_if_needed( + self, device: dict[str, Any], need_cloud: bool, need_token: bool + ) -> bool: + if need_cloud and self.cloud is None: + self._validate_auth_config_complete(device, need_token) + try: + self.cloud = await self.client.async_connect_to_cloud(self.config) + except AuthenticationError as ex: + raise ConfigEntryAuthFailed( + f"Unable to login to Midea cloud {ex}" + ) from ex + except Exception as ex: # pylint: disable=broad-except + self.errors[device[CONF_UNIQUE_ID]] = str(ex) + return False + return True + + def _validate_auth_config_complete(self, device, need_token): + if not self.config.get(CONF_USERNAME) or not self.config.get(CONF_PASSWORD): + if not device: + cause = "" + elif need_token: + cause = f" because {device.get(CONF_NAME)} is missing token," + else: + cause = f" because {device.get(CONF_NAME)} uses cloud polling," + raise ConfigEntryAuthFailed( + f"Integration needs to connect to Midea cloud," + f"{cause}" + f" but username or password are not configured." + ) + + def _create_coordinator( + self, appliance: LanDevice | None, device: dict[str, Any], need_token: bool + ) -> ApplianceUpdateCoordinator: + available = appliance is not None + if not available: + appliance = _get_placeholder_appliance(device) + appliance.name = device[CONF_NAME] + self._fix_version_if_missing(appliance, device) + self._update_token(appliance, device, need_token) + coordinator = ApplianceUpdateCoordinator( + self.hass, self, appliance, device, available=available + ) + + _LOGGER.debug("Created coordinator for %s", RedactedConf(device)) + self.coordinators.append(coordinator) + return coordinator + + def _update_token( + self, appliance: LanDevice, device: dict[str, Any], need_token: bool + ) -> None: + if need_token and appliance.token and appliance.key: + device[CONF_TOKEN] = appliance.token + device[CONF_TOKEN_KEY] = appliance.key + self.updated_conf = True + _LOGGER.debug("Updating token for %s", appliance) + + def _fix_version_if_missing( + self, appliance: LanDevice, device: dict[str, Any] + ) -> None: + if not device.get(CONF_API_VERSION): + device[CONF_API_VERSION] = appliance.version + self.updated_conf = True + _LOGGER.debug("Updating version for %s", appliance) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/humidifier.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/humidifier.py new file mode 100644 index 0000000..72335dd --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/humidifier.py @@ -0,0 +1,125 @@ +"""Adds dehumidifer entity for each dehumidifer appliance.""" + +import logging +from typing import Final + +from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity +from homeassistant.components.humidifier.const import HumidifierEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.midea_dehumidifier_lan.const import ( + ATTR_RUNNING, + DOMAIN, + MAX_TARGET_HUMIDITY, + MIN_TARGET_HUMIDITY, +) +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.hub import Hub + +_LOGGER = logging.getLogger(__name__) + +MODE_SET: Final = "Set" +MODE_DRY: Final = "Dry" +MODE_SMART: Final = "Smart" +MODE_CONTINOUS: Final = "Continuous" +MODE_PURIFIER: Final = "Purifier" +MODE_ANTIMOULD: Final = "Antimould" +MODE_FAN: Final = "Fan" + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up dehumidifier entites""" + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + DehumidifierEntity(c) for c in hub.coordinators if c.is_dehumidifier() + ) + + +_MODES = [ + (1, MODE_SET), + (2, MODE_CONTINOUS), + (3, MODE_SMART), + (4, MODE_DRY), + (6, MODE_PURIFIER), + (7, MODE_ANTIMOULD), +] + +_MODES_FROM_CAPABILITY = { + 1: [MODE_PURIFIER], + 2: [MODE_ANTIMOULD], + 3: [MODE_PURIFIER, MODE_ANTIMOULD], + 4: [MODE_FAN], +} + + +# pylint: disable=too-many-ancestors,too-many-instance-attributes +class DehumidifierEntity(ApplianceEntity, HumidifierEntity): + """(de)Humidifer entity for Midea dehumidifier""" + + _attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER + _attr_max_humidity = MAX_TARGET_HUMIDITY + _attr_min_humidity = MIN_TARGET_HUMIDITY + _attr_supported_features = HumidifierEntityFeature.MODES + _name_suffix = "" + _add_extra_attrs = True + + def __init__(self, coordinator: ApplianceUpdateCoordinator) -> None: + super().__init__(coordinator) + + self._attr_mode = None + self._attr_available_modes = [MODE_SET] + + def on_online(self, update: bool) -> None: + capabilities = self.coordinator.appliance.state.capabilities + + self._attr_available_modes = [MODE_SET] + if capabilities.get("auto"): + self._attr_available_modes.append(MODE_SMART) + self._attr_available_modes.append(MODE_CONTINOUS) + if capabilities.get("dry_clothes"): + self._attr_available_modes.append(MODE_DRY) + + more_modes = capabilities.get("mode", 0) + self._attr_available_modes += _MODES_FROM_CAPABILITY.get(more_modes, []) + + super().on_online(update) + + def on_update(self) -> None: + dehumi = self.dehumidifier() + self._attr_mode = next((i[1] for i in _MODES if i[0] == dehumi.mode), MODE_SET) + self._attr_target_humidity = dehumi.target_humidity + self._attr_current_humidity = dehumi.current_humidity # add new attribute current_humidity + self._attr_is_on = dehumi.running + super().on_update() + + def turn_on(self, **kwargs) -> None: # pylint: disable=unused-argument + """Turn the entity on.""" + self.apply(ATTR_RUNNING, True) + + def turn_off(self, **kwargs) -> None: # pylint: disable=unused-argument + """Turn the entity off.""" + self.apply(ATTR_RUNNING, False) + + def set_mode(self, mode) -> None: + """Set new target preset mode.""" + midea_mode = next((i[0] for i in _MODES if i[1] == mode), None) + if midea_mode is None: + _LOGGER.debug("Unsupported dehumidifer mode %s", mode) + midea_mode = 1 + self.apply("mode", midea_mode) + + def set_humidity(self, humidity) -> None: + """Set new target humidity.""" + self.apply("target_humidity", humidity) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/manifest.json b/homeassistant/config/custom_components/midea_dehumidifier_lan/manifest.json new file mode 100644 index 0000000..99e04a4 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "midea_dehumidifier_lan", + "name": "Midea Air Appliances (LAN)", + "after_dependencies": [ + "network", + "logger" + ], + "codeowners": [ + "@nbogojevic" + ], + "config_flow": true, + "documentation": "https://github.com/nbogojevic/homeassistant-midea-air-appliances-lan/blob/main/README.md", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/nbogojevic/homeassistant-midea-air-appliances-lan/issues", + "requirements": [ + "midea-beautiful-air==0.10.5" + ], + "version": "0.9.6" +} diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/sensor.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/sensor.py new file mode 100644 index 0000000..389bf24 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/sensor.py @@ -0,0 +1,101 @@ +"""Adds sensors for each appliance.""" + +from homeassistant.components.sensor import ( + SensorEntity, + SensorStateClass, + SensorDeviceClass +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, +) +from custom_components.midea_dehumidifier_lan.const import DOMAIN, UNIQUE_CLIMATE_PREFIX +from custom_components.midea_dehumidifier_lan.hub import Hub + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up current environment humidity and temperature sensors""" + + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + # Dehumidifier sensors + async_add_entities( + CurrentHumiditySensor(c) for c in hub.coordinators if c.is_dehumidifier() + ) + async_add_entities( + CurrentTemperatureSensor(c) for c in hub.coordinators if c.is_dehumidifier() + ) + async_add_entities( + TankLevelSensor(c) + for c in hub.coordinators + if c.is_dehumidifier() and c.dehumidifier().capabilities.get("water_level") + ) + # Climate sensors + async_add_entities( + OutsideTemperatureSensor(c) for c in hub.coordinators if c.is_climate() + ) + + +class CurrentHumiditySensor(ApplianceEntity, SensorEntity): + """Crrent environment humidity sensor""" + + _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT + _name_suffix = " Humidity" + + def on_update(self) -> None: + self._attr_native_value = self.dehumidifier().current_humidity + + +class CurrentTemperatureSensor(ApplianceEntity, SensorEntity): + """Current environment temperature sensor""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _name_suffix = " Temperature" + + def on_update(self) -> None: + self._attr_native_value = self.dehumidifier().current_temperature + + +class TankLevelSensor(ApplianceEntity, SensorEntity): + """Current tank water level sensor""" + + _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT + _name_suffix = " Water Level" + + def on_online(self, update: bool) -> None: + self._attr_entity_registry_enabled_default = ( + self.dehumidifier().capabilities.get("water_level", False) + ) + return super().on_online(update) + + def on_update(self) -> None: + self._attr_native_value = self.dehumidifier().tank_level + + +class OutsideTemperatureSensor(ApplianceEntity, SensorEntity): + """Current outside temperature sensor""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _unique_id_prefx = UNIQUE_CLIMATE_PREFIX + _name_suffix = " Outdoor Temperature" + + def on_update(self) -> None: + self._attr_native_value = self.airconditioner().outdoor_temperature diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/switch.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/switch.py new file mode 100644 index 0000000..fcb9876 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/switch.py @@ -0,0 +1,182 @@ +"""Support for different Midea appliances switches""" + +from dataclasses import dataclass +from typing import Final +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.midea_dehumidifier_lan.hub import ( + Hub, +) +from custom_components.midea_dehumidifier_lan.appliance_coordinator import ( + ApplianceEntity, + ApplianceUpdateCoordinator, +) +from custom_components.midea_dehumidifier_lan.const import ( + ENTITY_DISABLED_BY_DEFAULT, + DOMAIN, + UNIQUE_CLIMATE_PREFIX, + UNIQUE_DEHUMIDIFIER_PREFIX, +) +from custom_components.midea_dehumidifier_lan.util import is_enabled_by_capabilities + + +@dataclass +class _MideaSwitchDescriptor: + attr: str + name: str + icon: str + capability: str + prefix: str + + +ION_MODE_SWITCH: Final = _MideaSwitchDescriptor( + attr="ion_mode", + name="Ion Mode", + icon="mdi:air-purifier", + capability="ion", + prefix=UNIQUE_DEHUMIDIFIER_PREFIX, +) +PUMP_SWITCH: Final = _MideaSwitchDescriptor( + attr="pump", + name="Pump", + icon="mdi:pump", + capability="pump", + prefix=UNIQUE_DEHUMIDIFIER_PREFIX, +) +PUMP_SWITCH_ENABLED: Final = _MideaSwitchDescriptor( + attr="pump_enabled", + name="Pump Enabled", + icon="mdi:electric-switch", + capability="pump", + prefix=UNIQUE_DEHUMIDIFIER_PREFIX, +) +DEHUMIDIFIER_BEEP_SWITCH: Final = _MideaSwitchDescriptor( + attr="beep_prompt", + name="Beep", + icon="mdi:bell-check", + capability=ENTITY_DISABLED_BY_DEFAULT, + prefix=UNIQUE_DEHUMIDIFIER_PREFIX, +) +DEHUMIDIFER_SWITCHES: Final = [ + DEHUMIDIFIER_BEEP_SWITCH, + ION_MODE_SWITCH, + PUMP_SWITCH, + PUMP_SWITCH_ENABLED, +] +# Climate +CLIMATE_BEEP_SWITCH: Final = _MideaSwitchDescriptor( + attr="beep_prompt", + name="Beep", + icon="mdi:bell-check", + capability=ENTITY_DISABLED_BY_DEFAULT, + prefix=UNIQUE_CLIMATE_PREFIX, +) +FAHRENHEIT_SWITCH: Final = _MideaSwitchDescriptor( + attr="fahrenheit", + name="Fahrenheit", + icon="mdi:temperature-fahrenheit", + capability="fahrenheit", + prefix=UNIQUE_CLIMATE_PREFIX, +) +DRYER_SWITCH: Final = _MideaSwitchDescriptor( + attr="dryer", + name="Dry Mode", + icon="mdi:water-opacity", + capability="_DISABLED_BY_DEFAULT", + prefix=UNIQUE_CLIMATE_PREFIX, +) +PURIFIER_SWITCH: Final = _MideaSwitchDescriptor( + attr="purifier", + name="Purifier", + icon="mdi:air-purifier", + capability="anion", + prefix=UNIQUE_CLIMATE_PREFIX, +) +TURBO_FAN_SWITCH: Final = _MideaSwitchDescriptor( + attr="turbo_fan", + name="Turbo Fan", + icon="mdi:fan-alert", + capability="strong_fan", + prefix=UNIQUE_CLIMATE_PREFIX, +) +# SCREEN_SWITCH: Final = _MideaSwitchDescriptor( +# attr="show_screen", +# name="Show Screen", +# icon="mdi:clock-digital", +# capability="screen_display", +# prefix=UNIQUE_CLIMATE_PREFIX, +# ) +CLIMATE_SWITCHES: Final = [ + CLIMATE_BEEP_SWITCH, + DRYER_SWITCH, + FAHRENHEIT_SWITCH, + # SCREEN_SWITCH, + PURIFIER_SWITCH, + TURBO_FAN_SWITCH, +] + + +def _is_enabled( + coordinator: ApplianceUpdateCoordinator, switch: _MideaSwitchDescriptor +) -> bool: + return is_enabled_by_capabilities( + coordinator.appliance.state.capabilities, switch.capability + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Sets up appliance switches""" + + hub: Hub = hass.data[DOMAIN][config_entry.entry_id] + + switches = [] + # Dehumidifier sensors + for switch in DEHUMIDIFER_SWITCHES: + for coord in hub.coordinators: + if coord.is_dehumidifier() and _is_enabled(coord, switch): + switches.append(MideaSwitch(coord, switch)) + + # Air conditioner entities + for switch in CLIMATE_SWITCHES: + for coord in hub.coordinators: + if coord.is_climate() and _is_enabled(coord, switch): + switches.append(MideaSwitch(coord, switch)) + + async_add_entities(switches) + + +# pylint: disable=too-many-ancestors +class MideaSwitch(ApplianceEntity, SwitchEntity): + """Generic attr based switch""" + + def __init__( + self, + coordinator: ApplianceUpdateCoordinator, + descriptor: _MideaSwitchDescriptor, + ) -> None: + self._switch_descriptor = descriptor + self._capability_attr = descriptor.capability + self._unique_id_prefix = descriptor.prefix + self._name_suffix = " " + descriptor.name.strip() + super().__init__(coordinator) + + self._attr_icon = descriptor.icon + self._attribute_name = descriptor.attr + + def on_update(self) -> None: + self._attr_is_on = getattr(self.appliance.state, self._attribute_name, None) + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + self.apply(self._attribute_name, True) + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + self.apply(self._attribute_name, False) diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/en.json b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/en.json new file mode 100644 index 0000000..7272971 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/en.json @@ -0,0 +1,96 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Account", + "password": "Password", + "mobile_app": "Mobile app", + "advanced_settings": "Advanced settings" + }, + "description": "Please enter username and password that you use to connect with the selected Midea mobile application.", + "title": "Sign-in with Midea app account" + }, + "advanced_settings": { + "data": { + "username": "Account", + "password": "Password", + "appkey": "Mobile app key", + "appid": "Mobile app id", + "broadcast_address": "Appliance address or network range", + "include": "Discover following appliance categories:", + "scan_interval": "Network scan interval", + "debug": "Advanced debug mode" + }, + "description": "You can specify network address (e.g. 192.0.2.2) or range (e.g. 192.0.2.4/24) to search for specific appliance(s) if regular discovery doesn't work.", + "title": "Advanced settings" + }, + "unreachable_appliance": { + "data": { + "discovery": "Choose action (discovery mode)", + "ip_address": "IPv4 address", + "name": "Device name", + "token": "Token", + "token_key": "Token key", + "ttl": "Minutes before unavailable" + }, + "description": "We were unable to discover an appliance called {name} with the serial number {id}.\n\nPlease choose if you want to exclude appliance, search for it later, use cloud API to poll it, or provide its IPv4 address if you know it.\n\nYou can also provide token and key if you have them. If not, we will try to obtain them from Midea cloud.", + "title": "Unable to discover appliance" + }, + "reauth_confirm": { + "data": { + "username": "Account", + "password": "Password", + "appkey": "Mobile app key", + "appid": "Mobile app id", + "broadcast_address": "Appliance or network broadcast address" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Midea Air Appliance (LAN) integration needs to re-authenticate your account" + } + }, + "error": { + "cannot_connect": "Unable to connect to appliance ({cause}).", + "connection_error": "Connection error ({cause}).", + "duplicate_ip_provided": "Same IPv4 address was used for {cause}. Please specify different addresses for different appliances.", + "invalid_auth": "Invalid username or password ({cause})", + "invalid_ip_address": "Invalid IPv4 address ({cause}), please enter a valid IPv4 address (e.g. 192.0.2.2)", + "invalid_ip_range": "Invalid IPv4 address or range ({cause}), please enter a valid IPv4 address or range (e.g. 192.0.2.2 or 192.0.2.0/24)", + "no_cloud": "Unable to connect to Midea cloud API ({cause}).", + "not_discovered": "Unable to find appliance at specified IPv4 address.", + "midea_client": "An error in communication with Midea API has occurred. See log for more information.", + "unknown": "An unknown error has occurred. See log for more information." + }, + "abort": { + "single_instance_allowed": "Already defined a Midea app account. Only a single account is supported for Midea Air Appliances (LAN).", + "reauth_successful": "Re-authentication was successful", + "no_configured_devices": "There are no devices to configure" + } + }, + "options": { + "step": { + "appliance": { + "description": "Appliance {index} of {count}", + "data": { + "discovery": "Choose action (discovery mode)", + "ip_address": "IPv4 address", + "name": "Device name", + "token": "Token", + "token_key": "Token key", + "ttl": "Minutes before unavailable" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to appliance ({cause}).", + "connection_error": "Connection error ({cause}).", + "duplicate_ip_provided": "Same IPv4 address was used for {cause}. Please specify different address for different appliances.", + "invalid_auth": "Invalid username or password ({cause})", + "invalid_ip_address": "Invalid IPv4 address ({cause}), please enter a valid IPv4 address (e.g. 192.0.2.2)", + "invalid_ip_range": "Invalid IPv4 address or range ({cause}), please enter a valid IPv4 address or range (e.g. 192.0.2.2 or 192.0.2.0/24)", + "no_cloud": "Unable to connect to Midea cloud API ({cause}).", + "not_discovered": "Unable to find appliance at specified IPv4 address.", + "unknown": "An unknown error has occurred. See log for more information." + } + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/it.json b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/it.json new file mode 100644 index 0000000..c800c36 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/it.json @@ -0,0 +1,96 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Account", + "password": "Password", + "mobile_app": "App mobile", + "advanced_settings": "Impostazioni avanzate" + }, + "description": "Inserisci il nome utente e la password che usi per connetterti con l'applicazione Midea selezionata.", + "title": "Accedi con l'account dell'app Midea" + }, + "advanced_settings": { + "data": { + "username": "Account", + "password": "Password", + "appkey": "Chiave dell'app mobile", + "appid": "ID dell'app mobile", + "broadcast_address": "Indirizzo dell'elettrodomestico o della rete", + "include": "Scopri le seguenti categorie di elettrodomestici:", + "scan_interval": "Intervallo di scansione della rete", + "debug": "Modalità di debug avanzata" + }, + "description": "Puoi specificare l'indirizzo di rete (ad esempio 192.0.2.2) o il range (ad esempio 192.0.2.4/24) per cercare un elettrodomestico specifico se la ricerca regolare non funziona.", + "title": "Impostazioni avanzate" + }, + "unreachable_appliance": { + "data": { + "discovery": "Scegli l'azione (modalità di ricerca)", + "ip_address": "Indirizzo IPv4", + "name": "Nome del dispositivo", + "token": "Token", + "token_key": "Chiave del token", + "ttl": "Minuti prima che diventi non disponibile" + }, + "description": "Non siamo riusciti a scoprire un elettrodomestico chiamato {name} con il numero di serie {id}.\n\nScegli se escludere l'elettrodomestico, cercarlo successivamente, usare l'API cloud per interrogarlo o fornire il suo indirizzo IPv4 se lo conosci.\n\nPuoi anche fornire il token e la chiave se li hai. In caso contrario, proveremo a ottenerli dal cloud Midea.", + "title": "Impossibile scoprire l'elettrodomestico" + }, + "reauth_confirm": { + "data": { + "username": "Account", + "password": "Password", + "appkey": "Chiave dell'app mobile", + "appid": "ID dell'app mobile", + "broadcast_address": "Indirizzo dell'elettrodomestico o della rete di broadcast" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "L'integrazione Midea Air Appliance (LAN) ha bisogno di riautenticare il tuo account" + } + }, + "error": { + "cannot_connect": "Impossibile connettersi all'elettrodomestico ({cause}).", + "connection_error": "Errore di connessione ({cause}).", + "duplicate_ip_provided": "Lo stesso indirizzo IPv4 è stato usato per {cause}. Specifica indirizzi diversi per elettrodomestici diversi.", + "invalid_auth": "Nome utente o password non validi ({cause})", + "invalid_ip_address": "Indirizzo IPv4 non valido ({cause}), inserisci un indirizzo IPv4 valido (ad esempio 192.0.2.2)", + "invalid_ip_range": "Indirizzo IPv4 o intervallo non valido ({cause}), inserisci un indirizzo IPv4 o intervallo valido (ad esempio 192.0.2.2 o 192.0.2.0/24)", + "no_cloud": "Impossibile connettersi all'API cloud di Midea ({cause}).", + "not_discovered": "Impossibile trovare l'elettrodomestico all'indirizzo IPv4 specificato.", + "midea_client": "Si è verificato un errore nella comunicazione con l'API di Midea. Consulta il registro per ulteriori informazioni.", + "unknown": "Si è verificato un errore sconosciuto. Consulta il registro per ulteriori informazioni." + }, + "abort": { + "single_instance_allowed": "Hai già configurato un account Midea. È supportato un solo account per gli elettrodomestici Midea Air (LAN).", + "reauth_successful": "Rautenticazione riuscita", + "no_configured_devices": "Non ci sono dispositivi da configurare" + } + }, + "options": { + "step": { + "appliance": { + "description": "Elettrodomestico {index} di {count}", + "data": { + "discovery": "Scegli azione (modalità di scoperta)", + "ip_address": "Indirizzo IPv4", + "name": "Nome dispositivo", + "token": "Token", + "token_key": "Chiave token", + "ttl": "Minuti prima di non disponibile" + } + } + }, + "error": { + "cannot_connect": "Impossibile connettersi all'elettrodomestico ({cause}).", + "connection_error": "Errore di connessione ({cause}).", + "duplicate_ip_provided": "Lo stesso indirizzo IPv4 è stato usato per {cause}. Specifica indirizzi diversi per elettrodomestici diversi.", + "invalid_auth": "Nome utente o password non validi ({cause})", + "invalid_ip_address": "Indirizzo IPv4 non valido ({cause}), inserisci un indirizzo IPv4 valido (ad esempio 192.0.2.2)", + "invalid_ip_range": "Indirizzo IPv4 o intervallo non valido ({cause}), inserisci un indirizzo IPv4 o intervallo valido (ad esempio 192.0.2.2 o 192.0.2.0/24)", + "no_cloud": "Impossibile connettersi all'API cloud di Midea ({cause}).", + "not_discovered": "Impossibile trovare l'elettrodomestico all'indirizzo IPv4 specificato.", + "unknown": "Si è verificato un errore sconosciuto. Consulta il registro per ulteriori informazioni." + } + } +} diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/pt.json b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/pt.json new file mode 100644 index 0000000..c2e9b99 --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/pt.json @@ -0,0 +1,96 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Conta", + "password": "Password", + "mobile_app": "Aplicação Móvel", + "advanced_settings": "Definições Avançadas" + }, + "description": "Por favor, introduza o nome de utilizador e a palavra-passe que utiliza para conectar-se à aplicação móvel Midea selecionada.", + "title": "Iniciar sessão com a conta da aplicação Midea" + }, + "advanced_settings": { + "data": { + "username": "Conta", + "password": "Password", + "appkey": "Key da Aplicação Móvel", + "appid": "Id da Aplicação Móvel", + "broadcast_address": "Endereço do AC / Desumificador ou gama de rede", + "include": "Desbra as seguintes categorias de AC / Desumificador:", + "scan_interval": "Intervalo de scan da rede", + "debug": "Modo de depuraçao avançado" + }, + "description": "Pode especificar o endereço de rede (por exemplo, 192.0.2.2) ou gama (por exemplo, 192.0.2.4/24) para procurar AC / Desumificador(es) específico(s) se a descoberta regular não funcionar.", + "title": "Definições Avançadas" + }, + "unreachable_appliance": { + "data": { + "discovery": "Escolha a ação (modo discovery)", + "ip_address": "Endereço IPv4", + "name": "Nome do dispositivo", + "token": "Token", + "token_key": "Token key", + "ttl": "Minutos antes de ficar indisponível" + }, + "description": "Não conseguimos descobrir um AC / Desumificador chamado {nome} com o número de série {id}.\n\nPor favor, escolha se deseja excluir o AC / Desumificador, procurá-lo mais tarde, usar a API da cloud para verificá-lo ou fornecer o seu endereço IPv4.\n\nPode também fornecer o token e a key se os tiver. Caso contrário, tentaremos obtê-los a partir da cloud da Midea.", + "title": "Unable to discover appliance" + }, + "reauth_confirm": { + "data": { + "username": "Conta", + "password": "Password", + "appkey": "Key da aplicação móvel", + "appid": "ID da aplicação móvel", + "broadcast_address": "AC / Desumificador ou IP de broadcast da rede" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "A integração do AC / Desumificador da Midea (LAN) precisa de reautenticar a sua conta" + } + }, + "error": { + "cannot_connect": "Não é possível conectar ao dispositivo ({cause}).", + "connection_error": "Erro de conexão ({cause}).", + "duplicate_ip_provided": "O mesmo endereço IPv4 foi usado para {cause}. Por favor, especifique endereços IP diferentes para diferentes dispositivos.", + "invalid_auth": "Conta ou password inválido ({cause})", + "invalid_ip_address": "Endereço IPv4 inválido ({cause}), por favor insira um endereço IPv4 válido (por exemplo, 192.0.2.2)", + "invalid_ip_range": "Endereço ou gama IPv4 inválida ({cause}), por favor insira um endereço ou gama IPv4 válidos (por exemplo, 192.0.2.2 ou 192.0.2.0/24)", + "no_cloud": "Não é possível conectar à API da cloud Midea ({cause}).", + "not_discovered": "Não foi possível encontrar o dispositivo no endereço IPv4 especificado.", + "midea_client": "Ocorreu um erro na comunicação com a API da Midea. Consulte o log para mais informações.", + "unknown": "Ocorreu um erro desconhecido. Consulte o log para mais informações." + }, + "abort": { + "single_instance_allowed": "Já está definida uma conta Midea. Apenas uma única conta é suportada para os seus dispositivos Midea (LAN).", + "reauth_successful": "A reautenticação foi bem-sucedida", + "no_configured_devices": "Não há dispositivos para configurar" + } + }, + "options": { + "step": { + "appliance": { + "description": "Dispositivo {index} de {count}", + "data": { + "discovery": "Escolha a acção (Modo discovery)", + "ip_address": "Endereço IPv4", + "name": "Nome do Dispositivo", + "token": "Token", + "token_key": "Token key", + "ttl": "Minutos antes de ficar indisponível" + } + } + }, + "error": { + "cannot_connect": "Não é possível conectar ao dispositivo ({cause}).", + "connection_error": "Erro de conexão ({cause}).", + "duplicate_ip_provided": "O mesmo endereço IPv4 foi usado para {cause}. Por favor, especifique endereços IP diferentes para diferentes dispositivos.", + "invalid_auth": "Conta ou password inválido ({cause})", + "invalid_ip_address": "Endereço IPv4 inválido ({cause}), por favor insira um endereço IPv4 válido (por exemplo, 192.0.2.2)", + "invalid_ip_range": "Endereço ou gama IPv4 inválida ({cause}), por favor insira um endereço ou gama IPv4 válidos (por exemplo, 192.0.2.2 ou 192.0.2.0/24)", + "no_cloud": "Não é possível conectar à API da cloud Midea ({cause}).", + "not_discovered": "Não foi possível encontrar o dispositivo no endereço IPv4 especificado.", + "unknown": "Ocorreu um erro desconhecido. Consulte o log para mais informações." + } + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/sk.json b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/sk.json new file mode 100644 index 0000000..5b1de3c --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/translations/sk.json @@ -0,0 +1,96 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Účet", + "password": "Heslo", + "mobile_app": "Mobile app", + "advanced_settings": "Pokročilé nastavenia" + }, + "description": "Zadajte používateľské meno a heslo, ktoré používate na pripojenie k vybranej mobilnej aplikácii Midea.", + "title": "Prihláste sa pomocou účtu aplikácie Midea" + }, + "advanced_settings": { + "data": { + "username": "Účet", + "password": "Heslo", + "appkey": "Mobile app kľúč", + "appid": "Mobile app id", + "broadcast_address": "Adresa spotrebiča alebo rozsah siete", + "include": "Objavte nasledujúce kategórie spotrebičov:", + "scan_interval": "Interval skenovania siete", + "debug": "Pokročilý režim ladenia" + }, + "description": "Ak bežné zisťovanie nefunguje, môžete zadať sieťovú adresu (napr. 192.0.2.2) alebo rozsah (napr. 192.0.2.4/24) a vyhľadať konkrétne zariadenia.", + "title": "Pokročilé nastavenia" + }, + "unreachable_appliance": { + "data": { + "discovery": "Vyberte akciu (režim objavovania)", + "ip_address": "IPv4 addresa", + "name": "Názov zariadenia", + "token": "Token", + "token_key": "Token kľúč", + "ttl": "Minúty predtým nedostupné" + }, + "description": "Nepodarilo sa nám nájsť zariadenie s názvom {name} so sériovým číslom {id}.\n\nVyberte, či chcete zariadenie vylúčiť, vyhľadať ho neskôr, použiť cloudové rozhranie API na prieskum alebo poskytnúť jeho adresu IPv4, ak ju poznáte.\n\nMôžete tiež poskytnúť token a kľúč, ak ich máte. Ak nie, pokúsime sa ich získať z cloudu Midea.", + "title": "Zariadenie sa nepodarilo nájsť" + }, + "reauth_confirm": { + "data": { + "username": "Účet", + "password": "Heslo", + "appkey": "Mobile app kľúč", + "appid": "Mobile app id", + "broadcast_address": "Adresa zariadenia alebo sieťového vysielania" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Integrácia zariadenia Midea Air Appliance (LAN) musí znova overiť váš účet" + } + }, + "error": { + "cannot_connect": "Nedá sa pripojiť k zariadeniu ({cause}).", + "connection_error": "Chyba spojenia({cause}).", + "duplicate_ip_provided": "Bola použitá rovnaká adresa IPv4 {cause}. Uveďte rôzne adresy pre rôzne spotrebiče.", + "invalid_auth": "nesprávne užívateľské meno alebo heslo ({cause})", + "invalid_ip_address": "Neplatná adresa IPv4 ({cause}), zadajte platnú adresu IPv4 (napr. 192.0.2.2)", + "invalid_ip_range": "Neplatná adresa alebo rozsah IPv4 ({cause}), zadajte platnú adresu IPv4 alebo rozsah (napr. 192.0.2.2 alebo 192.0.2.0/24)", + "no_cloud": "Nedá sa pripojiť ku cloudovému API Midea ({cause}).", + "not_discovered": "Nemožno nájsť zariadenie na zadanej adrese IPv4.", + "midea_client": "Vyskytla sa chyba v komunikácii s Midea API. Viac informácií nájdete v denníku.", + "unknown": "Vyskytla sa neznáma chyba. Viac informácií nájdete v denníku." + }, + "abort": { + "single_instance_allowed": "Už definovaný účet aplikácie Midea. Pre zariadenia Midea Air Appliance (LAN) je podporovaný iba jeden účet.", + "reauth_successful": "Opätovné overenie bolo úspešné", + "no_configured_devices": "Neexistujú žiadne zariadenia na konfiguráciu" + } + }, + "options": { + "step": { + "appliance": { + "description": "Spotrebič {index} z {count}", + "data": { + "discovery": "Vyberte akciu (režim objavovania)", + "ip_address": "IPv4 addresa", + "name": "Názov zariadenia", + "token": "Token", + "token_key": "Token kľúč", + "ttl": "Minúty predtým nedostupné" + } + } + }, + "error": { + "cannot_connect": "Nedá sa pripojiť k zariadeniu ({cause}).", + "connection_error": "Chyba spojenia ({cause}).", + "duplicate_ip_provided": "Bola použitá rovnaká adresa IPv4 {cause}. Zadajte inú adresu pre rôzne spotrebiče.", + "invalid_auth": "Nesprávne užívateľské meno alebo heslo ({cause})", + "invalid_ip_address": "Neplatná IPv4 adresa ({cause}), zadajte platnú adresu IPv4 (napr. 192.0.2.2)", + "invalid_ip_range": "Neplatná IPv4 adresa alebo rozsah ({cause}), zadajte platnú adresu IPv4 alebo rozsah (napr. 192.0.2.2 alebo 192.0.2.0/24)", + "no_cloud": "Nedá sa pripojiť ku cloudovému API Midea ({cause}).", + "not_discovered": "Nemožno nájsť zariadenie na zadanej adrese IPv4.", + "unknown": "Vyskytla sa neznáma chyba. Viac informácií nájdete v denníku." + } + } +} diff --git a/homeassistant/config/custom_components/midea_dehumidifier_lan/util.py b/homeassistant/config/custom_components/midea_dehumidifier_lan/util.py new file mode 100644 index 0000000..d78209e --- /dev/null +++ b/homeassistant/config/custom_components/midea_dehumidifier_lan/util.py @@ -0,0 +1,244 @@ +"""Utilities for Midea Air Appliances integration""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Any, Tuple, cast, final + +import homeassistant.components.logger as hass_logger +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICES, + CONF_ID, + CONF_INCLUDE, + CONF_PASSWORD, + CONF_TOKEN, + CONF_UNIQUE_ID, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +import midea_beautiful as midea_beautiful_api +from midea_beautiful.appliance import AirConditionerAppliance, DehumidifierAppliance +from midea_beautiful.cloud import MideaCloud +from midea_beautiful.lan import LanDevice +from midea_beautiful.midea import ( + APPLIANCE_TYPE_AIRCON, + APPLIANCE_TYPE_DEHUMIDIFIER, +) +from midea_beautiful.util import very_verbose + +from custom_components.midea_dehumidifier_lan.const import ( + _ALWAYS_CREATE, + CONF_MOBILE_APP, + CONF_TOKEN_KEY, + UNKNOWN_IP, +) + +_SUPPORTABLE_APPLIANCES = { + APPLIANCE_TYPE_AIRCON: AirConditionerAppliance.supported, + APPLIANCE_TYPE_DEHUMIDIFIER: DehumidifierAppliance.supported, +} + + +def _redact(data: dict[str, Any], key: str, char="*", length: int = 0) -> None: + """Redacts/obfuscates key in disctionary""" + if data.get(key) is not None: + to_redact = str(data[key]) + if length <= 0 or length >= len(to_redact): + data[key] = char * len(to_redact) + else: + data[key] = to_redact[:-length] + char * length + + +def _redact_device_conf(device) -> None: + _redact(device, CONF_TOKEN) + _redact(device, CONF_TOKEN_KEY) + _redact(device, CONF_UNIQUE_ID, length=8) + _redact(device, CONF_ID, length=4) + + +class RedactedConf: + """Outputs redacted configuration dictionary by removing or masking + confidential data.""" + + def __init__(self, data: dict[str, Any]) -> None: + """Remove sensitive information from configuration""" + self.conf = data + + @property + def __dict__(self) -> dict[str, Any]: + conf = deepcopy(self.conf) + _redact(conf, CONF_USERNAME) + _redact(conf, CONF_PASSWORD) + _redact_device_conf(conf) + if conf.get(CONF_DEVICES) and isinstance(conf.get(CONF_DEVICES), list): + for device in conf[CONF_DEVICES]: + if device and isinstance(device, dict): + _redact_device_conf(device) + return conf + + def __str__(self) -> str: + """Remove sensitive information from configuration""" + + return str(self.__dict__) + + +def is_enabled_by_capabilities(capabilities: dict[str, Any], capability: str) -> bool: + """Returns True if given capability is enabled""" + if capability in _ALWAYS_CREATE: + return True + if not capabilities or capabilities.get(capability, False): + return True + return False + + +def is_climate(appliance: LanDevice) -> bool: + """True if appliance is air conditioner""" + return AirConditionerAppliance.supported(appliance.type) + + +def is_dehumidifier(appliance: LanDevice) -> bool: + """True if appliance is dehumidifier""" + return DehumidifierAppliance.supported(appliance.type) + + +def supported_appliance(conf: dict, appliance: LanDevice) -> bool: + """Checks if appliance is supported by integration""" + included = conf.get(CONF_INCLUDE, []) + for type_id, check in _SUPPORTABLE_APPLIANCES.items(): + if type_id in included and check(appliance.type): + return True + return False + + +class ApplianceCoordinator(ABC): # pylint: disable=too-few-public-methods + """Abstract interface for Appliance update coordinators""" + + appliance: LanDevice + available: bool + device: dict[str, Any] + + def is_climate(self) -> bool: + """True if appliance is air conditioner""" + return is_climate(self.appliance) + + def is_dehumidifier(self) -> bool: + """True if appliance is dehumidifier""" + return is_dehumidifier(self.appliance) + + @final + def dehumidifier(self) -> DehumidifierAppliance: + """Returns state as dehumidifier""" + return cast(DehumidifierAppliance, self.appliance.state) + + @final + def airconditioner(self) -> AirConditionerAppliance: + """Returns state as air conditioner""" + return cast(AirConditionerAppliance, self.appliance.state) + + +class AbstractHub(ABC): + """Interface for central class for interacting with appliances""" + + coordinators: list[ApplianceCoordinator] + config: dict[str, Any] + errors: dict[str, Any] + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + self.client = MideaClient(hass) + self.cloud: MideaCloud | None = None + self.hass = hass + self.config_entry = config_entry + + @abstractmethod + async def async_discover_device( + self, device: dict[str, Any], initial_discovery=False + ) -> Tuple[bool, LanDevice | None]: + """Finds device on local network or cloud""" + return False, None + + @abstractmethod + async def async_update_config(self) -> None: + """Updates config entry from Hub's data""" + + +class MideaClient: + """Delegate to midea API""" + + def __init__(self, hass: HomeAssistant) -> None: + self.hass = hass + + async def async_debug_mode(self, activate: bool) -> None: + """Activated advanced debug mode.""" + very_verbose(activate) + if activate: + await self.hass.services.async_call( + domain=hass_logger.DOMAIN, + service=hass_logger.SERVICE_SET_LEVEL, + service_data={"midea_beautiful": "DEBUG"}, + ) + + async def async_connect_to_cloud(self, conf: dict[str, Any]) -> MideaCloud: + """Delegate to midea_beautiful_api.connect_to_cloud""" + return await self.hass.async_add_executor_job( + self.connect_to_cloud, + conf, + ) + + # pylint: disable=no-self-use + def connect_to_cloud(self, conf: dict[str, Any]) -> MideaCloud: + """Delegate to midea_beautiful_api.connect_to_cloud""" + return midea_beautiful_api.connect_to_cloud( + account=conf[CONF_USERNAME], + password=conf[CONF_PASSWORD], + appname=conf[CONF_MOBILE_APP], + ) + + def appliance_state( # pylint: disable=too-many-arguments,no-self-use + self, + address: str = None, + token: str = None, + key: str = None, + cloud: MideaCloud = None, + use_cloud: bool = False, + appliance_id: str = None, + ): + """Delegate to midea_beautiful_api.appliance_state""" + return midea_beautiful_api.appliance_state( + address=address, + token=token, + key=key, + cloud=cloud, + use_cloud=use_cloud, + appliance_id=appliance_id, + retries=5, + cloud_timeout=6, + ) + + def find_appliances( # pylint: disable=too-many-arguments,no-self-use + self, + cloud: MideaCloud = None, + addresses: list[str] = None, + retries: int = 3, + timeout: int = 3, + ) -> list[LanDevice]: + """Delegate to midea_beautiful_api.find_appliances""" + return midea_beautiful_api.find_appliances( + cloud=cloud, + addresses=addresses, + retries=retries, + timeout=timeout, + ) + + # pylint: disable=no-self-use + async def async_list_appliances(self, cloud: MideaCloud) -> list: + """Delegate to midea_beautiful_api.connect_to_cloud""" + return await self.hass.async_add_executor_job( + cloud.list_appliances, + ) + + +def address_ok(address: str | None) -> bool: + """Returns True if address is not known""" + return address is not None and address != UNKNOWN_IP diff --git a/homeassistant/config/custom_components/polaris/__init__.py b/homeassistant/config/custom_components/polaris/__init__.py new file mode 100644 index 0000000..3c60b8d --- /dev/null +++ b/homeassistant/config/custom_components/polaris/__init__.py @@ -0,0 +1,42 @@ +"""The Polaris IQ Home component.""" + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +# Import global values. +from .const import PLATFORMS + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Trigger the creation of sensors.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Активация всех деактивированных устройств при загрузке + await async_enable_disabled_devices(hass, entry) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload all sensor entities and services if integration is removed via UI. + No restart of home assistant is required. + """ + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + return unload_ok + + +async def async_enable_disabled_devices(hass: HomeAssistant, entry: ConfigEntry): + """Enable all disabled devices associated with this integration.""" + device_registry = dr.async_get(hass) + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + + for device in devices: + if device.disabled: + _LOGGER.debug("Enabling disabled device: %s (%s)", device.name, device.id) + device_registry.async_update_device(device.id, disabled_by = None) # Убираем disabled diff --git a/homeassistant/config/custom_components/polaris/binary_sensor.py b/homeassistant/config/custom_components/polaris/binary_sensor.py new file mode 100644 index 0000000..0e288d9 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/binary_sensor.py @@ -0,0 +1,324 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable, List +import copy +from datetime import datetime + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.binary_sensor import ( + DOMAIN, + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +# Device +from homeassistant.helpers.device_registry import DeviceEntry, DeviceEntryDisabler +from homeassistant.helpers import device_registry as dev_reg +from homeassistant.helpers.entity import Entity +from homeassistant.helpers import entity_registry as ent_reg +from homeassistant.const import STATE_UNAVAILABLE + +from .common import PolarisBaseEntity + +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + BINARYSENSOR_KETTLE, + BINARYSENSOR_LID, + BINARYSENSOR_WATER_TANK, + BINARYSENSOR_CAPPUCCINATOR, + BINARYSENSOR_AVAILABLE, + BINARYSENSOR_THERMOSTAT, + PolarisBinarySensorEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COOKER_WITH_LID_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_CLIMATE_TYPE, + POLARIS_AIRCLEANER_TYPE, + POLARIS_AIRCLEANER_EAP_TYPE, + POLARIS_BOILER_TYPE, + POLARIS_VACUUM_TYPE, + POLARIS_IRRIGATOR_TYPE, + POLARIS_HEATER_TYPE, + POLARIS_AIRCONDITIONER_TYPE, + POLARIS_THERMOSTAT_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + binarysensorList = [] + + if (device_type in POLARIS_KETTLE_WITH_WEIGHT_TYPE): + BINARYSENSOR_KETTLE_LC = copy.deepcopy(BINARYSENSOR_KETTLE) + for description in BINARYSENSOR_KETTLE_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COOKER_WITH_LID_TYPE): + BINARYSENSOR_LID_LC = copy.deepcopy(BINARYSENSOR_LID) + for description in BINARYSENSOR_LID_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HUMIDDIFIER_TYPE and device_type not in {"835","881"}): + BINARYSENSOR_WATER_TANK_LC = copy.deepcopy(BINARYSENSOR_WATER_TANK) + for description in BINARYSENSOR_WATER_TANK_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + BINARYSENSOR_CAPPUCCINATOR_LC = copy.deepcopy(BINARYSENSOR_CAPPUCCINATOR) + for description in BINARYSENSOR_CAPPUCCINATOR_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_THERMOSTAT_TYPE): + BINARYSENSOR_THERMOSTAT_LC = copy.deepcopy(BINARYSENSOR_THERMOSTAT) + for description in BINARYSENSOR_THERMOSTAT_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_KETTLE_TYPE or + device_type in POLARIS_KETTLE_WITH_WEIGHT_TYPE or + device_type in POLARIS_HUMIDDIFIER_TYPE or + device_type in POLARIS_COOKER_TYPE or + device_type in POLARIS_COFFEEMAKER_ROG_TYPE or + device_type in POLARIS_COFFEEMAKER_TYPE or + device_type in POLARIS_CLIMATE_TYPE or + device_type in POLARIS_AIRCLEANER_TYPE or + device_type in POLARIS_AIRCLEANER_EAP_TYPE or + device_type in POLARIS_BOILER_TYPE or + device_type in POLARIS_VACUUM_TYPE or + device_type in POLARIS_IRRIGATOR_TYPE or + device_type in POLARIS_HEATER_TYPE or + device_type in POLARIS_AIRCONDITIONER_TYPE or + device_type in POLARIS_THERMOSTAT_TYPE): + BINARYSENSOR_AVAILABLE_LC = copy.deepcopy(BINARYSENSOR_AVAILABLE) + for description in BINARYSENSOR_AVAILABLE_LC: + description.mqttTopicStatus = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStatus}" + description.device_prefix_topic = device_prefix_topic + binarysensorList.append( + PolarisBinarySensor( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + + async_add_entities(binarysensorList, update_before_add=True) + + +class PolarisBinarySensor(PolarisBaseEntity, BinarySensorEntity, ConfigEntry): + + entity_description: PolarisBinarySensorEntityDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisBinarySensorEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_has_entity_name = True + self._attr_is_on = False + self.device_entities = [] + if self.entity_description.name != "available": + self._attr_available = False + + async def async_added_to_hass(self): + @callback + async def message_received_base(message): + if int(self.device_type) == 45 and self.entity_description.key == "cappuccinator": + if int(message.payload) == 255: + self._attr_is_on = False + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_tank" + service_data["value"] = 7.777 + await self.hass.services.async_call("number", "set_value", service_data) + else: + self._attr_is_on = True + elif str(message.payload).lower() in ("1", "true"): + if self.entity_description.name == "available": + self._attr_is_on = False +# await self.update_device_availability(False) +# await self.get_device_entities(False) + else: + self._attr_is_on = True + elif str(message.payload).lower() in ("0", "false"): + if self.entity_description.name == "available": + self._attr_is_on = True +# await self.update_device_availability(True) +# await self.get_device_entities(True) + else: + self._attr_is_on = False + self.async_write_ha_state() + + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicStatus, + message_received_base, + 1, + ) + + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + + + async def get_device_entities(self, available: bool): # -> List[Entity]: + """Return a list of entities that belong to the same device as this binary sensor""" + entities = [] + entity_registry = ent_reg.async_get(self.hass) + entity_entry = entity_registry.async_get(self.entity_id) + _LOGGER.debug("-------------------------------------------------") + _LOGGER.debug("entity_id 0 %s", self.entity_id) + if entity_entry: + _LOGGER.debug("entity_entry 0 %s", entity_entry) + dev_id = entity_entry.device_id + _LOGGER.debug("dev_id 0 %s", dev_id) + entity_ids = [ + entry.entity_id for entry in entity_registry.entities.values() + if entry.device_id == dev_id + ] + _LOGGER.debug("entity_ids 0 %s", entity_ids) + for entity_id_av in entity_ids: + entity_av = self.hass.states.get(entity_id_av) + if entity_av: + _LOGGER.debug("entity off: %s", entity_av) +# entity_av.attributes["_attr_available"] = available + self.hass.states.async_set(entity_id_av, entity_av.state, entity_av.attributes) + +# return entities + + + + + + + + +# async def get_device_entities(self) -> List[Entity]: +# """Return a list of entities that belong to the same device as this binary sensor""" +# entities = [] +# for entity in self.hass.entities: +# dev_find = f"{POLARIS_DEVICE[int(self.device_type)]['class'].replace('-', '_').lower()}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_').lower()}" +# if dev_find in entity.entity_id: +# entities.append(entity.entity_id) +# return entities + + + + +# async def update_device_availability(self, available: bool): +# """Обновляет статус устройства в Device Registry.""" +# dev_registry = dev_reg.async_get(self.hass) +# device = dev_registry.async_get_device(self.device_info["identifiers"]) + +# if device: +# for entry in self.hass.config_entries.async_entries(): +# if entry.domain == "polaris" and entry.unique_id == f"{POLARIS_DEVICE[int(self.device_type)]['class']}-{POLARIS_DEVICE[int(self.device_type)]['model']}-{self.device_id}": +# config_entries = entry +# if available: +# dev_registry.async_update_device(device.id, disabled_by = None) +# state = self.hass.states.get(self.entity_id) +# if state == None: +# delta_time = 1000 +# else: +# self._new_time_available = datetime.now().timestamp() +# delta_time = self._new_time_available - state.last_changed.timestamp() +# if delta_time > 5: +# await self.hass.config_entries.async_reload(config_entries.entry_id) +# else: +# dev_registry.async_update_device(device.id, disabled_by = DeviceEntryDisabler.INTEGRATION) + diff --git a/homeassistant/config/custom_components/polaris/button.py b/homeassistant/config/custom_components/polaris/button.py new file mode 100644 index 0000000..f85aee7 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/button.py @@ -0,0 +1,364 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy +from datetime import datetime +import os + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.button import ( + DOMAIN, + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers import entity_registry as er +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + CUSTOM_SELECT_FILE_PATH, + BUTTON_HUMIDIFIER, + BUTTON_COOKER, + SELECT_COOKER, + BUTTON_COFFEEMAKER, + SELECT_COFFEEMAKER, + SELECT_COFFEEMAKER_ROG, + BUTTON_CLIMATES, + BUTTON_CLIMATES_200, + BUTTON_AIRCLEANER, + PolarisButtonEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_CLIMATE_TYPE, + POLARIS_AIRCLEANER_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + buttonList = [] + + if (device_type in POLARIS_HUMIDDIFIER_TYPE and device_type not in {"835","881"}): + BUTTON_HUMIDIFIER_LC = copy.deepcopy(BUTTON_HUMIDIFIER) + for description in BUTTON_HUMIDIFIER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + if (device_type in POLARIS_COOKER_TYPE): + BUTTON_COOKER_LC = copy.deepcopy(BUTTON_COOKER) + for description in BUTTON_COOKER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + if (device_type in POLARIS_COFFEEMAKER_TYPE): + BUTTON_COFFEEMAKER_LC = copy.deepcopy(BUTTON_COFFEEMAKER) + for description in BUTTON_COFFEEMAKER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + if (device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + BUTTON_COFFEEMAKER_LC = copy.deepcopy(BUTTON_COFFEEMAKER) + for description in BUTTON_COFFEEMAKER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + if (device_type in POLARIS_CLIMATE_TYPE): + if (device_type == "859"): + BUTTON_CLIMATES_200_LC = copy.deepcopy(BUTTON_CLIMATES_200) + for description in BUTTON_CLIMATES_200_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + else: + BUTTON_CLIMATES_LC = copy.deepcopy(BUTTON_CLIMATES) + for description in BUTTON_CLIMATES_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + if (device_type in POLARIS_AIRCLEANER_TYPE): + BUTTON_AIRCLEANER_LC = copy.deepcopy(BUTTON_AIRCLEANER) + for description in BUTTON_AIRCLEANER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + buttonList.append( + PolarisButton( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + device_prefix_topic=device_prefix_topic, + ) + ) + async_add_entities(buttonList, update_before_add=True) + + +class PolarisButton(PolarisBaseEntity, ButtonEntity): + + entity_description: PolarisButtonDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisButtonEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + device_prefix_topic: str | None = None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_available = False + self._attr_has_entity_name = True + self.device_prefix_topic = device_prefix_topic + + + + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker": + self._select_options = json.loads(json.dumps(SELECT_COOKER[0].options)) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if int(self.device_type) == 45: + self._select_options = json.loads(json.dumps(SELECT_COFFEEMAKER_ROG[0].options)) + else: + self._select_options = json.loads(json.dumps(SELECT_COFFEEMAKER[0].options)) + + self._custom_data_select = self._read_file() + if self._custom_data_select is not None: + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker" and "SELECT_COOKER_options" in self._custom_data_select: +# self._select_options = json.loads(json.dumps(SELECT_COOKER[0].options)) + for key, value in self._custom_data_select["SELECT_COOKER_options"].items(): + self._select_options[key] = json.dumps([value]) +# _LOGGER.debug("cooker %s", self._select_options) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if int(self.device_type) == 45 and "SELECT_COFFEEMAKER_ROG_options" in self._custom_data_select: +# self._select_options = json.loads(json.dumps(SELECT_COFFEEMAKER_ROG[0].options)) + for key, value in self._custom_data_select["SELECT_COFFEEMAKER_ROG_options"].items(): + self._select_options[key] = json.dumps([value]) +# _LOGGER.debug("coffee_rog %s", self._select_options) + elif "SELECT_COFFEEMAKER_options" in self._custom_data_select: +# self._select_options = json.loads(json.dumps(SELECT_COFFEEMAKER[0].options)) + for key, value in self._custom_data_select["SELECT_COFFEEMAKER_options"].items(): + self._select_options[key] = json.dumps([value]) +# _LOGGER.debug("coffee %s", self._select_options) + + + # self._attr_options = list(self._select_options.keys()) + # self._attr_current_option = self._attr_options[0] + + + + async def async_added_to_hass(self): + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + def _read_file(self): + file_path = CUSTOM_SELECT_FILE_PATH + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + content = json.loads(file.read()) + else: + content = None + return content + + + def get_state_by_unique_id(self, entity_domain, entity_name): + entity_unique_id = f"{self.device_id}_{entity_name}" + entity_registry = er.async_get(self.hass) + entity_id = entity_registry.async_get_entity_id(entity_domain, "polaris", entity_unique_id) + return self.hass.states.get(entity_id).state + + + async def async_press(self) -> None: + if (self.device_type in POLARIS_COFFEEMAKER_TYPE): + if self.entity_description.key == "button_stop": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"mode", "0") + + # Get entity_id by unique_id + # Работает + zzzz = self.get_state_by_unique_id("number", "amount") + _LOGGER.debug("state %s", zzzz) + + else: + state_amount = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_amount").state + state_weight = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_weight").state + state_tank = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_tank").state + state_pressure = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_pressure").state + state_speed = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_speed").state + state_temp = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_temperature").state + state_mode = self.hass.states.get(f"select.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_select_mode_cofeemaker").state + state_coffee_maker = self.hass.states.get(f"switch.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_power").state + if state_coffee_maker != "off": + if state_amount != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"amount", state_amount) + if state_weight != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"weight", state_weight) + if state_tank != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"tank", state_tank) + if state_pressure != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"pressure", state_pressure) + if state_speed != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"speed", state_speed) + if state_temp != "unavailable": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"temperature", state_temp) + if state_mode != "not_selected": + # !!! + command_mode = self._select_options[state_mode] + coffee_mode = json.loads(command_mode) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"mode", coffee_mode[0]["mode"]) + + if (self.device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + if self.entity_description.key == "button_stop": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"mode", "0") + else: + state_amount = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_amount").state + state_tank = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_tank").state + state_temp = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_temperature").state + state_mode = self.hass.states.get(f"select.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_select_mode_cofeemaker").state + state_cappuccinator = self.hass.states.get(f"binary_sensor.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_cappuccinator").state + state_power = self.hass.states.get(f"switch.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_power").state + if state_power == "off": + mqtt.publish(self.hass, f"{self.mqtt_root}/{self.device_prefix_topic}/control/mode", 5) + if state_cappuccinator == "off" and state_mode in ("cappuccino", "double_cappuccino", "latte", "double_latte", "flat_white", "hot_milk"): + mqtt.publish(self.hass, f"{self.mqtt_root}/{self.device_prefix_topic}/state/error/code", "99") + return + elif state_mode == "not_selected": + mqtt.publish(self.hass, f"{self.mqtt_root}/{self.device_prefix_topic}/state/error/code", "98") + return + else: + if state_amount == "unavailable": + state_amount = "0" + if state_tank == "unavailable": + state_tank = "0" + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"amount", state_amount) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"temperature", state_temp) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"tank", state_tank) + # !!! + command_mode = self._select_options[state_mode] + coffee_mode = json.loads(command_mode) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand+"mode", coffee_mode[0]["mode"]) + mqtt.publish(self.hass, f"{self.mqtt_root}/{self.device_prefix_topic}/state/error/code", "00") + + + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker": + if self.entity_description.key == "button_stop": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, "[]") + else: + state_temp = self.hass.states.get(f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_set_temperature").state + state_time = self.hass.states.get(f"time.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_cooking_time").state + state_time_obj = datetime.strptime(state_time, "%H:%M:%S") + state_time_seconds = state_time_obj.hour * 3600 + state_time_obj.minute * 60 + state_time_obj.second + state_mode = self.hass.states.get(f"select.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_select_mode_cooker").state + # !!! + command_mode = self._select_options[state_mode] + cook_mode = json.loads(command_mode) + payload = "[{" + f'"mode":{cook_mode[0]["mode"]}, "time":{state_time_seconds}, "temperature":{state_temp}' + "}]" + state_time_delay = self.hass.states.get(f"time.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_delay_start").state + state_time_delay_obj = datetime.strptime(state_time_delay, "%H:%M:%S") + state_time_delay_seconds = state_time_delay_obj.hour * 3600 + state_time_delay_obj.minute * 60 + state_time_delay_obj.second + if state_time_delay_seconds > 59: + mqtt.publish(self.hass, f"{self.mqtt_root}/{self.device_prefix_topic}/control/delay_start", state_time_delay_seconds) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, payload) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "humidifier": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, self.entity_description.payloads) + if (self.device_type in POLARIS_CLIMATE_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, self.entity_description.payloads) + if (self.device_type in POLARIS_AIRCLEANER_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, self.entity_description.payloads) diff --git a/homeassistant/config/custom_components/polaris/climate.py b/homeassistant/config/custom_components/polaris/climate.py new file mode 100644 index 0000000..fa9fafc --- /dev/null +++ b/homeassistant/config/custom_components/polaris/climate.py @@ -0,0 +1,493 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.components.climate import ( + DOMAIN, + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + CLIMATES, + CLIMATES_200, + AIRCLEANER, + AIRCLEANER_EAP, + CLIMATES_HEATER, + CLIMATES_AIRCONDITIONER, + CLIMATES_THERMOSTAT, + PolarisClimateEntityDescription, + POLARIS_CLIMATE_TYPE, + POLARIS_AIRCLEANER_TYPE, + POLARIS_AIRCLEANER_EAP_TYPE, + POLARIS_HEATER_TYPE, + POLARIS_AIRCONDITIONER_TYPE, + POLARIS_THERMOSTAT_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + climateList = [] + + if (device_type in POLARIS_CLIMATE_TYPE): + # Create humidifier + if (device_type == "859"): + CLIMATES_LC = copy.deepcopy(CLIMATES_200) + else: + CLIMATES_LC = copy.deepcopy(CLIMATES) + for description in CLIMATES_LC: + description.mqttTopicStateTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateTemperature}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_TYPE): + # Create humidifier + AIRCLEANER_LC = copy.deepcopy(AIRCLEANER) + for description in AIRCLEANER_LC: + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_EAP_TYPE): + # Create aircleaner + AIRCLEANER_EAP_LC = copy.deepcopy(AIRCLEANER_EAP) + for description in AIRCLEANER_EAP_LC: + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HEATER_TYPE): + # Create Heater + CLIMATES_HEATER_LC = copy.deepcopy(CLIMATES_HEATER) + for description in CLIMATES_HEATER_LC: + description.mqttTopicStateTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateTemperature}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCONDITIONER_TYPE): + # Create AIRCONDITIONER + CLIMATES_AIRCONDITIONER_LC = copy.deepcopy(CLIMATES_AIRCONDITIONER) + for description in CLIMATES_AIRCONDITIONER_LC: + description.mqttTopicStateTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateTemperature}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.mqttTopicStateSwingMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateSwingMode}" + description.mqttTopicCommandSwingMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandSwingMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_THERMOSTAT_TYPE): + # Create THERMOSTAT + CLIMATES_THERMOSTAT_LC = copy.deepcopy(CLIMATES_THERMOSTAT) + for description in CLIMATES_THERMOSTAT_LC: + description.mqttTopicStateTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateTemperature}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicCommandPower = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPower}" + description.mqttTopicCurrentPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentPresetMode}" + description.mqttTopicCommandPresetMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandPresetMode}" + description.device_prefix_topic = device_prefix_topic + climateList.append( + PolarisClimate( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(climateList, update_before_add=True) + + +class PolarisClimate(PolarisBaseEntity, ClimateEntity): + + entity_description: PolarisClimateEntityDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisClimateEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + device_class: ClimateDeviceClass | None = None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self.payload_on=description.payload_on + self.payload_off=description.payload_off + self._attr_has_entity_name = True + + self._attr_precision = 1.0 + self._attr_target_temperature_step = 1.0 + self._attr_hvac_modes = self.entity_description.hvac_modes + self._attr_preset_modes = list(self.entity_description.preset_modes.keys()) + if device_type in {"806","847"}: + self.entity_description.fan_modes = {"auto": "0", "20_5_percent": "1", "40_5_percent": "2", "60_5_percent": "3", "80_5_percent": "4", "100_5_percent": "5"} + if device_type == "820": + self.entity_description.fan_modes = {"auto": "0", "low": "1", "middle": "2", "high": "3"} + if self.entity_description.fan_modes is not None: + self._attr_fan_modes = list(self.entity_description.fan_modes.keys()) + self._attr_fan_mode = self.entity_description.fan_mode + self._attr_supported_features = self.entity_description.supported_features + self._enable_turn_on_off_backwards_compatibility = False + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + + self._attr_precision = self.entity_description.temp_step + self._attr_target_temperature = 20 + if device_type == "820": + self._attr_max_temp = 32 + else: + self._attr_max_temp = self.entity_description.max_temp + self._attr_min_temp = self.entity_description.min_temp + self._attr_preset_mode = self.entity_description.preset_mode + self._attr_hvac_mode = HVACMode.OFF + self._attr_available = False + + if self.entity_description.swing_mode is not None: + self._attr_swing_mode = self.entity_description.swing_mode + self._attr_swing_modes = list(self.entity_description.swing_modes.keys()) +# self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + self._swing_message = "00000000" + if device_type == "826": + self._EAP_data0 = "0000" + if device_type == "882": + self._swing_message = "000000000000" + + async def async_added_to_hass(self): + @callback + def message_received_curr_temp(message): + self._attr_current_temperature = float(message.payload) + self.async_write_ha_state() + @callback + def message_received_targ_temp(message): + if float(message.payload) < self._attr_min_temp: + self._attr_target_temperature = self._attr_min_temp + else: + self._attr_target_temperature = float(message.payload) + self.async_write_ha_state() + @callback + def message_received_mode(message): + payload = message.payload + if int(payload)==0: + self._attr_hvac_mode = HVACMode.OFF + elif (self.device_type in POLARIS_AIRCLEANER_TYPE): + self._attr_hvac_mode = HVACMode.DRY + elif (self.device_type in POLARIS_AIRCLEANER_EAP_TYPE): + self._attr_hvac_mode = HVACMode.DRY + elif (self.device_type in POLARIS_HEATER_TYPE): + self._attr_hvac_mode = HVACMode.HEAT + elif (self.device_type in POLARIS_THERMOSTAT_TYPE): + self._attr_hvac_mode = HVACMode.HEAT + elif (self.device_type in POLARIS_AIRCONDITIONER_TYPE): + match int(payload): + case 1: + self._attr_hvac_mode = HVACMode.AUTO + case 3: + self._attr_hvac_mode = HVACMode.DRY + case 5: + self._attr_hvac_mode = HVACMode.FAN_ONLY + case 4: + self._attr_hvac_mode = HVACMode.HEAT + case 2: + self._attr_hvac_mode = HVACMode.COOL + else: + self._attr_hvac_mode = HVACMode.FAN_ONLY + self.async_write_ha_state() + @callback + def message_received_preset_mode(message): + payload = message.payload + if int(payload) > 0: + self._attr_preset_mode = list(self.entity_description.preset_modes.keys())[list(self.entity_description.preset_modes.values()).index(payload)] + if self.device_type == "826": + self._EAP_data0 = "0" + payload + self._EAP_data0[-2:] + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode.replace("mode", "program_data/0"), self._EAP_data0) + self.async_write_ha_state() + @callback + def message_received_fan_mode(message): + self._attr_fan_mode = list(self.entity_description.fan_modes.keys())[list(self.entity_description.fan_modes.values()).index(message.payload)] + self.async_write_ha_state() + @callback + def message_received_swing_mode(message): +# _LOGGER.debug("swing message %s", message.payload) + self._swing_message = message.payload # 00112233 00-качание верх/низ 11-качание влево/вправо 22-эко режим охлаждения 33-автообогрев + match self._swing_message[:4]: + case "0000": + swmode = "off" + case "0001": + swmode = "horizontal" + case "0100": + swmode = "vertical" + case "0101": + swmode = "both" +# _LOGGER.debug("swing mode %s", swmode) + self._attr_swing_mode = swmode + self.async_write_ha_state() + + @callback + def EAP_data_message_received(message): + self._EAP_data0 = message.payload +# _LOGGER.debug("EAP mode data0 message %s", self._EAP_data0) + + + if self.device_type == "826": + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentPresetMode.replace("mode", "program_data/0"), + EAP_data_message_received, + 1, + ) + + + if self.entity_description.mqttTopicCurrentTemperature is not None: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentTemperature, + message_received_curr_temp, + 1, + ) + if self.entity_description.mqttTopicStateTemperature is not None: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicStateTemperature, + message_received_targ_temp, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentPresetMode, + message_received_mode, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentPresetMode, + message_received_preset_mode, + 1, + ) + if self.entity_description.mqttTopicStateFanMode is not None: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicStateFanMode, + message_received_fan_mode, + 1, + ) + if self.entity_description.mqttTopicStateSwingMode is not None: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicStateSwingMode, + message_received_swing_mode, + 1, + ) + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + async def async_turn_on(self) -> None: + if (self.device_type in POLARIS_AIRCLEANER_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 1) + await self.async_set_hvac_mode(HVACMode.DRY) + elif (self.device_type in POLARIS_AIRCLEANER_EAP_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 1) + await self.async_set_hvac_mode(HVACMode.DRY) + elif (self.device_type in POLARIS_HEATER_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 1) + await self.async_set_hvac_mode(HVACMode.HEAT) + elif (self.device_type in POLARIS_THERMOSTAT_TYPE): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 1) + await self.async_set_hvac_mode(HVACMode.HEAT) + else: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 5) + await self.async_set_hvac_mode(HVACMode.FAN_ONLY) + + async def async_turn_off(self) -> None: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 0) + await self.async_set_hvac_mode(HVACMode.OFF) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperatures.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) + if (kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None and kwargs.get(ATTR_TARGET_TEMP_LOW) is not None): + self._attr_target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + self._attr_target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: + self._attr_hvac_mode = hvac_mode + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandTemperature, int(self._attr_target_temperature)) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new fan mode.""" + if (fan_mode == "9_speed"): + fan_mode = "8_speed" + if (self.device_type == "826" and fan_mode == "off"): + fan_mode = "low" + self._attr_fan_mode = fan_mode + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandFanMode, self.entity_description.fan_modes[fan_mode]) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new operation mode.""" + self._attr_hvac_mode = hvac_mode + if hvac_mode == "off": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode, 0) + elif ((self.device_type in POLARIS_AIRCLEANER_TYPE) or (self.device_type in POLARIS_HEATER_TYPE) or (self.device_type in POLARIS_AIRCLEANER_EAP_TYPE)): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPower, 1) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandFanMode, self.entity_description.fan_modes[self._attr_fan_mode]) # add for on fan after off heater + elif (self.device_type in POLARIS_AIRCONDITIONER_TYPE): + match hvac_mode: + case "auto": + command = 1 + case "dry": + command = 3 + case "fan_only": + command = 5 + case "heat": + command = 4 + case "cool": + command = 2 + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode, command) + else: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode, 5) # 5 = FAN_ONLY for AIRCLEANER and HEATER_TYPE + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Update preset_mode on.""" + self._attr_preset_mode = preset_mode + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode, self.entity_description.preset_modes[preset_mode]) + if self.device_type == "826": + if (preset_mode != "night" and int(self._EAP_data0[-2:]) > 1): + self._EAP_data0 = "0" + self.entity_description.preset_modes[preset_mode] + "01" + else: + self._EAP_data0 = "0" + self.entity_description.preset_modes[preset_mode] + self._EAP_data0[-2:] +# _LOGGER.debug("EAP data0 mode select %s", self._EAP_data0) # отправить в prog_data + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandPresetMode.replace("mode", "program_data/0"), self._EAP_data0) + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + self._attr_swing_mode = swing_mode +# _LOGGER.debug("set swing mode %s", swing_mode) + match swing_mode: + case "off": + swmessage = "0000" + case "horizontal": + swmessage = "0001" + case "vertical": + swmessage = "0100" + case "both": + swmessage = "0101" + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandSwingMode, swmessage + self._swing_message[4:]) + self.async_write_ha_state() \ No newline at end of file diff --git a/homeassistant/config/custom_components/polaris/common.py b/homeassistant/config/custom_components/polaris/common.py new file mode 100644 index 0000000..522ad56 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/common.py @@ -0,0 +1,35 @@ +"""The Polaris IQ Home component.""" + +import logging + +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN, MANUFACTURER, POLARIS_DEVICE + +#_LOGGER = logging.getLogger(__name__) +#_LOGGER.setLevel(logging.DEBUG) + +class PolarisBaseEntity: + + deviceID: str | None = None + + def __init__( + self, + device_friendly_name: str, + mqtt_root: str, + device_type: str, + device_id: str + ) -> None: + self.device_friendly_name = device_friendly_name + self.mqtt_root = mqtt_root + self.device_type=device_type + self.device_id=device_id + + + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + name=POLARIS_DEVICE[int(self.device_type)]["class"]+" "+ POLARIS_DEVICE[int(self.device_type)]["model"], + identifiers={(DOMAIN, self.device_id, self.mqtt_root)}, + manufacturer=MANUFACTURER, + model=POLARIS_DEVICE[int(self.device_type)]["class"]+" - "+ POLARIS_DEVICE[int(self.device_type)]["model"], + ) diff --git a/homeassistant/config/custom_components/polaris/config_flow.py b/homeassistant/config/custom_components/polaris/config_flow.py new file mode 100644 index 0000000..4c03fa3 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/config_flow.py @@ -0,0 +1,194 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import asyncio +import logging +import time +import voluptuous as vol +from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant, callback +from homeassistant.components import mqtt +from homeassistant.helpers.translation import async_get_translations + +import homeassistant.helpers.config_validation as cv + +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import DEVICEID, DEVICETYPE, DOMAIN, MQTT_ROOT_TOPIC, MQTT_ROOT_TOPIC_DEFAULT, POLARIS_DEVICE +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + +class PolarisConfigFlow(ConfigFlow, domain=DOMAIN): + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + self._serial_number = None + self._topic_prefix = {} + self._device_found = {} + self._device_prefix_topic = {} + self._unknown_devtype = 0 + + + async def _get_devtypes_from_mqtt(self): + await mqtt.async_subscribe(self.hass, "polaris/+/state/mac", self._mqtt_message_newdev) + await mqtt.async_subscribe(self.hass, "polaris/+/+/state/mac", self._mqtt_message_olddev) + await mqtt.async_subscribe(self.hass, "rusclimate/+/+/state/mac", self._mqtt_message_rusclidev) + + @callback + async def _mqtt_message_newdev(self, message: ReceiveMessage): + topic = message.topic + device_id = topic.split("/")[1] + topic_check = f"polaris/{device_id}/state/devtype" + topic_bool = await self.topic_exists(topic_check) + if not topic_bool: + self._device_type = "0" + if int(self._device_type) not in POLARIS_DEVICE: +# _LOGGER.debug("newdevice unknown - %s", self._device_type) + self._unknown_devtype = int(self._device_type) + if device_id not in self._device_found: + self._device_found[device_id] = self._device_type + self._device_prefix_topic[device_id] = device_id + self._topic_prefix[device_id] = "polaris" + + @callback + async def _mqtt_message_olddev(self, message: ReceiveMessage): + topic = message.topic + device_id = message.payload + device_type = topic.split("/")[1] + device_oldid = topic.split("/")[2] + if int(device_type) not in POLARIS_DEVICE: +# _LOGGER.debug("olddevice unknown - %s", device_type) + self._unknown_devtype = int(device_type) + if device_id not in self._device_found: + self._device_found[device_id] = device_type + self._device_prefix_topic[device_id] = f"{device_type}/{device_oldid}" + self._topic_prefix[device_id] = "polaris" + + @callback + async def _mqtt_message_rusclidev(self, message: ReceiveMessage): + topic = message.topic + device_id = message.payload + device_type = str(int(topic.split("/")[1]) + 800) + device_oldid = topic.split("/")[2] + if int(device_type) not in POLARIS_DEVICE: +# _LOGGER.debug("rusclidevice unknown - %s", device_type) + self._unknown_devtype = int(device_type) + if device_id not in self._device_found: + self._device_found[device_id] = device_type + self._device_prefix_topic[device_id] = f"{topic.split("/")[1]}/{device_oldid}" + self._topic_prefix[device_id] = "rusclimate" + + async def topic_exists(self, topic: str, timeout: float = 1.0) -> bool: + loop = asyncio.get_running_loop() + future: asyncio.Future = loop.create_future() + + def message_received(msg): + if not future.done(): + future.set_result(msg) + unsub = await mqtt.async_subscribe(self.hass, topic, message_received, qos=0) + try: + msg = await asyncio.wait_for(future, timeout) + self._device_type = msg.payload + except asyncio.TimeoutError: + return False + finally: + unsub() + return bool(msg.retain) + + async def get_translated_type(self, device_type_lang): + language = self.hass.config.language + translations = await async_get_translations(self.hass, language, "common", {DOMAIN}) + key = f"component.polaris.common.{device_type_lang}" + return translations.get(key, device_type_lang) + + async def async_step_user(self, user_input=None): + await self._get_devtypes_from_mqtt() + await self.hass.async_add_executor_job(time.sleep, 2) + errors = {} + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(DEVICEID): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict( + value=dev_mac, + label=f"{POLARIS_DEVICE[int(dev_type) if int(dev_type) in POLARIS_DEVICE else 0]["model"]} (mac: {dev_mac})", + ) + for dev_mac, dev_type in self._device_found.items() + ], + mode=SelectSelectorMode.DROPDOWN, + translation_key="config_selector_devicetype", + ) + ), + } + ), + errors=errors, + ) + user_input[MQTT_ROOT_TOPIC] = self._topic_prefix[user_input[DEVICEID]] + user_input[DEVICETYPE] = self._device_found[user_input[DEVICEID]] + user_input["DEVPREFIXTOPIC"] = self._device_prefix_topic[user_input[DEVICEID]] + self.oldstep = user_input +# _LOGGER.debug("input1 %s", user_input) + if user_input[DEVICETYPE] == "0" or self._unknown_devtype > 0: + return self.async_show_form( + step_id="undef", + data_schema=vol.Schema( + { + vol.Required(DEVICETYPE): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict( + value=str(dev_id), + label=f'{str(dev_id)} {await self.get_translated_type(POLARIS_DEVICE[int(dev_id)]["class"])} ({POLARIS_DEVICE[int(dev_id)]["model"]})', + ) + for dev_id in POLARIS_DEVICE + ], + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ), + ) + +# _LOGGER.debug("input3 %s", user_input) + title = f"{POLARIS_DEVICE[int(user_input[DEVICETYPE])]['class']}-{POLARIS_DEVICE[int(user_input[DEVICETYPE])]['model']}-{user_input[DEVICEID]}" + await self.async_set_unique_id(title) + self._abort_if_unique_id_configured(error="already_configured") + # Create entities + return self.async_create_entry( + title=title, + data=user_input, + ) + + async def async_step_undef(self, user_input): + if user_input[DEVICETYPE] == "0": + error = "no_dev" + return self.async_abort(reason="not_supported") + self.oldstep[DEVICETYPE] = user_input[DEVICETYPE] + user_input = self.oldstep + user_input[MQTT_ROOT_TOPIC] = self._topic_prefix[user_input[DEVICEID]] +# _LOGGER.debug("input2 %s", user_input) + mqtt.publish(self.hass, f'{user_input[MQTT_ROOT_TOPIC]}/{user_input["DEVPREFIXTOPIC"]}/state/devtype', user_input[DEVICETYPE], 0, True) + title = f"{POLARIS_DEVICE[int(user_input[DEVICETYPE])]['class']}-{POLARIS_DEVICE[int(user_input[DEVICETYPE])]['model']}-{user_input[DEVICEID]}" + await self.async_set_unique_id(title) + self._abort_if_unique_id_configured(error="already_configured") + # Create entities + return self.async_create_entry( + title=title, + data=user_input, + ) + diff --git a/homeassistant/config/custom_components/polaris/const.py b/homeassistant/config/custom_components/polaris/const.py new file mode 100644 index 0000000..4d8c51e --- /dev/null +++ b/homeassistant/config/custom_components/polaris/const.py @@ -0,0 +1,3524 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import datetime +import json +from zoneinfo import ZoneInfo + +import voluptuous as vol + +from homeassistant.components.image import Image, ImageEntityDescription +from homeassistant.components.vacuum import ( + DOMAIN, + ATTR_CLEANED_AREA, + StateVacuumEntity, +# VacuumActivity, + VacuumEntityFeature, +) +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + ClimateEntityDescription, + HVACMode, +) +from homeassistant.components.time import TimeEntity, TimeEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.select import SelectEntityDescription +from homeassistant.components.number import NumberDeviceClass, NumberEntityDescription +from homeassistant.components.select import SelectEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntityDescription, +) +from homeassistant.components.water_heater import WaterHeaterEntity, WaterHeaterEntityDescription # removed in HA Core 2026.1 +from homeassistant.components.humidifier import HumidifierEntity, HumidifierEntityDescription, HumidifierDeviceClass +from homeassistant.const import ( + PERCENTAGE, + UnitOfTemperature, + UnitOfMass, + SIGNAL_STRENGTH_DECIBELS, + CONCENTRATION_PARTS_PER_MILLION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + UnitOfTime, + UnitOfVolume, + UnitOfEnergy, + UnitOfPower, + Platform, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +PLATFORMS = [ + Platform.SELECT, + Platform.SENSOR, + Platform.HUMIDIFIER, + Platform.WATER_HEATER, + Platform.SWITCH, + Platform.NUMBER, + Platform.LIGHT, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.TIME, + Platform.CLIMATE, + Platform.VACUUM +# Platform.IMAGE +] + +# Global values +DOMAIN = "polaris" +MQTT_ROOT_TOPIC = "MQTT_ROOT_TOPIC" +MQTT_ROOT_TOPIC_DEFAULT = "polaris" +DEVICETYPE = "DEVICETYPE" +DEVICEID = "DEVICEID" +MANUFACTURER = "Polaris IQ Home" +CUSTOM_SELECT_FILE_PATH = "www/polaris/polaris_custom_select.js" + +POLARIS_DEVICE = { + 0: {"model": "Unknown", "class": "all"}, + 37: {"model": "PWK-7111CGLD-WIFI-(old)", "class": "kettle"}, + 245: {"model": "PWK-0105", "class": "kettle"}, + 205: {"model": "PWK-1538CC", "class": "kettle"}, + 271: {"model": "PWK-1701CGLD", "class": "kettle"}, + 36: {"model": "PWK-17107CGLD-WIFI-(old)", "class": "kettle"}, + 29: {"model": "PWK-1712CGLD", "class": "kettle"}, + 38: {"model": "PWK-1712CGLD", "class": "kettle"}, + 54: {"model": "PWK-1712CGLD", "class": "kettle"}, + 59: {"model": "PWK-1712CGLD", "class": "kettle"}, + 63: {"model": "PWK-1712CGLD", "class": "kettle"}, + 97: {"model": "PWK-1712CGLD", "class": "kettle"}, + 117: {"model": "PWK-1712CGLD", "class": "kettle"}, + 208: {"model": "PWK-1712CGLD", "class": "kettle"}, + 83: {"model": "PWK-1712CGLD-RGB", "class": "kettle"}, + 253: {"model": "PWK-1715", "class": "kettle"}, + 244: {"model": "PWK-1716CGLD", "class": "kettle"}, + 67: {"model": "PWK-1720CGLD", "class": "kettle"}, + 84: {"model": "PWK-1720CGLD-RGB", "class": "kettle"}, + 6: {"model": "PWK-1725CGLD", "class": "kettle"}, + 52: {"model": "PWK-1725CGLD", "class": "kettle"}, + 57: {"model": "PWK-1725CGLD", "class": "kettle"}, + 61: {"model": "PWK-1725CGLD", "class": "kettle"}, + 82: {"model": "PWK-1725CGLD", "class": "kettle"}, + 86: {"model": "PWK-1725CGLD", "class": "kettle"}, + 105: {"model": "PWK-1725CGLD", "class": "kettle"}, + 106: {"model": "PWK-1725CGLD", "class": "kettle"}, + 177: {"model": "PWK-1725CGLD", "class": "kettle"}, + 194: {"model": "PWK-1725CGLD", "class": "kettle"}, + 196: {"model": "PWK-1725CGLD", "class": "kettle"}, + 164: {"model": "PWK-1728CGLDA", "class": "kettle"}, + 209: {"model": "PWK-1729CAD", "class": "kettle"}, + 189: {"model": "PWK-1746CA", "class": "kettle"}, + 260: {"model": "PWK-1746CA", "class": "kettle"}, + 308: {"model": "PWK-1746CA", "class": "kettle"}, + 8: {"model": "PWK-1755CAD", "class": "kettle"}, + 53: {"model": "PWK-1755CAD", "class": "kettle"}, + 58: {"model": "PWK-1755CAD", "class": "kettle"}, + 62: {"model": "PWK-1755CAD", "class": "kettle"}, + 185: {"model": "PWK-1755CAD", "class": "kettle"}, + 165: {"model": "PWK-1755CAD-VOICE", "class": "kettle"}, + 121: {"model": "PWK-1774CAD", "class": "kettle"}, + 2: {"model": "PWK-1775CGLD", "class": "kettle"}, + 51: {"model": "PWK-1775CGLD", "class": "kettle"}, + 56: {"model": "PWK-1775CGLD", "class": "kettle"}, + 60: {"model": "PWK-1775CGLD", "class": "kettle"}, + 98: {"model": "PWK-1775CGLD", "class": "kettle"}, + 188: {"model": "PWK-1775CGLD", "class": "kettle"}, + 223: {"model": "PWK-1775CGLD", "class": "kettle"}, + 262: {"model": "PWK-1775CGLD", "class": "kettle"}, + 263: {"model": "PWK-1775CGLD", "class": "kettle"}, + 275: {"model": "PWK-1775CGLD", "class": "kettle"}, + 294: {"model": "PWK-1775CGLD", "class": "kettle"}, + 85: {"model": "PWK-1775CGLD-SMART", "class": "kettle"}, + 139: {"model": "PWK-1775CGLD-VOICE", "class": "kettle"}, + 175: {"model": "PWK-1823CGLD", "class": "kettle"}, + 254: {"model": "PWK-1823CGLD", "class": "kettle"}, + 176: {"model": "PWK-1841CGLD", "class": "kettle"}, + 255: {"model": "PWK-1841CGLD", "class": "kettle"}, + 147: {"model": "PUH-0205", "class": "humidifier"}, + 71: {"model": "PUH-1010", "class": "humidifier"}, + 72: {"model": "PUH-2300", "class": "humidifier"}, + 73: {"model": "PUH-3030", "class": "humidifier"}, + 75: {"model": "PUH-4040", "class": "humidifier"}, + 99: {"model": "PUH-4040", "class": "humidifier"}, + 153: {"model": "PUH-4055", "class": "humidifier"}, + 137: {"model": "PUH-4066", "class": "humidifier"}, + 157: {"model": "PUH-4550", "class": "humidifier"}, + 158: {"model": "PUH-6060", "class": "humidifier"}, + 25: {"model": "PUH-6090", "class": "humidifier"}, + 15: {"model": "PUH-7406", "class": "humidifier"}, + 87: {"model": "PUH-8080/PUH-4606", "class": "humidifier"}, + 155: {"model": "PUH-8802", "class": "humidifier"}, + 74: {"model": "PUH-9009", "class": "humidifier"}, + 4: {"model": "PUH-9105/PUH-2709", "class": "humidifier"}, + 17: {"model": "PUH-9105/PUH-2709", "class": "humidifier"}, + 18: {"model": "PUH-9105/PUH-2709", "class": "humidifier"}, + 44: {"model": "PUH-9105/PUH-2709", "class": "humidifier"}, + 70: {"model": "PUH-9105/PUH-2709", "class": "humidifier"}, + 1: {"model": "EVO-0225", "class": "cooker"}, + 95: {"model": "PMC-00000", "class": "cooker"}, + 303: {"model": "PMC-0510", "class": "cooker"}, + 301: {"model": "PMC-0515", "class": "cooker"}, + 10: {"model": "PMC-0521WIFI", "class": "cooker"}, + 41: {"model": "PMC-0521WIFI", "class": "cooker"}, + 267: {"model": "PMC-0521WIFI", "class": "cooker"}, + 55: {"model": "PMC-0524WIFI", "class": "cooker"}, + 206: {"model": "PMC-0524WIFI", "class": "cooker"}, + 9: {"model": "PMC-0526WIFI", "class": "cooker"}, + 40: {"model": "PMC-0526WIFI", "class": "cooker"}, + 138: {"model": "PMC-0526WIFI", "class": "cooker"}, + 39: {"model": "PMC-0528WIFI", "class": "cooker"}, + 48: {"model": "PMC-0528WIFI", "class": "cooker"}, + 268: {"model": "PMC-0528WIFI", "class": "cooker"}, + 47: {"model": "PMC-0530WIFI", "class": "cooker"}, + 270: {"model": "PMC-0530WIFI", "class": "cooker"}, + 210: {"model": "PMC-0590AD", "class": "cooker"}, + 302: {"model": "PMC-0597", "class": "cooker"}, + 215: {"model": "PMC-5001WIFI", "class": "cooker"}, + 79: {"model": "PMC-5017WIFI", "class": "cooker"}, + 192: {"model": "PMC-5017WIFI", "class": "cooker"}, + 80: {"model": "PMC-5020WIFI", "class": "cooker"}, + 266: {"model": "PMC-5020WIFI", "class": "cooker"}, + 77: {"model": "PMC-5040WIFI", "class": "cooker"}, + 78: {"model": "PMC-5050WIFI", "class": "cooker"}, + 89: {"model": "PMC-5055WIFI", "class": "cooker"}, + 114: {"model": "PMC-5060-Smart-Motion", "class": "cooker"}, + 240: {"model": "PMC-5060-Smart-Motion", "class": "cooker"}, + 162: {"model": "PMC-5063WIFI", "class": "cooker"}, + 169: {"model": "PPC-1505-WiFI", "class": "cooker"}, + 183: {"model": "PPC-1505-WiFI", "class": "cooker"}, + 235: {"model": "AM7310-(test)", "class": "coffeemaker"}, + 305: {"model": "PACM-2072", "class": "coffeemaker"}, + 103: {"model": "PACM-2080AC", "class": "coffeemaker"}, + 261: {"model": "PACM-2080AC", "class": "coffeemaker"}, + 276: {"model": "PACM-2080AC", "class": "coffeemaker"}, + 277: {"model": "PACM-2080AC", "class": "coffeemaker"}, + 200: {"model": "PACM-2081AC", "class": "coffeemaker"}, + 265: {"model": "PACM-2081AC", "class": "coffeemaker"}, + 280: {"model": "PACM-2081AC", "class": "coffeemaker"}, + 166: {"model": "PACM-2085GC", "class": "coffeemaker"}, + 278: {"model": "PACM-2085GC", "class": "coffeemaker"}, + 247: {"model": "PCM-1255", "class": "coffeemaker"}, + 45: {"model": "PCM-1540WIFI", "class": "coffeemaker"}, + 222: {"model": "PCM-1540WIFI", "class": "coffeemaker"}, + 274: {"model": "PCM-1540WIFI", "class": "coffeemaker"}, + 279: {"model": "PCM-1540WIFI", "class": "coffeemaker"}, + 190: {"model": "PCM-1560", "class": "coffeemaker"}, + 207: {"model": "PCM-2070CG", "class": "coffeemaker"}, + 172: {"model": "PAW-0804", "class": "air-cleaner"}, + 140: {"model": "PAW-0804(c3-test)", "class": "air-cleaner"}, + 151: {"model": "PPA-2025", "class": "air-cleaner"}, + 203: {"model": "PPA-2025", "class": "air-cleaner"}, + 250: {"model": "PPA-2025", "class": "air-cleaner"}, + 152: {"model": "PPA-4050", "class": "air-cleaner"}, + 204: {"model": "PPA-4050", "class": "air-cleaner"}, + 251: {"model": "PPA-4050", "class": "air-cleaner"}, + 236: {"model": "PPAT-02A", "class": "air-cleaner"}, + 238: {"model": "PPAT-80P", "class": "air-cleaner"}, + 239: {"model": "PPAT-90GDi", "class": "air-cleaner"}, + 132: {"model": "PWF-2005", "class": "irrigator"}, + 252: {"model": "PWF-2005", "class": "irrigator"}, + 273: {"model": "PAF-4001WIFI", "class": "air_fryer"}, + 290: {"model": "PAF-6003WIFI", "class": "air_fryer"}, + 291: {"model": "PAF-8003WIFI", "class": "air_fryer"}, + 292: {"model": "PAF-8005WIFI", "class": "air_fryer"}, + 31: {"model": "ENIGMA-WI-FI", "class": "boiler"}, + 11: {"model": "PWH-IDF06", "class": "boiler"}, + 30: {"model": "SIGMA-WIFI", "class": "boiler"}, + 249: {"model": "VEKTOR-WIFI", "class": "boiler"}, + 46: {"model": "PCH-0320WIFI", "class": "heater"}, + 65: {"model": "PCH-0320WIFI", "class": "heater"}, + 16: {"model": "PHV-1401", "class": "heater"}, + 49: {"model": "PMH-21XX", "class": "heater"}, + 64: {"model": "PMH-21XX", "class": "heater"}, + 246: {"model": "PRWC-3001", "class": "cleaner"}, + 101: {"model": "PVCR-0726-Aqua", "class": "cleaner"}, + 108: {"model": "PVCR-0726-GYRO", "class": "cleaner"}, + 21: {"model": "PVCR-0735", "class": "cleaner"}, + 163: {"model": "PVCR-0735", "class": "cleaner"}, + 19: {"model": "PVCR-0833", "class": "cleaner"}, + 43: {"model": "PVCR-0833", "class": "cleaner"}, + 104: {"model": "PVCR-0905", "class": "cleaner"}, + 156: {"model": "PVCR-0905", "class": "cleaner"}, + 107: {"model": "PVCR-0926", "class": "cleaner"}, + 23: {"model": "PVCR-1028", "class": "cleaner"}, + 22: {"model": "PVCR-1050", "class": "cleaner"}, + 102: {"model": "PVCR-1226-Aqua", "class": "cleaner"}, + 109: {"model": "PVCR-1226-GYRO", "class": "cleaner"}, + 24: {"model": "PVCR-1229", "class": "cleaner"}, + 68: {"model": "PVCR-3100", "class": "cleaner"}, + 7: {"model": "PVCR-3200", "class": "cleaner"}, + 76: {"model": "PVCR-3200", "class": "cleaner"}, + 115: {"model": "PVCR-3200", "class": "cleaner"}, + 12: {"model": "PVCR-3300", "class": "cleaner"}, + 81: {"model": "PVCR-3400", "class": "cleaner"}, + 130: {"model": "PVCR-3600", "class": "cleaner"}, + 112: {"model": "PVCR-3700", "class": "cleaner"}, + 88: {"model": "PVCR-3800", "class": "cleaner"}, + 66: {"model": "PVCR-3900", "class": "cleaner"}, + 131: {"model": "PVCR-3900", "class": "cleaner"}, + 113: {"model": "PVCR-4000", "class": "cleaner"}, + 197: {"model": "PVCR-4000", "class": "cleaner"}, + 110: {"model": "PVCR-4105", "class": "cleaner"}, + 127: {"model": "PVCR-4105", "class": "cleaner"}, + 199: {"model": "PVCR-4250", "class": "cleaner"}, + 241: {"model": "PVCR-4250", "class": "cleaner"}, + 211: {"model": "PVCR-4260", "class": "cleaner"}, + 269: {"model": "PVCR-4260", "class": "cleaner"}, + 142: {"model": "PVCR-4500", "class": "cleaner"}, + 195: {"model": "PVCR-4500", "class": "cleaner"}, + 307: {"model": "PVCR-4750", "class": "cleaner"}, + 119: {"model": "PVCR-5001", "class": "cleaner"}, + 146: {"model": "PVCR-5001", "class": "cleaner"}, + 154: {"model": "PVCR-5001", "class": "cleaner"}, + 201: {"model": "PVCR-5003", "class": "cleaner"}, + 242: {"model": "PVCR-5005", "class": "cleaner"}, + 123: {"model": "PVCR-6001", "class": "cleaner"}, + 148: {"model": "PVCR-6001", "class": "cleaner"}, + 221: {"model": "PVCR-6001", "class": "cleaner"}, + 187: {"model": "PVCR-6003", "class": "cleaner"}, + 256: {"model": "PVCR-7026", "class": "cleaner"}, + 128: {"model": "PVCRAC-7050", "class": "cleaner"}, + 212: {"model": "PVCRAC-7290", "class": "cleaner"}, + 178: {"model": "PVCRAC-7750", "class": "cleaner"}, + 198: {"model": "PVCRAC-7790", "class": "cleaner"}, + 264: {"model": "PVCRAC-7790", "class": "cleaner"}, + 126: {"model": "PVCRDC-0101", "class": "cleaner"}, + 160: {"model": "PVCRDC-0101", "class": "cleaner"}, + 124: {"model": "PVCRDC-5002", "class": "cleaner"}, + 149: {"model": "PVCRDC-5002", "class": "cleaner"}, + 213: {"model": "PVCRDC-5002", "class": "cleaner"}, + 202: {"model": "PVCRDC-5004", "class": "cleaner"}, + 181: {"model": "PVCRDC-5006", "class": "cleaner"}, + 125: {"model": "PVCRDC-6002", "class": "cleaner"}, + 150: {"model": "PVCRDC-6002", "class": "cleaner"}, + 186: {"model": "PVCRDC-6004", "class": "cleaner"}, + 257: {"model": "PVCRDC-7028", "class": "cleaner"}, + 217: {"model": "PVCRDC-G2-5002", "class": "cleaner"}, + 218: {"model": "PVCRDC-G2-6002", "class": "cleaner"}, + 133: {"model": "PVCR-G2-0726W", "class": "cleaner"}, + 193: {"model": "PVCR-G2-0826", "class": "cleaner"}, + 134: {"model": "PVCR-G2-0926W", "class": "cleaner"}, + 135: {"model": "PVCR-G2-1226", "class": "cleaner"}, + 129: {"model": "PVCR-G2-3200", "class": "cleaner"}, + 122: {"model": "PVCR-G2-3600", "class": "cleaner"}, + 219: {"model": "PVCR-G2-5001", "class": "cleaner"}, + 220: {"model": "PVCR-G2-6001", "class": "cleaner"}, + 100: {"model": "PVCR-Wave-15", "class": "cleaner"}, + 93: {"model": "PHB-1350-WIFI", "class": "blender"}, + 35: {"model": "PHB-1503-WIFI-(old)", "class": "blender"}, + 34: {"model": "PHB-1551-WIFI", "class": "blender"}, + 282: {"model": "induction-hob", "class": "cooktop"}, + 286: {"model": "XFC302I-B3SF", "class": "cooktop"}, + 288: {"model": "XFC302T-B1D", "class": "cooktop"}, + 285: {"model": "XFC604I", "class": "cooktop"}, + 304: {"model": "XFC604I-(test)", "class": "cooktop"}, + 284: {"model": "XFC604I-B8F", "class": "cooktop"}, + 287: {"model": "XFC604T-B7D", "class": "cooktop"}, + 289: {"model": "XFG640F-B3P", "class": "cooktop"}, + 111: {"model": "PVCS-1150", "class": "cordless_cleaner"}, + 90: {"model": "PVCS-2090", "class": "cordless_cleaner"}, + 136: {"model": "PVCS-4070", "class": "cordless_cleaner"}, + 229: {"model": "PVCS-4070", "class": "cordless_cleaner"}, + 232: {"model": "PVCS-6020", "class": "cordless_cleaner"}, + 281: {"model": "PVCS-6020", "class": "cordless_cleaner"}, + 230: {"model": "PVCS-8200", "class": "cordless_cleaner"}, + 234: {"model": "PVCSDC-3000", "class": "cordless_cleaner"}, + 233: {"model": "PVCSDC-3005", "class": "cordless_cleaner"}, + 306: {"model": "PVCSDC-3005", "class": "cordless_cleaner"}, + 180: {"model": "PSF-3315", "class": "fan"}, + 248: {"model": "PSF-4025", "class": "fan"}, + 179: {"model": "PGP-3010-SMOKELESS", "class": "grill"}, + 96: {"model": "PGP-4001", "class": "grill"}, + 120: {"model": "PHD-4000", "class": "hair_care"}, + 184: {"model": "PHS-1300", "class": "hair_care"}, + 171: {"model": "PHSB-5000DF", "class": "hair_care"}, + 145: {"model": "PHSC-1234", "class": "hair_care"}, + 297: {"model": "8006", "class": "hood"}, + 296: {"model": "8029", "class": "hood"}, + 295: {"model": "6065A-600", "class": "hood"}, + 283: {"model": "PGS-2250VA", "class": "iron"}, + 91: {"model": "PIR-2624AK-3m", "class": "iron"}, + 161: {"model": "PIR-3074SG", "class": "iron"}, + 173: {"model": "PIR-3210AK-3m", "class": "iron"}, + 174: {"model": "PIR-3225AK-3m", "class": "iron"}, + 191: {"model": "PSS-2002K", "class": "iron"}, + 259: {"model": "PSS-8010K", "class": "iron"}, + 159: {"model": "PSS-9090K", "class": "iron"}, + 237: {"model": "SM-8095", "class": "kitchen_machine"}, + 272: {"model": "SM-8095", "class": "kitchen_machine"}, + 32: {"model": "PMG-2580", "class": "meat_grinder"}, + 216: {"model": "PMG-3060", "class": "meat_grinder"}, + 116: {"model": "Smart-Lid", "class": "other"}, + 299: {"model": "xbcook55", "class": "oven"}, + 300: {"model": "xbcook56", "class": "oven"}, + 298: {"model": "xbcook62", "class": "oven"}, + 92: {"model": "PGS-1450CWIFI", "class": "steamer"}, + 94: {"model": "PSS-7070KWIFI", "class": "steamer"}, + 50: {"model": "PETB-0202TC", "class": "toothbrush"}, + 69: {"model": "Ballu-OneAir-ASP-100", "class": "air_cleaner"}, # совместимость с <= v1.0.8 + 869: {"model": "Ballu-OneAir-ASP-100", "class": "air_cleaner"}, + 859: {"model": "Ballu-OneAir-ASP-200", "class": "air_cleaner"}, + 826: {"model": "Electrolux-EAP-2050D/2075D", "class": "air_cleaner"}, + 876: {"model": "Electrolux-Royal-Flash/Centurio-IQ-Inverter", "class": "boiler"}, + 833: {"model": "Electrolux-Centurio-IQ-3.0", "class": "boiler"}, + 802: {"model": "SmartInverter", "class": "boiler"}, + 844: {"model": "Royal-Thermo-Aqua-Inverter/Royal-Thermo-Aqua-Inox-Inverter", "class": "boiler"}, + 806: {"model": "Electrolux-Air-Gate-Transformer-DI-3.0", "class": "heater"}, + 846: {"model": "Electrolux-Air-Gate-Transformer-DI-4.0", "class": "heater"}, + 847: {"model": "Wi-Fi-Convection-Heater", "class": "heater"}, + 820: {"model": "Ballu-Platinum-Evol-DC/Olympio-Legend", "class": "air-conditioner"}, + 813: {"model": "Electrolux-Smartline/Ballu-Eco-Smart/Ice-Peak", "class": "air-conditioner"}, + 882: {"model": "Goldstar-GSAC/GSACI", "class": "air-conditioner"}, + 881: {"model": "UHB-960-ET", "class": "humidifier"}, + 835: {"model": "Electrolux-YOGAhealthline-2.0", "class": "humidifier"}, + 878: {"model": "Electrolux/Royal-Thermo", "class": "thermostat"}, +} + +POLARIS_KETTLE_TYPE = ["2","6","8","29","36","37","38","51","52","53","54","56","57","58","59","60","61","62","63","67","82","83","84","85","86","97","105","106","117","121","139","165","175","176","177","189","194","196","205","209","253","254","255","260","271","308"] +POLARIS_KETTLE_WITH_WEIGHT_TYPE = ["98","164","185","188","208","223","244","245","262","263","275","294"] +POLARIS_KETTLE_WITH_NIGHT_TYPE = ["36","37","86","97","106","117","164","175","176","177","189","194","196","205","208","209","244","253","254","255","260","271","308"] +POLARIS_KETTLE_WITH_BACKLIGHT_TYPE = ["36","37","51","52","53","54","60","61","62","63","67","82","83","84","85","86","97","98","105","106","117","139","164","175","176","177","188","189","194","196","208","209","223","244","245","253","254","255","260","262","263","271","275","294","308"] +POLARIS_KETTLE_WITH_TEA_TIME_MODE_TYPE = ["2","8","51","53","56","58","60","62","85","98","139","165","185","188","205","223","262","263","275","294"] +POLARIS_KETTLE_WITH_KEEP_WITH_WARM_MODE_TYPE = ["205","262","294"] +POLARIS_HUMIDDIFIER_TYPE = ["4","15","17","18","25","44","70","71","72","73","74","75","87","99","137","147","153","155","157","158","835","881"] +POLARIS_HUMIDDIFIER_WITH_IONISER_TYPE = ["4","15","17","18","44","70","72","73","74","137","147","153","155","157","158","835"] +POLARIS_HUMIDDIFIER_WITH_WARM_STREAM_TYPE = ["4","15","17","18","44","70","72","74","147","157","158","835","881"] +POLARIS_HUMIDDIFIER_LOW_FAN_TYPE = ["25","71","72","73","74","75","87","99","137","153","155","157","158"] +POLARIS_HUMIDDIFIER_7_MODE_TYPE = ["17","18","44","70"] +POLARIS_HUMIDDIFIER_5A_MODE_TYPE = ["4"] +POLARIS_HUMIDDIFIER_5B_MODE_TYPE = ["72","74","87","147","155"] +POLARIS_HUMIDDIFIER_4_MODE_TYPE = ["15","71","73","75","99"] +POLARIS_HUMIDDIFIER_3A_MODE_TYPE = ["25"] +POLARIS_HUMIDDIFIER_3B_MODE_TYPE = ["153","157","158"] +POLARIS_HUMIDDIFIER_2_MODE_TYPE = ["881"] +POLARIS_HUMIDDIFIER_1_MODE_TYPE = ["137"] +POLARIS_HUMIDDIFIER_11_MODE_TYPE = ["835"] +POLARIS_COOKER_TYPE = ["1","9","10","39","40","41","47","48","55","77","78","79","80","89","95","114","138","162","169","183","192","206","210","215","240","266","267","268","270","301","302","303"] +POLARIS_COOKER_WITH_LID_TYPE = ["9","39","40","41","47","48","55","77","78","79","80","89","95","114","138","162","169","183","192","206","210","215","240","266","267","268","270","301","302","303"] +POLARIS_COFFEEMAKER_TYPE = ["103", "166", "200","261","265","276","277","278","280","305"] +POLARIS_COFFEEMAKER_ROG_TYPE = ["45", "190", "207", "222", "235", "247", "274", "279"] +POLARIS_CLIMATE_TYPE = ["69", "869", "859"] +POLARIS_AIRCLEANER_TYPE = ["140", "151", "152", "172", "203", "204", "236", "238", "239", "250", "251"] +POLARIS_AIRCLEANER_EAP_TYPE = ["826"] +POLARIS_VACUUM_TYPE = ["7","12","19","21","22","23","24","43","66","68","76","81","88","100","101","102","104","107","108","109","110","112","113","115","119","122","123","124","125","126","127","128","129","130","131","133","134","135","142","146","148","149","150","154","156","160","163","178","181","186","187","193","195","197","198","199","201","202","211","212","213","217","218","219","220","221","241","242","246"] +POLARIS_BOILER_TYPE = ["802","833","844","876"] +POLARIS_IRRIGATOR_TYPE = ["132", "252"] +POLARIS_HEATER_TYPE = ["806","846","847"] +POLARIS_AIRCONDITIONER_TYPE = ["813","820","882"] +POLARIS_THERMOSTAT_TYPE = ["878"] + +KETTLE_WITH_TEA_TIME_MODES = {"off": "0", "performance": "1", "electric": "3", "heat_pump": "4", "eco": "5", "gas": "6"} +KETTLE_WITH_KEEP_WITH_WARM_MODES = {"off": "0", "performance": "1", "high_demand": "2", "electric": "3", "heat_pump": "4", "eco": "5", "gas": "6"} +HUMIDDIFIER_5A_AVAILABLE_MODES = {"auto": "1", "comfort": "2", "baby": "3", "sleep": "4", "boost": "5"} +HUMIDDIFIER_5B_AVAILABLE_MODES = {"auto": "1", "sleep": "4", "boost": "5", "home": "6", "eco": "7"} +HUMIDDIFIER_4_AVAILABLE_MODES = {"auto": "1", "boost": "5", "home": "6", "eco": "7"} +HUMIDDIFIER_3A_AVAILABLE_MODES = {"boost": "5", "home": "6", "eco": "7"} +HUMIDDIFIER_3B_AVAILABLE_MODES = {"auto": "1", "boost": "5", "eco": "7"} +HUMIDDIFIER_2_AVAILABLE_MODES = {"home": "1", "auto": "2"} +HUMIDDIFIER_1_AVAILABLE_MODES = {"boost": "5"} +HUMIDDIFIER_11_AVAILABLE_MODES = {"home": "1", "auto": "2", "sleep": "3", "baby": "4", "comfort": "5", "fitnes": "6", "yoga": "7", "meditation": "8", "prana_hand": "9", "prana_auto": "10", "aroma": "11"} + +KETTLE_ERROR = { +"00": "no_error", +"01": "low_water", +"02": "kettle_out_of_base", +"03": "temperature_sensor_failure", +"04": "temperature_sensor_failure", +"05": "child_lock", +"06": "recommended_to_change_water", +"07": "changed_water_for_long_time" +} +HUMIDDIFIER_ERROR = { +"00": "no_error", +"01": "low_water", +"02": "child_lock", +"03": "replace_filter", +"04": "maximum_schedules", +"05": "clean_tank" +} +COOKER_ERROR = { +"00": "no_error", +"01": "temperature_sensor_failure", +"02": "temperature_sensor_failure", +"06": "cup_not_present", +"07": "child_lock", +"08": "gesture_sensor_error" +} +COFFEEMAKER_ERROR = { +"00": "no_error", +"01": "side_door_open", +"02": "waste_container_not_installed", +"03": "drip_tray_not_installed", +"04": "the_brewing_unit_not_installed", +"05": "missing_water_tank", +"06": "waste_container_full", +"07": "not_enough_coffee_beans", +"08": "water_supply_blocked", +"09": "code_e001", +"10": "code_e002", +"11": "code_e003", +"12": "code_e004", +"13": "code_e005", +"14": "decalcification_required", +"15": "water_changed_for_long_time", +"16": "cleaning_milk_system", +"17": "cleaning_brewing_system", +"18": "decalcification_progress", +"19": "cleaning_hydraulic_system", +"20": "check_water_tank", +"98": "nothing_is_selected", +"99": "cappuccinator_false" +} + +AIRCLEANER_ERROR = { +"00": "no_error", +"01": "replace_filter", +"02": "child_lock" +} + +VACUUM_ERROR = { +"00": "no_error", +"01": "Cliff error", +"02": "Check front collision plate", +"04": "Check wheels", +"08": "Check edge brushes", +"10": "Change side brushes", +"11": "Change the main brush", +"12": "Change filter", +"13": "Empty the dust bin", +"16": "Check fan motor", +"22": "The robot is not on the floor", +"32": "Check roll brush", +"64": "Low batttery" +} + +@dataclass +class PolarisSensorEntityDescription(SensorEntityDescription): + + value_fn: Callable | None = None + valueMap: dict | None = None + mqttTopicCurrentValue: str | None = None + +SENSORS_ALL_DEVICES = [ + PolarisSensorEntityDescription( + key="sensor/temperature", + name="Temperature", + translation_key="temperature_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:thermometer", + ), + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), +] + +SENSORS_RUSCLIMATE_HUMIDIFIER = [ + PolarisSensorEntityDescription( + key="sensor/humidity", + name="Humidity", + translation_key="humidity_sensor", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:water-percent", + ), + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), +] + +SENSORS_HUMIDIFIER = [ + PolarisSensorEntityDescription( + key="sensor/humidity", + name="Humidity", + translation_key="humidity_sensor", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:water-percent", + ), + PolarisSensorEntityDescription( + key="expendables", + name="filter_retain", + translation_key="filter_retain", + device_class=None, + native_unit_of_measurement=UnitOfTime.HOURS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), + PolarisSensorEntityDescription( + key="expendables", + name="clean_retain", + translation_key="clean_retain", + device_class=None, + native_unit_of_measurement=UnitOfTime.HOURS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:cup-water", + ), +] + +SENSORS_WEIGHT = [ + PolarisSensorEntityDescription( + key="weight", + name="weight", + translation_key="weight_sensor", + device_class=SensorDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.GRAMS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:weight-gram", + ), +] + +SENSORS_COOKER = [ + PolarisSensorEntityDescription( + key="time", + name="time_to_end", + translation_key="time_to_end_sensor", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:timer", + ), +] + +SENSORS_COFFEEMAKER = [ + PolarisSensorEntityDescription( + key="mode", + name="mode", + translation_key="mode_sensor", + valueMap={ + "0": "off", + "1": "espresso", + "2": "ristretto", + "3": "long_espresso", + "4": "americano", + "5": "heating", + "8": "cappuccino", + "9": "latte", + "10": "flat_white", + "11": "cortado", + "12": "double_espresso", + "13": "double_cappuccino", + "14": "double_latte", + "15": "double_long_espresso", + "16": "double_macchiato", + "17": "milk_coffee", + "18": "macchiato", + "19": "latte_macchiato", + "20": "hot_water", + "21": "hot_milk foam", + "22": "hot_milk" + }, + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:coffee-maker", + ), + PolarisSensorEntityDescription( + key="program_data/5", + name="power_state", + translation_key="power_state", + valueMap={ + "01": "power_on", + "02": "power_off", + "03": "turns_on", + "04": "turns_off" + }, + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:power", + ), +] + +SENSORS_COFFEEMAKER_ROG = [ + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), +] + +SENSORS_CLIMATE = [ + PolarisSensorEntityDescription( + key="sensor/co2", + name="CO2", + translation_key="co2_sensor", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, +# entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:molecule-co2", + ), + PolarisSensorEntityDescription( + key="expendables", + name="filter_retain", + translation_key="filter_retain", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), +] + +SENSORS_CLIMATE_200 = [ + PolarisSensorEntityDescription( + key="sensor/pm2", + name="PM2.5", + translation_key="pm2_5_sensor", + device_class=None, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:molecule", + ), + PolarisSensorEntityDescription( + key="sensor/co2", + name="CO2", + translation_key="co2_sensor", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:molecule-co2", + ), + PolarisSensorEntityDescription( + key="expendables", + name="filter_retain", + translation_key="filter_retain", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), + PolarisSensorEntityDescription( + key="expendables", + name="pre_filter_retain", + translation_key="pre_filter_retain", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), + PolarisSensorEntityDescription( + key="time", + name="time_to_end", + translation_key="time_to_end_sensor_turbo", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:timer", + ), +] + +SENSORS_AIRCLEANER = [ + PolarisSensorEntityDescription( + key="sensor/pm2", + name="PM2.5", + translation_key="pm2_5_sensor", + device_class=None, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:molecule", + ), + PolarisSensorEntityDescription( + key="expendables", + name="filter_retain", + translation_key="filter_retain", + device_class=None, + native_unit_of_measurement=UnitOfTime.HOURS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), + PolarisSensorEntityDescription( + key="time", + name="time_to_end", + translation_key="time_to_end_sensor", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:timer", + ), +] + +SENSORS_AIRCLEANER_EAP = [ + PolarisSensorEntityDescription( + key="sensor/pm2", + name="PM2.5", + translation_key="pm2_5_sensor", + device_class=None, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:molecule", + ), + PolarisSensorEntityDescription( + key="expendables", + name="filter_retain", + translation_key="filter_retain", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:filter", + ), + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), +] + +SENSORS_VACUUM = [ + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="error/code", + name="error", + translation_key="error", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:alert", + ), + PolarisSensorEntityDescription( + key="go_area", + name="go_area", + translation_key="go_area", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:map-marker-radius", + ), + PolarisSensorEntityDescription( + key="location_current", + name="current_id_room", + translation_key="current_id_room", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:map-marker-radius", + ), +] + +SENSORS_WATER_BOILER = [ + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="expendables", + name="anode_retain", + translation_key="anode_retain", + device_class=None, + native_unit_of_measurement=UnitOfTime.DAYS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:sign-pole", + ), + PolarisSensorEntityDescription( + key="sensor/temperature", + name="Temperature", + translation_key="temperature_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:thermometer", + ), +] + +SENSORS_IRRIGATOR = [ + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="time", + name="time_to_end", + translation_key="time_to_end_sensor", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:timer", + ), + PolarisSensorEntityDescription( + key="program_data/0", + name="quality", + translation_key="quality", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:equalizer", + ), +] + +SENSORS_HEATER = [ + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), + PolarisSensorEntityDescription( + key="program_data/0", + name="сurrent_power", + translation_key="сurrent_power", + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:equalizer", + ), + PolarisSensorEntityDescription( + key="sensor/temperature", + name="Temperature", + translation_key="temperature_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:thermometer", + ), +] + +SENSORS_AIRCONDITIONER = [ + PolarisSensorEntityDescription( + key="sensor/temperature", + name="Temperature", + translation_key="temperature_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + icon="mdi:thermometer", + ), + PolarisSensorEntityDescription( + key="firmware", + name="Firmware Version", + translation_key="firmware_sensor", + device_class=None, + native_unit_of_measurement=None, + state_class=None, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="devtype", + name="Device Type", + translation_key="type_sensor", + device_class=None, + native_unit_of_measurement=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:information-outline", + ), + PolarisSensorEntityDescription( + key="diag/rssi", + name="RSSI", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", + ), +] + +SENSORS_THERMOSTAT = [ + PolarisSensorEntityDescription( + key="power_consume", + name="Power consume", + translation_key="power_consume", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=True, + icon="mdi:meter-electric-outline", + ), +] + +@dataclass +class PolarisSwitchEntityDescription(SwitchEntityDescription): + + mqttTopicCommand: str | None = None + mqttTopicCurrentValue: str | None = None + payload_on: str | None = None + payload_off: str | None = None + +SWITCHES_ALL_DEVICES = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", +# entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="sound", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Sound", + mqttTopicCommand="control/sound", + mqttTopicCurrentValue="state/sound", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:volume-high", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:lock", + ), +] + +SWITCH_KETTLE_BACKLIGHT = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:alarm-light", + ), +] + +SWITCHES_RUSCLIMATE_HUMIDIFIER = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight bottom", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="night", + translation_key="night_switch", + entity_category=EntityCategory.CONFIG, + name="Night light", + mqttTopicCommand="control/night", + mqttTopicCurrentValue="state/night", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:weather-night", + ), +] + +SWITCH_HUMIDIFIER_BACKLIGHT = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:alarm-light", + ), +] + +SWITCH_HUMIDIFIER_IONISER = [ + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ioniser_switch", + entity_category=EntityCategory.CONFIG, + name="Ioniser", + mqttTopicCommand="control/ioniser", + mqttTopicCurrentValue="state/ioniser", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), +] + +SWITCH_HUMIDIFIER_WARM_STREAM = [ + PolarisSwitchEntityDescription( + key="warm_stream", + translation_key="warm_stream_switch", + entity_category=EntityCategory.CONFIG, + name="Warm stream", + mqttTopicCommand="control/warm_stream", + mqttTopicCurrentValue="state/warm_stream", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:heat-wave", + ), +] + +SWITCH_HUMIDIFIER_ULTRAVIOLET = [ + PolarisSwitchEntityDescription( + key="ultraviolet", + translation_key="ultraviolet_switch", + entity_category=EntityCategory.CONFIG, + name="Ultraviolet", + mqttTopicCommand="control/uv", + mqttTopicCurrentValue="state/uv", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:white-balance-sunny", + ), +] + +SWITCHES_COOKER = [ + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:lock", + ), + PolarisSwitchEntityDescription( + key="keepwarm", + translation_key="keepwarm_switch", + entity_category=EntityCategory.CONFIG, + name="Keepwarm", + mqttTopicCommand="control/keepwarm", + mqttTopicCurrentValue="state/keepwarm", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:heat-wave", + ), +] + +SWITCHES_COFFEEMAKER = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/program_data/5", + mqttTopicCurrentValue="state/program_data/5", + device_class=SwitchDeviceClass.SWITCH, + payload_on="03", + payload_off="04", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="sound", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Sound", + mqttTopicCommand="control/sound", + mqttTopicCurrentValue="state/sound", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), +] + +SWITCHES_COFFEEMAKER_ROG = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="5", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="sound", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Sound", + mqttTopicCommand="control/sound", + mqttTopicCurrentValue="state/sound", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), +] + +SWITCHES_CLIMATE = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="2", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="volume", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Volume", + mqttTopicCommand="control/volume", + mqttTopicCurrentValue="state/volume", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), +] + +SWITCHES_CLIMATE_200 = [ + PolarisSwitchEntityDescription( + key="damper", + translation_key="damper_switch", + entity_category=EntityCategory.CONFIG, + name="Damper", + mqttTopicCommand="control/damper", + mqttTopicCurrentValue="state/damper", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:swap-horizontal-circle-outline", + ), + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ioniser_switch", + entity_category=EntityCategory.CONFIG, + name="Ioniser", + mqttTopicCommand="control/ionizer", + mqttTopicCurrentValue="state/ionizer", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="ultraviolet", + translation_key="ultraviolet_switch", + entity_category=EntityCategory.CONFIG, + name="Ultraviolet", + mqttTopicCommand="control/uv", + mqttTopicCurrentValue="state/uv", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:white-balance-sunny", + ), +] + +SWITCHES_AIRCLEANER = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:alarm-light", + ), + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ioniser_switch", + entity_category=EntityCategory.CONFIG, + name="Ioniser", + mqttTopicCommand="control/ioniser", + mqttTopicCurrentValue="state/ioniser", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="ultraviolet", + translation_key="ultraviolet_switch", + entity_category=EntityCategory.CONFIG, + name="Ultraviolet", + mqttTopicCommand="control/warm_stream", + mqttTopicCurrentValue="state/warm_stream", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:white-balance-sunny", + ), +] + +SWITCHES_AIRCLEANER_EAP = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ioniser_switch", + entity_category=EntityCategory.CONFIG, + name="Ioniser", + mqttTopicCommand="control/ioniser", + mqttTopicCurrentValue="state/ioniser", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="ultraviolet", + translation_key="ultraviolet_switch", + entity_category=EntityCategory.CONFIG, + name="Ultraviolet", + mqttTopicCommand="control/uv", + mqttTopicCurrentValue="state/uv", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:white-balance-sunny", + ), + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), +] + +SWITCHES_VACUUM = [ + PolarisSwitchEntityDescription( + key="turbo", + translation_key="turbo_switch", + entity_category=EntityCategory.CONFIG, + name="mode clean carpet", + mqttTopicCommand="control/turbo", + mqttTopicCurrentValue="state/turbo", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", +# icon="mdi:alarm-light", + ), + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="emptying_dust_switch", + entity_category=EntityCategory.CONFIG, + name="sbros_musora", + mqttTopicCommand="control/ioniser", + mqttTopicCurrentValue="state/ioniser", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="volume", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Volume", + mqttTopicCommand="control/volume", + mqttTopicCurrentValue="state/volume", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), +] + +SWITCHES_WATER_BOILER = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", +# entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="smart_mode", + translation_key="smart_mode", + entity_category=EntityCategory.CONFIG, + name="smart_mode", + mqttTopicCommand="control/smart_mode", + mqttTopicCurrentValue="state/smart_mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:water-boiler-auto", + ), + PolarisSwitchEntityDescription( + key="bss_mode", + translation_key="bss_mode", + entity_category=EntityCategory.CONFIG, + name="bss_mode", + mqttTopicCommand="control/bss", + mqttTopicCurrentValue="state/bss", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:bacteria", + ), +] + +SWITCHES_WATER_BOILER_BACKLIGHT = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_bright", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), +] + +SWITCHES_WATER_BOILER_NO_FROST = [ + PolarisSwitchEntityDescription( + key="no_frost", + translation_key="no_frost", + entity_category=EntityCategory.CONFIG, + name="No frost", + mqttTopicCommand="control/keep_warm", + mqttTopicCurrentValue="state/keep_warm", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:snowflake-off", + ), +] + +SWITCHES_IRRIGATOR = [ + PolarisSwitchEntityDescription( + key="smart_mode", + translation_key="smart_mode_switch", + entity_category=EntityCategory.CONFIG, + name="Massage", + mqttTopicCommand="control/smart_mode", + mqttTopicCurrentValue="state/smart_mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:hand-wave", + ), + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ozonation_switch", + entity_category=EntityCategory.CONFIG, + name="O3", + mqttTopicCommand="control/ioniser", + mqttTopicCurrentValue="state/ioniser", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="power_switch", + translation_key="power_switch", + entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power", + ), +] + +SWITCHES_HEATER = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", +# entity_category=EntityCategory.CONFIG, + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="volume", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Volume", + mqttTopicCommand="control/volume", + mqttTopicCurrentValue="state/volume", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_bright", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="damper_heater", + translation_key="damper_heater", + entity_category=EntityCategory.CONFIG, + name="Detect open window", + mqttTopicCommand="control/damper", + mqttTopicCurrentValue="state/damper", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="display_off_heater", + translation_key="display_off_heater", + entity_category=EntityCategory.CONFIG, + name="Auto-turn off display", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), + PolarisSwitchEntityDescription( + key="half_power_heater", + translation_key="half_power_heater", + entity_category=EntityCategory.CONFIG, + name="Half the power", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), +] + +SWITCHES_AIRCONDITIONER = [ + PolarisSwitchEntityDescription( + key="turbo", + translation_key="turbo_switch", + entity_category=EntityCategory.CONFIG, + name="Turbo mode", + mqttTopicCommand="control/turbo", + mqttTopicCurrentValue="state/turbo", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:rocket-launch", + ), + PolarisSwitchEntityDescription( + key="night", + translation_key="night_switch", + entity_category=EntityCategory.CONFIG, + name="Night mode", + mqttTopicCommand="control/night", + mqttTopicCurrentValue="state/night", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:weather-night", + ), + PolarisSwitchEntityDescription( + key="self-cleaning", + translation_key="self_cleaning", + entity_category=EntityCategory.CONFIG, + name="Self cleaning", + mqttTopicCommand="control/program_data/1", + mqttTopicCurrentValue="state/program_data/1", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), + PolarisSwitchEntityDescription( + key="delicate-blowing", + translation_key="delicate_blowing", + entity_category=EntityCategory.CONFIG, + name="Delicate blowing", + mqttTopicCommand="control/program_data/2", + mqttTopicCurrentValue="state/program_data/2", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + icon="mdi:air-filter", + ), + PolarisSwitchEntityDescription( + key="eco_mode_switch", + translation_key="eco_mode_switch", + entity_category=EntityCategory.CONFIG, + name="Eco mode", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), + PolarisSwitchEntityDescription( + key="auto_heater_switch", + translation_key="auto_heater_switch", + entity_category=EntityCategory.CONFIG, + name="Auto heater", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), +] +SWITCHES_AIRCONDITIONER_820 = [ + PolarisSwitchEntityDescription( + key="ioniser", + translation_key="ioniser_switch", + entity_category=EntityCategory.CONFIG, + name="Ioniser", + mqttTopicCommand="control/ionizer", + mqttTopicCurrentValue="state/ionizer", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:atom-variant", + ), + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="turbo", + translation_key="turbo_switch", + entity_category=EntityCategory.CONFIG, + name="Turbo mode", + mqttTopicCommand="control/turbo", + mqttTopicCurrentValue="state/turbo", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:rocket-launch", + ), + PolarisSwitchEntityDescription( + key="night", + translation_key="night_switch", + entity_category=EntityCategory.CONFIG, + name="Night mode", + mqttTopicCommand="control/night", + mqttTopicCurrentValue="state/night", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:weather-night", + ), + PolarisSwitchEntityDescription( + key="self_cleaning", + translation_key="self_cleaning", + entity_category=EntityCategory.CONFIG, + name="Self cleaning", + mqttTopicCommand="control/program_data/1", + mqttTopicCurrentValue="state/program_data/1", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), + PolarisSwitchEntityDescription( + key="quiet_mode", + translation_key="quiet_mode", + entity_category=EntityCategory.CONFIG, + name="Quiet mode", + mqttTopicCommand="control/program_data/1", + mqttTopicCurrentValue="state/program_data/1", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + icon="mdi:fan-minus", + ), + PolarisSwitchEntityDescription( + key="eco_mode_switch", + translation_key="eco_mode_switch", + entity_category=EntityCategory.CONFIG, + name="Eco mode", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + ), + PolarisSwitchEntityDescription( + key="anti_fingus", + translation_key="anti_fingus_switch", + entity_category=EntityCategory.CONFIG, + name="Anti-fingus", + mqttTopicCommand="control/program_data/0", + mqttTopicCurrentValue="state/program_data/0", + device_class=SwitchDeviceClass.SWITCH, + payload_on="01", + payload_off="00", + icon="mdi:mushroom-off-outline", + ), +] + +SWITCHES_AIRCONDITIONER_882 = [ + PolarisSwitchEntityDescription( + key="backlight", + translation_key="backlight_switch", + entity_category=EntityCategory.CONFIG, + name="Backlight", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="turbo", + translation_key="turbo_switch", + entity_category=EntityCategory.CONFIG, + name="Turbo mode", + mqttTopicCommand="control/turbo", + mqttTopicCurrentValue="state/turbo", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:rocket-launch", + ), + PolarisSwitchEntityDescription( + key="night", + translation_key="night_switch", + entity_category=EntityCategory.CONFIG, + name="Night mode", + mqttTopicCommand="control/night", + mqttTopicCurrentValue="state/night", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:weather-night", + ), + PolarisSwitchEntityDescription( + key="smart_mode", + translation_key="smart_mode", + entity_category=EntityCategory.CONFIG, + name="smart_mode", + mqttTopicCommand="control/smart_mode", + mqttTopicCurrentValue="state/smart_mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + icon="mdi:thermometer-auto", + ), + PolarisSwitchEntityDescription( + key="volume", + translation_key="sound_switch", + entity_category=EntityCategory.CONFIG, + name="Volume", + mqttTopicCommand="control/volume", + mqttTopicCurrentValue="state/volume", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), +] + +SWITCHES_THERMOSTAT = [ + PolarisSwitchEntityDescription( + key="power", + translation_key="power_switch", + name="Power", + mqttTopicCommand="control/mode", + mqttTopicCurrentValue="state/mode", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + icon="mdi:power-standby", + ), + PolarisSwitchEntityDescription( + key="display_off_heater", + translation_key="display_off_heater", + entity_category=EntityCategory.CONFIG, + name="Backlight auto off", + mqttTopicCommand="control/backlight", + mqttTopicCurrentValue="state/backlight", + device_class=SwitchDeviceClass.SWITCH, + payload_on="1", + payload_off="0", + ), + PolarisSwitchEntityDescription( + key="child_lock", + translation_key="child_lock_switch", + entity_category=EntityCategory.CONFIG, + name="Child lock", + mqttTopicCommand="control/child_lock", + mqttTopicCurrentValue="state/child_lock", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), + PolarisSwitchEntityDescription( + key="damper_heater", + translation_key="damper_heater", + entity_category=EntityCategory.CONFIG, + name="Detect open window", + mqttTopicCommand="control/damper", + mqttTopicCurrentValue="state/damper", + device_class=SwitchDeviceClass.SWITCH, + payload_on="true", + payload_off="false", + ), +] + +@dataclass +class PolarisWaterHeaterEntityDescription(WaterHeaterEntityDescription): # breaks_in_ha_version="2026.1" + + mqttTopicCurrentMode: str | None = None + mqttTopicCommandMode: str | None = None + mqttTopicCurrentTemperature: str | None = None + mqttTopicCommandTemperature: str | None = None + mqttTopicTargetTemperature: str | None = None + payload_on: str | None = None + payload_off: str | None = None + min_temp: float | None = None + max_temp: float | None = None + operation_list: str | None = None + mode: str | None = None + +WATER_HEATERS = [ + PolarisWaterHeaterEntityDescription( + key="water_heater", + translation_key="water_heater", +# entity_category=EntityCategory.CONFIG, + name="water_heater", + mqttTopicCommandMode="control/mode", + mqttTopicCommandTemperature="control/temperature", + mqttTopicCurrentMode="state/mode", + mqttTopicCurrentTemperature="state/sensor/temperature", + mqttTopicTargetTemperature="state/temperature", + payload_on="1", + payload_off="0", +# icon="mdi:kettle", + min_temp=30, + max_temp=100, + mode="off", + operation_list = {"off": "0", "performance": "1", "electric": "3", "heat_pump": "4", "eco": "5"} + ) +] + +WATER_BOILERS = [ + PolarisWaterHeaterEntityDescription( + key="water_boiler", + translation_key="water_boiler", + name="water_boiler", + mqttTopicCommandMode="control/mode", + mqttTopicCommandTemperature="control/temperature", + mqttTopicCurrentMode="state/mode", + mqttTopicCurrentTemperature="state/sensor/temperature", + mqttTopicTargetTemperature="state/temperature", + payload_on="1", + payload_off="0", +# icon="mdi:water-boiler", + min_temp=30, + max_temp=75, + mode="off", + operation_list = {"off": "0", "performance": "1", "electric": "2", "heat_pump": "3", "eco": "6"} + ) +] + +@dataclass +class PolarisHumidifierEntityDescription(HumidifierEntityDescription): + + mqttTopicCurrentState: str | None = None + mqttTopicCommandState: str | None = None + mqttTopicCurrentMode: str | None = None + mqttTopicCommandMode: str | None = None + mqttTopicCurrentHumidity: str | None = None + mqttTopicCurrentTargetHumidity: str | None = None + mqttTopicCommandTargetHumidity: str | None = None + payload_on: str | None = None + payload_off: str | None = None + min_humidity: int | None = None + max_humidity: int | None = None + mode: str | None = None + available_modes: str | None = None + +HUMIDIFIERS = [ + PolarisHumidifierEntityDescription( + name="Humidifier", + key="humidifier", + translation_key="humidifier", + mode="boost", + available_modes={"auto": "1", "comfort": "2", "baby": "3", "sleep": "4", "boost": "5", "home": "6", "eco": "7"}, + mqttTopicCurrentState = "state/mode", + mqttTopicCommandState = "control/mode", + mqttTopicCurrentMode = "state/mode", + mqttTopicCommandMode = "control/mode", + mqttTopicCurrentHumidity = "state/sensor/humidity", + mqttTopicCurrentTargetHumidity = "state/humidity", + mqttTopicCommandTargetHumidity = "control/humidity", + payload_on = "1", + payload_off = "0", + min_humidity = 30, + max_humidity = 80, + device_class=HumidifierDeviceClass.HUMIDIFIER, + icon="mdi:air-humidifier", + ) +] + +@dataclass +class PolarisNumberEntityDescription(NumberEntityDescription): + + mqttTopicCurrent: str | None = None + mqttTopicCommand: str | None = None + native_value: int | None = None + +NUMBER_HUMIDIFIER = [ + PolarisNumberEntityDescription( + key="intensity", + name="intensity", + translation_key="intensity", + mqttTopicCurrent = "state/intensity", + mqttTopicCommand = "control/intensity", + entity_category=EntityCategory.CONFIG, + device_class=None, + native_unit_of_measurement=None, + entity_registry_enabled_default=True, + native_max_value=7, + native_min_value=0, + native_step=1, + native_value=1, + ) +] + +NUMBER_RUSCLIMATE_HUMIDIFIER = [ + PolarisNumberEntityDescription( + key="speed", + name="Evaporation rate", + translation_key="evaporation_rate", + mqttTopicCurrent = "state/speed", + mqttTopicCommand = "control/speed", + entity_category=EntityCategory.CONFIG, + device_class=None, + native_unit_of_measurement=None, + entity_registry_enabled_default=True, + native_max_value=3, + native_min_value=1, + native_step=1, + native_value=1, + ) +] + +NUMBER_COOKER = [ + PolarisNumberEntityDescription( + key="set_temperature", + name="set_temperature", + translation_key="set_temperature", + mqttTopicCurrent = "control/set_temperature", + mqttTopicCommand = "control/set_temperature", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=True, + native_max_value=160, + native_min_value=20, + native_step=1, + native_value=115, + mode="box", + ) +] + +NUMBERS_COFFEEMAKER = [ + PolarisNumberEntityDescription( + key="amount", + name="amount", + translation_key="amount", + mqttTopicCurrent = "state/amount", + mqttTopicCommand = "control/amount", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.MILLILITERS, + entity_registry_enabled_default=True, + native_max_value=250, + native_min_value=20, + native_step=5, + native_value=40, + mode="slider", + ), + PolarisNumberEntityDescription( + key="weight", + name="weight", + translation_key="weight", + mqttTopicCurrent = "state/weight", + mqttTopicCommand = "control/weight", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.GRAMS, + entity_registry_enabled_default=True, + native_max_value=12, + native_min_value=7, + native_step=1, + native_value=9, + mode="slider", + ), + PolarisNumberEntityDescription( + key="tank", + name="tank", + translation_key="tank", + mqttTopicCurrent = "state/tank", + mqttTopicCommand = "control/tank", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.MILLILITERS, + entity_registry_enabled_default=True, + native_max_value=250, + native_min_value=50, + native_step=5, + native_value=100, + mode="slider", + ), + PolarisNumberEntityDescription( + key="pressure", + name="pressure", + translation_key="pressure", + mqttTopicCurrent = "state/pressure", + mqttTopicCommand = "control/pressure", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_registry_enabled_default=True, + native_max_value=100, + native_min_value=0, + native_step=5, + native_value=40, + mode="slider", + ), + PolarisNumberEntityDescription( + key="speed", + name="speed", + translation_key="speed", + mqttTopicCurrent = "state/speed", + mqttTopicCommand = "control/speed", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_registry_enabled_default=True, + native_max_value=100, + native_min_value=0, + native_step=5, + native_value=30, + mode="slider", + ), + PolarisNumberEntityDescription( + key="temperature", + name="temperature", + translation_key="temperature", + mqttTopicCurrent = "state/temperature", + mqttTopicCommand = "control/temperature", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=True, + native_max_value=95, + native_min_value=0, + native_step=1, + native_value=92, + mode="slider", + ), +] + +NUMBERS_COFFEEMAKER_ROG = [ + PolarisNumberEntityDescription( + key="display_time", + name="display_time", + translation_key="display_time", + mqttTopicCurrent = "state/program_data/0", + mqttTopicCommand = "control/program_data/0", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_registry_enabled_default=True, + native_max_value=30, + native_min_value=10, + native_step=5, + native_value=10, + mode="slider", + ), + PolarisNumberEntityDescription( + key="amount", + name="amount", + translation_key="amount", + mqttTopicCurrent = "state/amount", + mqttTopicCommand = "control/amount", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.GRAMS, + entity_registry_enabled_default=True, + native_max_value=200, + native_min_value=30, + native_step=5, + native_value=40, + mode="slider", + ), + PolarisNumberEntityDescription( + key="tank", + name="tank", + translation_key="speed", + mqttTopicCurrent = "state/tank", + mqttTopicCommand = "control/tank", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_registry_enabled_default=True, + native_max_value=40, + native_min_value=1, + native_step=1, + native_value=15, + mode="slider", + ), + PolarisNumberEntityDescription( + key="temperature", + name="temperature", + translation_key="temperature", + mqttTopicCurrent = "state/temperature", + mqttTopicCommand = "control/temperature", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=True, + native_max_value=105, + native_min_value=95, + native_step=5, + native_value=95, + mode="slider", + ), +] + +NUMBERS_AIRCLEANER = [ + PolarisNumberEntityDescription( + key="time", + name="time_timer", + translation_key="time_timer", + mqttTopicCurrent = "state/time", + mqttTopicCommand = "control/time", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.HOURS, + entity_registry_enabled_default=True, + native_max_value=12, + native_min_value=0, + native_step=1, + native_value=1, + mode="slider", + ), +] + +NUMBERS_IRRIGATOR = [ + PolarisNumberEntityDescription( + key="speed_irrigator", + name="speed_irrigator", + translation_key="speed_irrigator", + mqttTopicCurrent = "state/speed", + mqttTopicCommand = "control/speed", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + native_max_value=10, + native_min_value=1, + native_step=1, + native_value=1, + mode="slider", + icon="mdi:speedometer", + ), +] + +NUMBERS_HEATER = [ + PolarisNumberEntityDescription( + key="temperature_difference_eco", + name="ECO difference temperature", + translation_key="temperature_difference_eco", + mqttTopicCurrent = "state/program_data/2", + mqttTopicCommand = "control/program_data/2", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=True, + native_max_value=7, + native_min_value=3, + native_step=1, + native_value=3, + mode="slider", + ), + PolarisNumberEntityDescription( + key="temperature_difference_antifrost", + name="Anti frost difference temperature", + translation_key="temperature_difference_antifrost", + mqttTopicCurrent = "state/program_data/3", + mqttTopicCommand = "control/program_data/3", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=True, + native_max_value=7, + native_min_value=3, + native_step=1, + native_value=3, + mode="slider", + ), +] + +NUMBERS_THERMOSTAT = [ + PolarisNumberEntityDescription( + key="bright_backlight", + name="Bright backlight", + translation_key="bright_backlight", + mqttTopicCurrent = "state/program_data/0", + mqttTopicCommand = "control/program_data/0", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.POWER_FACTOR, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=True, + native_max_value=100, + native_min_value=20, + native_step=10, + native_value=100, + mode="slider", + icon="mdi:brightness-percent", + ), + PolarisNumberEntityDescription( + key="power_cable", + name="Power cable", + translation_key="power_cable", + mqttTopicCurrent = "state/program_data/9", + mqttTopicCommand = "control/program_data/9", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=True, + native_max_value=3600, + native_min_value=50, + native_step=25, + native_value=50, + mode="box", + ), +] + +@dataclass +class PolarisSelectEntityDescription(SelectEntityDescription): + + options: dict[str, str] | None = None + mqttTopicCurrentMode: str | None = None + mqttTopicCommandMode: str | None = None + mqttTopicCommandTemperature: str | None = None + mqttTopicTargetTemperature: str | None = None + +SELECT_KETTLE = [ + PolarisSelectEntityDescription( + key="select_mode_kettle", + name="select_mode_kettle", + translation_key="select_mode_kettle", + mqttTopicCurrentMode="state/mode", + mqttTopicCommandMode="control/mode", + mqttTopicCommandTemperature="control/temperature", + mqttTopicTargetTemperature="state/temperature", + options={ + "not_selected": 0, + "black_tea": 100, + "baby_bottle": 40, + "instant_coffee": 95, + "green_tea": 80, + "flower_tea": 80, + "tea_bag": 100, + "red_tea": 90, + "puerh_tea": 95, + "oolong_tea": 90, + "white_tea": 65, + "herbal_tea": 90, + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:receipt-text", + entity_registry_enabled_default=True, + ) +] + +SELECT_COOKER = [ + PolarisSelectEntityDescription( + key="select_mode_cooker", + name="select_mode_cooker", + translation_key="select_mode_cooker", + mqttTopicCurrentMode="state/steps", + mqttTopicCommandMode="control/steps", + options={ + "my_recipe_plus": "[{\"mode\":1, \"time\":1200, \"temperature\":115}]", + "reheat": "[{\"mode\":2, \"time\":1200, \"temperature\":115}]", + "cake": "[{\"mode\":3, \"time\":3600, \"temperature\":130}]", + "soaked_rice": "[{\"mode\":4, \"time\":2400, \"temperature\":115}]", + "stew": "[{\"mode\":6, \"time\":7200, \"temperature\":93}]", + "fry": "[{\"mode\":7, \"time\":300, \"temperature\":160}]", + "pilaf": "[{\"mode\":9, \"time\":3600, \"temperature\":120}]", + "yogurt": "[{\"mode\":12, \"time\":28800, \"temperature\":38}]", + "oatmeal": "[{\"mode\":13, \"time\":300, \"temperature\":96}]", + "milk_porridge": "[{\"mode\":17, \"time\":3600, \"temperature\":95}]", + "soup": "[{\"mode\":18, \"time\":3600, \"temperature\":97}]", + "meat": "[{\"mode\":24, \"time\":21600, \"temperature\":93}]", + "cottage_cheese": "[{\"mode\":27, \"time\":2400, \"temperature\":80}]", + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:receipt-text", + entity_registry_enabled_default=True, + ) +] + +SELECT_COFFEEMAKER = [ + PolarisSelectEntityDescription( + key="select_mode_cofeemaker", + name="select_mode_cofeemaker", + translation_key="select_mode_cofeemaker", + mqttTopicCurrentMode="control/mode", # ?????? + mqttTopicCommandMode="control/mode", + options={ + "not_selected": "[{\"mode\": 0, \"amount\": 0, \"weight\": 0, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 0}]", + "espresso": "[{\"mode\": 1, \"amount\": 40, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "ristretto": "[{\"mode\": 2, \"amount\": 30, \"weight\": 11, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "long_espresso": "[{\"mode\": 3, \"amount\": 100, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "americano": "[{\"mode\": 4, \"amount\": 80, \"weight\": 9, \"tank\": 100, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "cappuccino": "[{\"mode\": 8, \"amount\": 50, \"weight\": 9, \"tank\": 0, \"pressure\": 35, \"speed\": 0, \"temperature\": 92}]", + "latte": "[{\"mode\": 9, \"amount\": 40, \"weight\": 9, \"tank\": 0, \"pressure\": 15, \"speed\": 30, \"temperature\": 92}]", + "flat_white": "[{\"mode\": 10, \"amount\": 80, \"weight\": 9, \"tank\": 0, \"pressure\": 5, \"speed\": 30, \"temperature\": 92}]", + "cortado": "[{\"mode\": 11, \"amount\": 50, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 10, \"temperature\": 92}]", + "double_espresso": "[{\"mode\": 12, \"amount\": 40, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "double_cappuccino": "[{\"mode\": 13, \"amount\": 80, \"weight\": 12, \"tank\": 0, \"pressure\": 50, \"speed\": 0, \"temperature\": 92}]", + "double_latte": "[{\"mode\": 14, \"amount\": 60, \"weight\": 10, \"tank\": 0, \"pressure\": 20, \"speed\": 45, \"temperature\": 92}]", + "double_long_espresso": "[{\"mode\": 15, \"amount\": 100, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 0, \"temperature\": 92}]", + "double_macchiato": "[{\"mode\": 16, \"amount\": 60, \"weight\": 10, \"tank\": 0, \"pressure\": 60, \"speed\": 0, \"temperature\": 92}]", + "milk_coffee": "[{\"mode\": 17, \"amount\": 50, \"weight\": 9, \"tank\": 0, \"pressure\": 0, \"speed\": 30, \"temperature\": 92}]", + "macchiato": "[{\"mode\": 18, \"amount\": 40, \"weight\": 9, \"tank\": 0, \"pressure\": 40, \"speed\": 0, \"temperature\": 92}]", + "latte_macchiato": "[{\"mode\": 19, \"amount\": 40, \"weight\": 9, \"tank\": 0, \"pressure\": 20, \"speed\": 25, \"temperature\": 92}]", + "hot_water": "[{\"mode\": 20, \"amount\": 0, \"weight\": 0, \"tank\": 100, \"pressure\": 0, \"speed\": 0, \"temperature\": 0}]", + "hot_milk_foam": "[{\"mode\": 21, \"amount\": 0, \"weight\": 0, \"tank\": 0, \"pressure\": 35, \"speed\": 0, \"temperature\": 0}]", + "hot_milk": "[{\"mode\": 22, \"amount\": 0, \"weight\": 0, \"tank\": 0, \"pressure\": 0, \"speed\": 45, \"temperature\": 0}]" + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:receipt-text", + entity_registry_enabled_default=True, + ) +] + +SELECT_COFFEEMAKER_ROG = [ + PolarisSelectEntityDescription( + key="select_mode_cofeemaker_rog", + name="select_mode_cofeemaker", + translation_key="select_mode_cofeemaker", + mqttTopicCurrentMode="state/mode", + mqttTopicCommandMode="control/mode", + options={ + "not_selected": "[{\"mode\": 0, \"amount\": 30, \"tank\": 0, \"temperature\": 95}]", + "espresso": "[{\"mode\": 1, \"amount\": 65, \"tank\": 0, \"temperature\": 95}]", + "doppio": "[{\"mode\": 1, \"amount\": 115, \"tank\": 0, \"temperature\": 95}]", + "cappuccino": "[{\"mode\": 2, \"amount\": 50, \"tank\": 15, \"temperature\": 95}]", + "double_cappuccino": "[{\"mode\": 2, \"amount\": 100, \"tank\": 25, \"temperature\": 95}]", + "latte": "[{\"mode\": 3, \"amount\": 65, \"tank\": 32, \"temperature\": 95}]", + "double_latte": "[{\"mode\": 3, \"amount\": 115, \"tank\": 40, \"temperature\": 95}]", + "lungo": "[{\"mode\": 1, \"amount\": 120, \"tank\": 0, \"temperature\": 95}]", + "flat_white": "[{\"mode\": 2, \"amount\": 70, \"tank\": 20, \"temperature\": 95}]", + "clearing": "[{\"mode\": 4, \"amount\": 0, \"tank\": 0, \"temperature\": 95}]", + "heating": "[{\"mode\": 5, \"amount\": 0, \"tank\": 0, \"temperature\": 95}]", + "hot_milk": "[{\"mode\": 6, \"amount\": 0, \"tank\": 15, \"temperature\": 95}]", + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:receipt-text", + entity_registry_enabled_default=True, + ) +] + +SELECT_CLIMATE = [ + PolarisSelectEntityDescription( + key="select_melody", + name="Melody", + translation_key="select_melody", + mqttTopicCurrentMode="state/amount", + mqttTopicCommandMode="control/amount", + options={ + "mute": 0, + "rainstorm": 1, + "surf": 2, + "forest": 3, + "birdsong": 4, + "bonfire": 5, + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:music-note", + entity_registry_enabled_default=True, + ) +] + +SELECT_AIRCLEANER_EAP = [ + PolarisSelectEntityDescription( + key="select_night_backlight", + name="Night backlight", + translation_key="select_night_backlight", + mqttTopicCurrentMode="state/program_data/0", + mqttTopicCommandMode="control/program_data/0", + options={ + "off": 0, + "all_on": 1, + "high_on": 2, + "mid_on": 3, + "low_on": 4, + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + ) +] + +SELECT_AIRCONDITIONER_SWING_HORIZONTAL = [ + PolarisSelectEntityDescription( + key="select_swing_horizontal", + name="Swing horizontal", + translation_key="select_swing_horizontal", + mqttTopicCurrentMode="state/program_data/3", + mqttTopicCommandMode="control/program_data/3", + options={ + "off": "00", + "top": "01", + "high": "02", + "middle": "03", + "low": "04", + "bottom": "05", + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + icon="mdi:arrow-split-horizontal", + ) +] + +SELECT_AIRCONDITIONER_SWING_VERTICAL = [ + PolarisSelectEntityDescription( + key="select_swing_vertical", + name="Swing vertical", + translation_key="select_swing_vertical", + mqttTopicCurrentMode="state/program_data/4", + mqttTopicCommandMode="control/program_data/4", + options={ + "off": "00", + "left": "01", + "center-left": "02", + "center": "03", + "center-right": "04", + "right": "05", + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + icon="mdi:arrow-split-vertical", + ) +] + + +SELECT_VACUUM = [ + PolarisSelectEntityDescription( + key="select_mode_vacuum", + name="select_mode_vacuum", + translation_key="select_mode", + mqttTopicCurrentMode="state/mode", + mqttTopicCommandMode="control/mode", + options={ + "mode0": 0, + "mode1": 1, + "mode2": 2, + "mode3": 3, + "mode4": 4, + "mode5": 5, + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:vacuum", + entity_registry_enabled_default=True, + ), + PolarisSelectEntityDescription( + key="select_room", + name="select_room", + translation_key="select_room", + mqttTopicCurrentMode="control/room", + mqttTopicCommandMode="control/room", + options={ + "All_rooms": {"id": "00", "coordinate": []} + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:vacuum", + entity_registry_enabled_default=True, + ) +] + +SELECT_IRRIGATOR = [ + PolarisSelectEntityDescription( + key="select_irrigator", + name="Preset", + translation_key="select_irrigator", + mqttTopicCurrentMode="state/program_data/1", + mqttTopicCommandMode="control/", + options={ + "preset1": 1, + "preset2": 2, + "preset3": 3, + }, + entity_category=EntityCategory.CONFIG, + device_class=None, + icon="mdi:format-list-numbered", + entity_registry_enabled_default=True, + ) +] + + +@dataclass +class PolarisLightEntityDescription(SelectEntityDescription): + + mqttTopicCurrentColor: str | None = None + mqttTopicCommandColor: str | None = None + mqttTopicCurrentState: str | None = None + mqttTopicCommandState: str | None = None + +LIGHTS = [ + PolarisLightEntityDescription( + key="night", + name="night", + translation_key="night_light", + mqttTopicCurrentColor="state/program_data/0", + mqttTopicCommandColor="control/program_data/0", + mqttTopicCurrentState="state/night", + mqttTopicCommandState="control/night", + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + ) +] + +LIGHTS_DOUBLE = [ + PolarisLightEntityDescription( + key="night_up", + name="night_up", + translation_key="night_light_up", + mqttTopicCurrentColor="state/backlight", + mqttTopicCommandColor="control/backlight", + mqttTopicCurrentState="state/backlight", + mqttTopicCommandState="control/backlight", + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + ), + PolarisLightEntityDescription( + key="night_down", + name="night_down", + translation_key="night_light_down", + mqttTopicCurrentColor="state/backlight", + mqttTopicCommandColor="control/backlight", + mqttTopicCurrentState="state/backlight", + mqttTopicCommandState="control/backlight", + entity_category=EntityCategory.CONFIG, + device_class=None, + entity_registry_enabled_default=True, + ) +] + +@dataclass +class PolarisBinarySensorEntityDescription(BinarySensorEntityDescription): + + mqttTopicStatus: str | None = None + +BINARYSENSOR_KETTLE = [ + PolarisBinarySensorEntityDescription( + key="base", + name="base", + translation_key="base_binary_sensor", + mqttTopicStatus="state/error/empty_base", + device_class=None, #BinarySensorDeviceClass.PLUG, + entity_registry_enabled_default=True, + ) +] + +BINARYSENSOR_LID = [ + PolarisBinarySensorEntityDescription( + key="lid", + name="lid", + translation_key="lid_binary_sensor", + mqttTopicStatus="state/lid_open", + device_class=None, + entity_registry_enabled_default=True, + ) +] + +BINARYSENSOR_WATER_TANK = [ + PolarisBinarySensorEntityDescription( + key="water_tank", + name="water_tank", + translation_key="water_tank_binary_sensor", + mqttTopicStatus="state/error/water", + device_class=None, + entity_registry_enabled_default=True, + ) +] + +BINARYSENSOR_CAPPUCCINATOR = [ + PolarisBinarySensorEntityDescription( + key="cappuccinator", + name="cappuccinator", + translation_key="cappuccinator_binary_sensor", + mqttTopicStatus="state/tank", + device_class=None, + entity_registry_enabled_default=True, + ) +] + +BINARYSENSOR_THERMOSTAT = [ + PolarisBinarySensorEntityDescription( + key="heating", + name="Heating", + translation_key="heating_binary_sensor", + mqttTopicStatus="state/program_data/10", + device_class=BinarySensorDeviceClass.HEAT, + entity_registry_enabled_default=True, + icon="mdi:heat-wave", + ) +] + +BINARYSENSOR_AVAILABLE = [ + PolarisBinarySensorEntityDescription( + key="available", + name="available", + translation_key="available_binary_sensor", + mqttTopicStatus="state/error/connection", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_registry_enabled_default=True, + ) +] + +@dataclass +class PolarisButtonEntityDescription(ButtonEntityDescription): + + payloads: str | None = None + mqttTopicCommand: str | None = None + +BUTTON_HUMIDIFIER = [ + PolarisButtonEntityDescription( + key="button_reset_filter", + name="button_reset_filter", + translation_key="button_reset_filter", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[0,0]", + icon="mdi:filter", + ), + PolarisButtonEntityDescription( + key="button_reset_tank", + name="button_reset_tank", + translation_key="button_reset_tank", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[1,0]", + icon="mdi:cup-water", + ) +] + +BUTTON_COOKER = [ + PolarisButtonEntityDescription( + key="button_stop", + name="button_stop", + translation_key="button_stop", + mqttTopicCommand="control/steps", + device_class=None, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + payloads="[]", + ), + PolarisButtonEntityDescription( + key="button_start", + name="button_start", + translation_key="button_start", + mqttTopicCommand="control/steps", + device_class=None, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + payloads="[]", + ) +] + +BUTTON_COFFEEMAKER = [ + PolarisButtonEntityDescription( + key="button_stop", + name="button_stop", + translation_key="button_stop_coffee", + mqttTopicCommand="control/", + device_class=None, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + payloads="[]", + ), + PolarisButtonEntityDescription( + key="button_start", + name="button_start", + translation_key="button_start_coffee", + mqttTopicCommand="control/", + device_class=None, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + payloads="[]", + ) +] + +BUTTON_CLIMATES = [ + PolarisButtonEntityDescription( + key="button_reset_filter", + name="button_reset_filter", + translation_key="button_reset_filter", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[100]", + icon="mdi:filter", + ) +] + +BUTTON_CLIMATES_200 = [ + PolarisButtonEntityDescription( + key="button_reset_filter", + name="button_reset_filter", + translation_key="button_reset_filter", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[0,100]", + icon="mdi:filter", + ), + PolarisButtonEntityDescription( + key="button_reset_prefilter", + name="button_reset_prefilter", + translation_key="button_reset_prefilter", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[100,0]", + icon="mdi:filter", + ) +] + +BUTTON_AIRCLEANER = [ + PolarisButtonEntityDescription( + key="button_reset_filter", + name="button_reset_filter", + translation_key="button_reset_filter", + mqttTopicCommand="control/expendables", + device_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + payloads="[0]", + icon="mdi:filter", + ) +] + +@dataclass +class PolarisTimeEntityDescription(TimeEntityDescription): + + min_time: int | None = None + max_time: int | None = None + default_time: int | None = None + mqttTopicCurrentTime: str | None = None + mqttTopicCommandTime: str | None = None + +TIME_COOKER = [ + PolarisTimeEntityDescription( + key="delay_start", + name="delay_start", + translation_key="delay_start", + mqttTopicCurrentTime="state/delay_start", + mqttTopicCommandTime="control/delay_start", + min_time=1, + max_time=720, + default_time=0, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + ), + PolarisTimeEntityDescription( + key="cooking_time", + name="cooking_time", + translation_key="cooking_time", + mqttTopicCurrentTime="control/cooking_time", + mqttTopicCommandTime="control/cooking_time", + min_time=1, + max_time=720, + default_time=20, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=True, + ) +] + +@dataclass +class PolarisClimateEntityDescription(ClimateEntityDescription): + + fan_mode: str | None = None + fan_modes: str | None = None + preset_mode: str | None = None + preset_modes: str | None = None + hvac_modes: list | None = None + supported_features: int | None = None + mqttTopicStateTemperature: str | None = None + mqttTopicCommandTemperature: str | None = None + mqttTopicCurrentTemperature: str | None = None + mqttTopicStateFanMode: str | None = None + mqttTopicCommandFanMode: str | None = None + mqttTopicCommandPower: str | None = None + mqttTopicCurrentPresetMode: str | None = None + mqttTopicCommandPresetMode: str | None = None + mqttTopicCommandSwingMode: str | None = None + mqttTopicStateSwingMode: str | None = None + payload_on: str | None = None + payload_off: str | None = None + min_temp: int | None = None + max_temp: int | None = None + temp_step: int | None = None + swing_mode: str | None = None + swing_modes: str | None = None + + +CLIMATES = [ + PolarisClimateEntityDescription( + name = "Climate", + key = "climate", + translation_key = "climate", + fan_mode = "off", + fan_modes = {"off": "0", "1_speed": "1", "2_speed": "2", "3_speed": "3", "4_speed": "4", "5_speed": "5", "6_speed": "6", "7_speed": "7"}, + preset_mode = "passive", + preset_modes = {"hands": "1", "auto": "2", "night": "3", "turbo": "4", "passive": "5"}, + hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY], + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateTemperature = "state/temperature", + mqttTopicCommandTemperature = "control/temperature", + mqttTopicCurrentTemperature = "state/sensor/temperature", + mqttTopicStateFanMode = "state/speed", + mqttTopicCommandFanMode = "control/speed", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "5", + payload_off = "0", + min_temp = 5, + max_temp = 25, + temp_step = 1, + device_class = None, + ) +] + +CLIMATES_200 = [ + PolarisClimateEntityDescription( + name = "Climate", + key = "climate", + translation_key = "climate", + fan_mode = "off", + fan_modes = {"off": "0", "1_speed": "1", "2_speed": "2", "3_speed": "3", "4_speed": "4", "5_speed": "5", "6_speed": "6", "7_speed": "7", "8_speed": "8", "9_speed": "9"}, + preset_mode = "auto", + preset_modes = {"hands": "1", "auto": "2", "night": "3", "turbo": "4"}, + hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY], + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateTemperature = "state/temperature", + mqttTopicCommandTemperature = "control/temperature", + mqttTopicCurrentTemperature = "state/sensor/temperature", + mqttTopicStateFanMode = "state/speed", + mqttTopicCommandFanMode = "control/speed", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "2", + payload_off = "0", + min_temp = 5, + max_temp = 25, + temp_step = 1, + device_class = None, + ) +] + + +AIRCLEANER = [ + PolarisClimateEntityDescription( + name = "Aircleaner", + key = "aircleaner", + translation_key = "aircleaner", + fan_mode = "auto", + fan_modes = {"auto": "0", "low": "1", "medium": "2", "high": "3"}, + preset_mode = "auto", + preset_modes = {"auto": "1", "hands": "2", "night": "3"}, + hvac_modes = [HVACMode.OFF, HVACMode.DRY], + supported_features = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateFanMode = "state/intensity", + mqttTopicCommandFanMode = "control/intensity", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "1", + payload_off = "0", + device_class = None, + ) +] + +AIRCLEANER_EAP = [ + PolarisClimateEntityDescription( + name = "Aircleaner", + key = "aircleaner", + translation_key = "aircleaner", + fan_mode = "low", + fan_modes = {"off": "0", "low": "1", "medium": "2", "high": "3", "top": "4"}, + preset_mode = "auto", + preset_modes = {"auto": "1", "night": "2", "hands": "3"}, + hvac_modes = [HVACMode.OFF, HVACMode.DRY], + supported_features = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateFanMode = "state/fan_speed", + mqttTopicCommandFanMode = "control/fan_speed", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "1", + payload_off = "0", + device_class = None, + ) +] + +CLIMATES_HEATER = [ + PolarisClimateEntityDescription( + name = "Heater", + key = "heater", + translation_key = "heater", + fan_mode = "auto", + fan_modes = {"auto": "0", "10_percent": "1", "20_percent": "2", "30_percent": "3", "40_percent": "4", "50_percent": "5", "60_percent": "6", "70_percent": "7", "80_percent": "8", "90_percent": "9", "100_percent": "10"}, + preset_mode = "comfort", + preset_modes = {"comfort": "1", "eco": "2", "away": "3"}, + hvac_modes = [HVACMode.OFF, HVACMode.HEAT], + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE +# | ClimateEntityFeature.AUX_HEAT + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateTemperature = "state/temperature", + mqttTopicCommandTemperature = "control/temperature", + mqttTopicCurrentTemperature = "state/sensor/temperature", + mqttTopicStateFanMode = "state/speed", + mqttTopicCommandFanMode = "control/speed", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "1", + payload_off = "0", + min_temp = 5, + max_temp = 35, + temp_step = 1, + device_class = None, + ) +] + +CLIMATES_AIRCONDITIONER = [ + PolarisClimateEntityDescription( + name = "Conditioner", + key = "conditioner", + translation_key = "conditioner", + fan_mode = "auto", + fan_modes = {"auto": "0", "min": "1", "low": "2", "middle": "3", "high": "4", "max": "5"}, + preset_mode = "auto", + preset_modes = {"auto": "1", "cooling": "2", "defrosting": "3", "heating": "4", "fan": "5"}, + hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.FAN_ONLY, HVACMode.DRY, HVACMode.AUTO], + swing_mode = "off", + swing_modes = {"off": "0", "both": "1", "vertical": "2", "horizontal": "3"}, + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.SWING_MODE + ), + mqttTopicStateTemperature = "state/temperature", + mqttTopicCommandTemperature = "control/temperature", + mqttTopicCurrentTemperature = "state/sensor/temperature", + mqttTopicStateFanMode = "state/speed", + mqttTopicCommandFanMode = "control/speed", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + mqttTopicCommandSwingMode = "control/program_data/0", + mqttTopicStateSwingMode = "state/program_data/0", + payload_on = "1", + payload_off = "0", + min_temp = 16, + max_temp = 30, + temp_step = 1, + device_class = None, + ) +] + +CLIMATES_THERMOSTAT = [ + PolarisClimateEntityDescription( + name = "Thermostat", + key = "thermostat", + translation_key = "thermostat", + preset_mode = "comfort", + preset_modes = {"eco": "1", "comfort": "2", "turbo": "3", "antifrost": "4", "schedule": "5", "vacation": "6", "manual": "7"}, + hvac_modes = [HVACMode.OFF, HVACMode.HEAT], + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ), + mqttTopicStateTemperature = "state/temperature", + mqttTopicCommandTemperature = "control/temperature", + mqttTopicCurrentTemperature = "state/sensor/temperature", + mqttTopicCommandPower = "control/mode", + mqttTopicCurrentPresetMode = "state/mode", + mqttTopicCommandPresetMode = "control/mode", + payload_on = "1", + payload_off = "0", + min_temp = 5, + max_temp = 45, + temp_step = 1, + device_class = None, + ) +] + +@dataclass +class PolarisVacuumEntityDescription(ClimateEntityDescription): + + fan_mode: str | None = None + fan_modes: str | None = None + mqttTopicCommandMode: str | None = None + mqttTopicCurrentMode: str | None = None + mqttTopicBatteryState: str | None = None + mqttTopicBatteryLevel: str | None = None + mqttTopicStateFanMode: str | None = None + mqttTopicCommandFanMode: str | None = None + mqttTopicCommandFindMe: str | None = None + mqttTopicCommandGoArea: str | None = None + +VACUUM = [ + PolarisVacuumEntityDescription( + name = "Vacuum", + key = "vacuum", + translation_key = "vacuum", + mqttTopicCommandMode = "control/mode", + mqttTopicCurrentMode = "state/mode", + mqttTopicStateFanMode = "state/suction", + mqttTopicCommandFanMode = "control/suction", + mqttTopicBatteryLevel = "state/battery", + mqttTopicBatteryState = "state/battery_state", + mqttTopicCommandFindMe = "control/find_me", + mqttTopicCommandGoArea = "control/go_area", + device_class = None, + ) +] + + +@dataclass +class PolarisImageEntityDescription(ImageEntityDescription): + + mqttTopicCommandGoArea: str | None = None + +IMAGE = [ + PolarisImageEntityDescription( + name = "Image", + key = "image", + translation_key = "image", + mqttTopicCommandGoArea = "control/go_area", + device_class = None, + ) +] \ No newline at end of file diff --git a/homeassistant/config/custom_components/polaris/humidifier.py b/homeassistant/config/custom_components/polaris/humidifier.py new file mode 100644 index 0000000..9472ec9 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/humidifier.py @@ -0,0 +1,224 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature # ????? +from homeassistant.components.humidifier import ( + DOMAIN, + HumidifierAction, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, +) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + HUMIDIFIERS, + PolarisHumidifierEntityDescription, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_HUMIDDIFIER_7_MODE_TYPE, + POLARIS_HUMIDDIFIER_5A_MODE_TYPE, + POLARIS_HUMIDDIFIER_5B_MODE_TYPE, + POLARIS_HUMIDDIFIER_4_MODE_TYPE, + POLARIS_HUMIDDIFIER_3A_MODE_TYPE, + POLARIS_HUMIDDIFIER_3B_MODE_TYPE, + POLARIS_HUMIDDIFIER_2_MODE_TYPE, + POLARIS_HUMIDDIFIER_1_MODE_TYPE, + POLARIS_HUMIDDIFIER_11_MODE_TYPE, + HUMIDDIFIER_5A_AVAILABLE_MODES, + HUMIDDIFIER_5B_AVAILABLE_MODES, + HUMIDDIFIER_4_AVAILABLE_MODES, + HUMIDDIFIER_3A_AVAILABLE_MODES, + HUMIDDIFIER_3B_AVAILABLE_MODES, + HUMIDDIFIER_2_AVAILABLE_MODES, + HUMIDDIFIER_1_AVAILABLE_MODES, + HUMIDDIFIER_11_AVAILABLE_MODES, +) + +SUPPORT_FLAGS = HumidifierEntityFeature(1) + +#_LOGGER = logging.getLogger(__name__) +#_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + humidifierList = [] + + if (device_type in POLARIS_HUMIDDIFIER_TYPE): + # Create humidifier + HUMIDIFIERS_LC = copy.deepcopy(HUMIDIFIERS) + for description in HUMIDIFIERS_LC: + description.mqttTopicCurrentState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentState}" + description.mqttTopicCommandState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandState}" + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicCurrentHumidity = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentHumidity}" + description.mqttTopicCurrentTargetHumidity = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTargetHumidity}" + description.mqttTopicCommandTargetHumidity = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTargetHumidity}" + description.device_prefix_topic = device_prefix_topic + humidifierList.append( + PolarisHumidifier( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(humidifierList, update_before_add=True) + + +class PolarisHumidifier(PolarisBaseEntity, HumidifierEntity): + + entity_description: PolarisHumidifierEntityDescription + _attr_supported_features = HumidifierEntityFeature.MODES + + def __init__( + self, + device_friendly_name: str, + description: PolarisHumidifierEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + device_class: HumidifierDeviceClass | None = None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_is_on = True + self._attr_max_humidity = description.max_humidity + self._attr_min_humidity = description.min_humidity + if device_type in POLARIS_HUMIDDIFIER_7_MODE_TYPE: + self.my_operation_list = description.available_modes + elif device_type in POLARIS_HUMIDDIFIER_5A_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_5A_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_5B_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_5B_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_4_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_4_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_3A_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_3A_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_3B_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_3B_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_2_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_2_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_1_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_1_AVAILABLE_MODES + elif device_type in POLARIS_HUMIDDIFIER_11_MODE_TYPE: + self.my_operation_list = HUMIDDIFIER_11_AVAILABLE_MODES + self._attr_available_modes = list(self.my_operation_list.keys()) + self._attr_mode = self._attr_available_modes[0] + self.payload_on=description.payload_on + self.payload_off=description.payload_off + self._attr_has_entity_name = True + self._attr_available = False + self._attr_current_humidity = self._attr_min_humidity + self._attr_target_humidity = self._attr_min_humidity + + + async def async_added_to_hass(self): + @callback + def message_received_curr_humid(message): + self._attr_current_humidity = float(message.payload) + self.async_write_ha_state() + @callback + def message_received_targ_humid(message): + if float(message.payload) < self._attr_min_humidity: + self._attr_target_humidity = self._attr_min_humidity + else: + self._attr_target_humidity = float(message.payload) + self.async_write_ha_state() + @callback + def message_received_mode(message): + payload = message.payload + if int(payload)==0: + self._attr_is_on = 0 + else: + self._attr_is_on = 1 + self._attr_mode = list(self.my_operation_list.keys())[list(self.my_operation_list.values()).index(message.payload)] + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentHumidity, + message_received_curr_humid, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentTargetHumidity, + message_received_targ_humid, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentState, + message_received_mode, + 1, + ) + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + def turn_on(self, **kwargs): + self._attr_is_on = self.payload_on + topic = f"{self.entity_description.mqttTopicCommandState}" + self.publishToMQTT(topic) + + def turn_off(self, **kwargs): + self._attr_is_on = self.payload_off + topic = f"{self.entity_description.mqttTopicCommandState}" + self.publishToMQTT(topic) + + def publishToMQTT(self, topic: str): + mqtt.publish(self.hass, topic, str(self._attr_is_on)) + + def set_humidity(self, humidity: int): + self._attr_target_humidity = humidity + topic = f"{self.entity_description.mqttTopicCommandTargetHumidity}" + mqtt.publish(self.hass, topic, str(humidity)) + + + def set_mode(self, mode: str): + self._attr_mode = mode + topic = f"{self.entity_description.mqttTopicCommandMode}" + mqtt.publish(self.hass, topic, self.my_operation_list[mode]) + diff --git a/homeassistant/config/custom_components/polaris/icons.json b/homeassistant/config/custom_components/polaris/icons.json new file mode 100644 index 0000000..a1121cc --- /dev/null +++ b/homeassistant/config/custom_components/polaris/icons.json @@ -0,0 +1,274 @@ +{ + "entity": { + "humidifier": { + "humidifier": { + "default": "mdi:air-humidifier", + "state": { + "off": "mdi:air-humidifier-off" + }, + "state_attributes": { + "mode": { + "state": { + "auto": "mdi:refresh-auto", + "baby": "mdi:baby-carriage", + "boost": "mdi:rocket-launch", + "comfort": "mdi:heart-pulse", + "eco": "mdi:cloud-percent", + "home": "mdi:gesture-tap", + "sleep": "mdi:power-sleep", + "fitnes": "mdi:walk", + "yoga": "mdi:yoga", + "meditation": "mdi:meditation", + "prana_hand": "mdi:human-handsdown", + "prana_auto": "mdi:human-handsup", + "aroma": "mdi:scent" + } + } + } + } + }, + "water_heater": { + "water_heater": { + "default": "mdi:kettle", + "state": { + "off": "mdi:kettle-off" + }, + "state_attributes": { + "operation_mode": { + "default": "mdi:circle-medium", + "state": { + "eco": "mdi:water-plus", + "electric": "mdi:heat-wave", + "gas": "mdi:fire-circle", + "heat_pump": "mdi:water-thermometer", + "high_demand": "mdi:finance", + "off": "mdi:power", + "performance": "mdi:chart-bubble" + } + } + } + }, + "water_boiler": { + "default": "mdi:water-boiler", + "state": { + "off": "mdi:water-boiler-off" + }, + "state_attributes": { + "operation_mode": { + "state": { + "off": "mdi:power", + "performance": "mdi:chart-bubble", + "electric": "mdi:heat-wave", + "heat_pump": "mdi:water-thermometer", + "eco": "mdi:water-plus" + } + } + } + } + }, + "switch": { + "sound_switch": { + "default": "mdi:volume-high", + "state": { + "off": "mdi:volume-off" + } + }, + "child_lock_switch": { + "default": "mdi:lock-open-variant", + "state": { + "on": "mdi:lock" + } + }, + "backlight_switch": { + "default": "mdi:alarm-light", + "state": { + "off": "mdi:alarm-light-off" + } + }, + "backlight_bottom_switch": { + "default": "mdi:lightbulb-on", + "state": { + "off": "mdi:lightbulb-on-50" + } + }, + "backlight_bright": { + "default": "mdi:lightbulb-on", + "state": { + "off": "mdi:lightbulb-on-50" + } + }, + "damper_heater": { + "default": "mdi:window-closed-variant", + "state": { + "on": "mdi:window-open-variant" + } + }, + "display_off_heater": { + "default": "mdi:lightbulb", + "state": { + "on": "mdi:lightbulb-auto" + } + }, + "half_power_heater": { + "default": "mdi:fraction-one-half", + "state": { + "off": "mdi:fraction-one-half" + } + }, + "auto_heater_switch": { + "default": "mdi:snowflake", + "state": { + "off": "mdi:snowflake-off" + } + }, + "self_cleaning": { + "default": "mdi:autorenew", + "state": { + "off": "mdi:autorenew-off" + } + }, + "eco_mode_switch": { + "default": "mdi:leaf", + "state": { + "off": "mdi:leaf-off" + } + } + }, + "binary_sensor": { + "base_binary_sensor": { + "default": "mdi:home", + "state": { + "on": "mdi:home-off" + } + }, + "water_tank_binary_sensor": { + "default": "mdi:cup", + "state": { + "on": "mdi:cup-off" + } + }, + "lid_binary_sensor": { + "default": "mdi:pot", + "state": { + "on": "mdi:pot-steam" + } + }, + "cappuccinator_binary_sensor": { + "default": "mdi:cup-off", + "state": { + "on": "mdi:cup" + } + } + }, + "climate": { + "climate": { + "default": "mdi:hvac", + "state": { + "off": "mdi:hvac-off" + }, + "state_attributes": { + "fan_mode": { + "state": { + "1_speed": "mdi:fan", + "2_speed": "mdi:fan", + "3_speed": "mdi:fan", + "4_speed": "mdi:fan", + "5_speed": "mdi:fan", + "6_speed": "mdi:fan", + "7_speed": "mdi:fan", + "8_speed": "mdi:fan", + "9_speed": "mdi:fan", + "off": "mdi:fan-off" + } + }, + "preset_mode": { + "default": "mdi:hvac", + "state": { + "hands": "mdi:gesture-tap", + "auto": "mdi:molecule-co2", + "night": "mdi:weather-night", + "turbo": "mdi:rocket-launch", + "passive": "mdi:air-filter" + } + } + } + }, + "aircleaner": { + "default": "mdi:air-purifier", + "state": { + "off": "mdi:air-purifier-off" + }, + "state_attributes": { + "preset_mode": { + "default": "mdi:hvac", + "state": { + "hands": "mdi:gesture-tap", + "auto": "mdi:refresh-auto", + "night": "mdi:weather-night" + } + }, + "fan_mode": { + "state": { + "top": "mdi:rocket-launch" + } + } + } + }, + "heater": { + "default": "mdi:radiator", + "state": { + "off": "mdi:radiator-off" + }, + "state_attributes": { + "fan_mode": { + "state": { + "auto": "mdi:flash-auto", + "10_percent": "mdi:numeric-1-box", + "20_percent": "mdi:numeric-2-box", + "30_percent": "mdi:numeric-3-box", + "40_percent": "mdi:numeric-4-box", + "50_percent": "mdi:numeric-5-box", + "60_percent": "mdi:numeric-6-box", + "70_percent": "mdi:numeric-7-box", + "80_percent": "mdi:numeric-8-box", + "90_percent": "mdi:numeric-9-box", + "100_percent": "mdi:numeric-10-box", + "20_5_percent": "mdi:numeric-1-box", + "40_5_percent": "mdi:numeric-2-box", + "60_5_percent": "mdi:numeric-3-box", + "80_5_percent": "mdi:numeric-4-box", + "100_5_percent": "mdi:numeric-5-box" + } + } + } + }, + "conditioner": { + "default": "mdi:air-conditioner", + "state_attributes": { + "fan_mode": { + "state": { + "min": "mdi:fan-minus", + "low": "mdi:fan-speed-1", + "middle": "mdi:fan-speed-2", + "high": "mdi:fan-speed-3", + "max": "mdi:fan-plus" + } + } + } + }, + "thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "turbo": "mdi:rocket-launch", + "antifrost": "mdi:snowflake-off", + "schedule": "mdi:calendar-blank", + "vacation": "mdi:plane-car", + "manual": "mdi:cog" + } + } + } + } + } + } +} diff --git a/homeassistant/config/custom_components/polaris/image.py b/homeassistant/config/custom_components/polaris/image.py new file mode 100644 index 0000000..946ef57 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/image.py @@ -0,0 +1,310 @@ + +import json +import re +import logging +from typing import Iterable, Final, Any +import copy +import datetime +import os +import voluptuous as vol +import struct +import numpy as np +import collections +import math + +from io import BytesIO +from PIL import Image, ImageDraw + +from random import SystemRandom +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback, ServiceCall +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.components.image import ImageEntity, DOMAIN, ImageEntityDescription +#from homeassistant.components.camera import Camera, DOMAIN, CameraEntityDescription, CameraEntityFeature +from homeassistant.helpers.typing import ( + UNDEFINED, +) +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + CUSTOM_SELECT_FILE_PATH, + PolarisImageEntityDescription, + POLARIS_VACUUM_TYPE, + IMAGE, +) + +DEFAULT_IMAGE_WIDTH = 500 +DEFAULT_IMAGE_HEIGHT = 500 +DEFAULT_RECTANGLE_COLOR = 'red' +ATTR_GENERATED_AT = "access_token" +_RND: Final = SystemRandom() + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + + + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + imageList = [] + + + if (device_type in POLARIS_VACUUM_TYPE): + IMAGE_LC = copy.deepcopy(IMAGE) + for description in IMAGE_LC: + description.mqttTopicCommandGoArea = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandGoArea}" + + imageList.append( + PolarisImage( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + ) + async_add_entities(imageList, update_before_add=True) + + +class PolarisImage(PolarisBaseEntity, ImageEntity): + + + entity_description: PolarisImageEntityDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisImageEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + + + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_has_entity_name = True + + self._available = True + self._last_update = False + + + + self._attr_model = "polaris" + self.access_tokens: collections.deque = collections.deque([], 2) +# self._name = name +# self._entity_id = entity_id + self._image = None + self._attr_content_type = "image/png" + self.async_update_token() +# self._generated_at = None + + + + + self._custom_data_select = self._read_file() + self.coord_rooms = {} + if self._custom_data_select is not None: + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cleaner" and "SELECT_VACUUM_rooms" in self._custom_data_select: + for key, value in self._custom_data_select["SELECT_VACUUM_rooms"].items(): + self.coord_rooms[key] = value["coordinate"] + _LOGGER.debug("rooms %s", self.coord_rooms) + + + + + + + def image(self) -> bytes | None: + """Return bytes of image.""" +# if self._image: +# return self._image + + + # Создание изображения + img = Image.new('RGB', (DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT), color='#c4c4c4') + d = ImageDraw.Draw(img) + + + rectangles = [] + for key, value in self.coord_rooms.items(): + rectangles.append(value) +# _LOGGER.debug("rectangles %s", rectangles) + self.draw_quadrilaterals(d, value, angle=89, color="yellow") + + + no_go_area = b'\x018\x00\x04\x00:\x00\x9e\xffk\x00\x9f\xffj\x00\x05\x00\x01\x1f\x00N\x00\x1f\x00?\x00t\x00A\x00s\x00O\x00\x01d\x00\x93\x00e\x00P\x00v\x00P\x00u\x00\x94\x00\x01\x1e\x00\x93\x00\x1f\x00\x85\x00`\x00\x87\x00`\x00\x94\x00\x00\x17\x00\x84\x00\x18\x00N\x00%\x00O\x00$\x00\x84\x00\x00-\x00,\x00-\x00 \x00[\x00!\x00Z\x00,\x00\x00\xfb\xff\xc8\xff\xfc\xff\x9f\xff\t\x00\x9f\xff\x08\x00\xc8\xff\x01\x83\xff\x8d\x00\x85\xff>\x00\xa2\xff>\x00\xa1\xff\x8d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +# _LOGGER.debug("no go area %s", self.parse_no_go_area(no_go_area)) + for i in range(0, len(self.parse_no_go_area(no_go_area)["coord_area"])): + self.draw_quadrilaterals(d, self.parse_no_go_area(no_go_area)["coord_area"][i], angle=89, color="red" if self.parse_no_go_area(no_go_area)["type_area"][i] == 0 else "blue") + + + + #Поиск маршрута + bbuf = b'\x00\x98\x00\xd0\x01~\x03\xa32\x01\x11\x03\xbd\x02\x01\t\x03\'\x01\x01\x02\x05\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x01\x02\x07\x01\x01\x03&\x01\x01\x02\x06\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x01\x02\x07\x01\x01\x03&\x01\x01\x02\x06\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x01\x02\x07\x01\x08\x03\x1f\x01\x01\x02\x06\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x01\x02\x0e\x01\x01\x03\x1f\x01\x01\x02\x06\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x01\x02\x0e\x01\x02\x03\x1e\x01\x01\x02\x06\x01\x01\x02\t\x01\x01\x03\xbd\x02\x01\x07\x02\t\x01\x01\x03\x1e\x01\x01\x02\x10\x01\x05\x03\xbc\x02\x01\x02\x02\x0b\x01\x05\x03\x1a\x01\x01\x02\x14\x01\x01\x03\xbc\x02\x01\x01\x02\x10\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x12\x03\xc6\x01\x01\x01\x02\x10\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x01\x02\x10\x01\x01\x03\xc6\x01\x01\x01\x02\x10\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x01\x02\x10\x01\x01\x03\xc6\x01\x01\x01\x02\x10\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x01\x02\x11\x01\x03\x03\xc2\x01\x01\x02\x02\x10\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x01\x02\x13\x01\x01\x03\xc2\x01\x01\x01\x02\x11\x01\x01\x03\x1a\x01\x01\x02\x14\x01\x01\x03d\x01\x01\x02\x13\x01\x06\x03\xbd\x01\x01\x01\x02\x11\x01\x1d\x02\x13\x01\x01\x03d\x01\x01\x02\x18\x01\x01\x03\xba\x01\x01\x04\x02\x11\x01\x1e\x02\x12\x01\x01\x03d\x01\x01\x02\x18\x01"\x03\x93\x01\x01\x07\x02D\x01\x01\x03d\x01\x01\x029\x01\x01\x03\x93\x01\x01\x01\x02:\x01\x02\x02\x0e\x01\x01\x03d\x01\x01\x029\x01\x01\x03\x93\x01\x01\x01\x02F\x01\x06\x03c\x01\x01\x029\x01\x01\x03\x93\x01\x01\x01\x02\x0b\x01\x01\x02\n\x01\x02\x02.\x01\x0f\x03Z\x01\x01\x029\x01\x01\x03\x93\x01\x01\x01\x02\x16\x01\x02\x02<\x01\x01\x03Z\x01\x01\x029\x01p\x03$\x01\x01\x02T\x01\x01\x03Z\x01\x01\x02\xa8\x01\x01\x01\x03"\x01\x03\x02T\x01\x01\x03Z\x01\x01\x02\xa8\x01\x01\x01\x03"\x01\x01\x02V\x01\x01\x03Z\x01\x01\x02\xa8\x01\x01\x01\x03 \x01\x03\x02V\x01\x01\x03Z\x01\x01\x02\xa8\x01\x01\x01\x03 \x01\x01\x02X\x01\x01\x03Z\x01\x01\x02\xa8\x01\x01\x01\x03\x1e\x01\x03\x02X\x01\x02\x03S\x01\x07\x02\xa8\x01\x01\x01\x03\x1e\x01\x01\x02[\x01\x01\x03S\x01\x01\x02\xae\x01\x01\x01\x03\x1c\x01\x03\x02[\x01\x01\x03S\x01\x01\x02?\x01\x02\x02m\x01\x01\x03\x1c\x01\x01\x02]\x01\x01\x03S\x01\x01\x02\xae\x01\x01\x01\x03\x1b\x01\x02\x02]\x01\x01\x03S\x01\x01\x02\xae\x01\x01\x01\x03\x1b\x01\x02\x02]\x01\x01\x03S\x01\x01\x02\x11\x01\x01\x02\x9c\x01\x01\x01\x03\x1b\x01\x02\x02]\x01\x01\x03S\x01\x01\x02\x11\x01\x01\x02\x9c\x01\x01\x01\x03\x1b\x01\x02\x02]\x01\x02\x03R\x01\x01\x02\x11\x01\x01\x02\x9c\x01\x01\x01\x03\x1b\x01\x02\x02^\x01\x01\x03R\x01\x01\x02\x11\x01\x01\x02\x9c\x01\x01\x01\x03\x1b\x01\x02\x02^\x01\x01\x03R\x01\x01\x02\x11\x01\x01\x02\x9c\x01\x01\x01\x03\x1b\x01\x02\x02^\x01\x04\x03O\x01\x01\x02\x11\x01\x02\x02\x06\x01\x02\x02\x93\x01\x01\x01\x03\x1b\x01\x02\x02a\x01\x04\x03L\x01\x01\x02\x10\x01\n\x02\x94\x01\x01\x01\x03\x1b\x01\x01\x02e\x01\x01\x03L\x01\x01\x02\n\x01\t\x03\x03\x01\x02\x02\x96\x01\x01\x01\x03\x1b\x01\x01\x02e\x01\x01\x03L\x01\x01\x02\x12\x01\x01\x03\x03\x01\x01\x02\x80\x01\x01\x06\x02\x11\x01\x01\x03\x1b\x01\x01\x02e\x01\x01\x03L\x01\x01\x02\x12\x01\x05\x02\x97\x01\x01\x01\x03\x19\x01\x03\x02e\x01\x01\x03L\x01\x01\x02\xae\x01\x01\x01\x03\x18\x01\x02\x02g\x01\x01\x03L\x01\x01\x02\x8b\x01\x01\x02\x02\x11\x01\x05\x02\x0b\x01\x01\x03\r\x01\x0b\x02U\x01\x01\x02\x13\x01\x01\x03L\x01\x01\x02\x9e\x01\x01\x01\x03\x03\x01\x05\x02\x07\x01\x01\x03\x07\x01\x07\x02)\x01\x02\x024\x01\x01\x02\x13\x01\x01\x03L\x01\x01\x02\x17\x01\x01\x02\x86\x01\x01\x01\x03\x07\x01\x01\x02\x07\x01\x01\x03\x07\x01\x01\x02\x05\x01\x01\x02)\x01\x02\x024\x01\x01\x02\x13\x01\x01\x03L\x01\x01\x02\x0f\x01\x01\x02\x07\x01\x01\x02\x86\x01\x01\t\x02\x07\x01\x01\x03\x07\x01\x01\x02\x05\x01\x01\x02)\x01\x02\x02H\x01\x01\x03L\x01\x01\x02\x0e\x01\x01\x02\x08\x01\x01\x02h\x01\x01\x02-\x01\x01\x03\x07\x01\x01\x02\x05\x01\x01\x02B\x01\x04\x02-\x01\x01\x03L\x01\x01\x02\x17\x01\x01\x02h\x01\x02\x02,\x01\x01\x03\x07\x01\x01\x02H\x01\x01\x03\x02\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x17\x01\x01\x02\x96\x01\x01\x01\x03\x07\x01\x01\x02H\x01\x01\x03\x02\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x16\x01\x13\x02\x85\x01\x01\x01\x03\x07\x01\x01\x02H\x01\x01\x03\x02\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x14\x01\x02\x03\x12\x01\x01\x02\x85\x01\x01\x01\x03\x07\x01\x01\x02H\x01\x02\x03\x01\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x14\x01\x01\x03\x13\x01\x01\x02\x85\x01\x01\x01\x03\x07\x01\x01\x02I\x01\x01\x03\x01\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x14\x01\x01\x03\x13\x01\x01\x02\x85\x01\x01\x01\x03\x07\x01\x01\x02I\x01\x01\x03\x01\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x13\x03\x15\x01\x01\x02g\x01\x01\x02\x1d\x01\x01\x03\x07\x01\x01\x02I\x01\x03\x02-\x01\x01\x03L\x01\x01\x02\x13\x03\x07\x01\x0f\x02g\x01\x01\x02\x1d\x01\x01\x03\x07\x01\x01\x02J\x01\x02\x02-\x01\x01\x03L\x01\x01\x02\x12\x03\x08\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02j\x01\x01\x02(\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02y\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02K\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02K\x01\x01\x02-\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02u\x01\x01\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\x01\x02\x93\x01\x01\x01\x03\x07\x01\x01\x02u\x01\x01\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\t\x01\t\x02\x8b\x01\x01\x01\x03\x07\x01\x01\x02u\x01\x01\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\x11\x01\x01\x02\x8b\x01\x01\x01\x03\x07\x01\x01\x02u\x01\x01\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\x11\x01\x01\x02\x82\x01\x01\x02\x02\x07\x01\x01\x03\x07\x01\x01\x02u\x01\x01\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\x11\x01\x01\x02\x8b\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x03\x02\x03\x01\x01\x03L\x01\x01\x02\x11\x03\x11\x01\x02\x02\x8a\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x01\x01\x05\x03L\x01\x01\x02\x11\x03\x12\x01\x01\x02\x8a\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03R\x01\x01\x02\x0e\x03\x15\x01\x01\x02\x8a\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03R\x01\x01\x02\x0e\x03\x15\x01\x01\x02\n\x01\x01\x02\x7f\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03R\x01\x01\x02\x0e\x03\x15\x01\x0b\x02\x80\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03R\x01\x01\x02\x06\x01\x07\x03\x1f\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03R\x01\x01\x02\x06\x01\x01\x03%\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02q\x01\x03\x03S\x02\x06\x01\x01\x03%\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02q\x01\x01\x03U\x02\x06\x01\x01\x03%\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02q\x01\x01\x03\x81\x01\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02q\x01\x01\x03\x81\x01\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02q\x01\x03\x03\x7f\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x81\x01\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02!\x01=\x02#\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02 \x01\x01\x03<\x01\x01\x02#\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x16\x01\n\x03=\x01\x01\x02#\x01\x01\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x14\x01\x03\x03F\x01\x01\x02 \x01\x04\x03\x07\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x14\x01\x01\x03H\x01\x03\x02\x1c\x01\x03\x03\n\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x14\x01\x01\x03J\x01\x01\x02\x1d\x01\x02\x03\n\x01\x01\x02s\x01\x01\x03\x7f\x01\x01\x02\x14\x01\x01\x03J\x01\x01\x02\x1e\x01\x01\x03\n\x01\x01\x02s\x01\x02\x03~\x01\x01\x02\x14\x01\x02\x03I\x01\x01\x02\x1e\x01\x02\x03\t\x01\x01\x02t\x01\x01\x03~\x01\x01\x02\x15\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02K\x01\x01\x02(\x01\x01\x03~\x01\x01\x02\x15\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02t\x01\x01\x03~\x01\x0b\x02\x0b\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02t\x01\x04\x03\x85\x01\x01\x01\x02\x0b\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02w\x01\x01\x03\x85\x01\x01\x04\x02\x08\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02w\x01\x01\x03\x88\x01\x01\x01\x02\x08\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02w\x01\x01\x03\x88\x01\x01\x01\x02\x08\x01\x01\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02+\x01\x01\x02?\x01\x01\x02\x0b\x01\x03\x03\x86\x01\x01\n\x03I\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02\x1f\x01\x01\x02\x0b\x01\x01\x02M\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02\x1f\x01\x01\x02Y\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02y\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02y\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02y\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02y\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02+\x01\x0b\x02C\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02+\x01\x01\x03\t\x01\x01\x02C\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02+\x01\x01\x03\t\x01\x01\x02C\x01\x01\x03\xd9\x01\x01\x01\x02\x1f\x01\x01\x03\t\x01\x01\x02%\x01\x07\x03\t\x01\x02\x02B\x01\x01\x03\xd9\x01\x01\x02\x02\x1e\x01\x02\x03\x08\x01\x01\x02%\x01\x01\x03\x10\x01\x01\x02@\x01\x03\x03\xda\x01\x01\x01\x02\x1f\x01\x01\x03\x08\x01\x01\x02\x1e\x01\x01\x02\x06\x01\x01\x03\x10\x01\x01\x02@\x01\x01\x03\xdc\x01\x01\x01\x02\x1f\x01\x01\x03\x08\x01\x01\x02\x1e\x01\x01\x02\x06\x01\x01\x03\x10\x01B\x03\xdc\x01\x01\x01\x02\x1f\x01\x01\x03\x08\x01\x01\x02\x1e\x01\x02\x02\x05\x01\x01\x03\xae\x02\x01\x01\x02\x1f\x01\x01\x03\x08\x01\x01\x02\x1b\x01\x0b\x03\xae\x02\x01\x05\x02\x1b\x01\x02\x03\x07\x01\x01\x02\x19\x01\x02\x03\xbd\x02\x01\x01\x02\x1c\x01\x01\x03\x07\x01\x01\x02\x19\x01\x01\x03\xbe\x02\x01\x01\x02\x1c\x01\x01\x03\x07\x01\x01\x02\x19\x01\x01\x03\xbe\x02\x01\x01\x02\x1c\x01\x01\x03\x07\x01\x01\x02\x19\x01\x01\x03\xbe\x02\x01\x01\x02\x1c\x01\t\x02\x19\x01\x01\x03\xbe\x02\x01\x01\x02>\x01\x01\x03\xbe\x02\x01\x01\x02>\x01\x01\x03\xb8\x02\x01\x07\x02>\x01\x07\x03\xb2\x02\x01\x01\x02E\x01\x02\x02\x03\x01\x01\x03\xc1\x01\x01\x1c\x03U\x01\x01\x02E\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x14\x03.\x01\x15\x02E\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x02\x02\x11\x01\x01\x03.\x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02\x12\x01\x01\x03.\x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02\x12\x01\x01\x03.\x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02\x12\x01\x01\x03.\x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02\x12\x01\x0f\x03 \x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02 \x01\x01\x03 \x01\x01\x02Y\x01\x01\x02\x04\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02 \x01\x01\x03 \x01\x01\x02^\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02 \x01\x01\x03 \x01\x01\x02^\x01\x01\x03\xc1\x01\x01\x01\x02\x1a\x01\x01\x02!\x01\x01\x03\x1f\x01\x01\x02^\x01\x01\x03\xbb\x01\x01\x07\x02\x1a\x01\x01\x02!\x01\x05\x03\x1b\x01\x01\x02^\x01\x01\x03\xbb\x01\x01\x01\x02 \x01\x01\x02%\x01\x01\x03\x1b\x01\x01\x02^\x01\x01\x03\xbb\x01\x01\x01\x02 \x01\x02\x02%\x01\x01\x03\x1a\x01\x01\x02^\x01\x01\x03\xbb\x01\x01\x01\x02 \x01\x03\x02$\x01\x01\x03\x13\x01\x08\x02^\x01\x01\x03\xbb\x01\x01\x01\x02 \x01\x03\x02$\x01\x01\x03\x13\x01\x01\x02e\x01\x01\x03\xbb\x01\x01\x01\x02 \x01\x03\x02$\x01\x01\x03\x13\x01\x01\x02e\x01\x01\x03\xa8\x01\x01\x14\x02 \x01\x03\x02$\x01\x01\x03\x13\x01\x01\x02e\x01\x01\x03\xa8\x01\x01\x01\x025\x01\x01\x02$\x01\x15\x02e\x01\x01\x03\xa8\x01\x01\x01\x02\xd4\x01\x01\x01\x03\xa8\x01\x01\x01\x02\xcf\x01\x01\x06\x03\xa8\x01\x01\x01\x02\xcf\x01\x01\x01\x03\x84\x01\x01\x1e\x03\x0b\x01\x01\x02\xcf\x01\x01\x01\x03\x84\x01\x01\x01\x02\x1c\x01\x01\x03\x0b\x01\x02\x02\xce\x01\x01\x01\x03\x84\x01\x01\x01\x02\x1c\x01\x01\x03\x0c\x01\x01\x02\xcb\x01\x01\x04\x03\x84\x01\x01\x01\x02\x1c\x01\x01\x03\x0c\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02\x1c\x01\x01\x03\x0c\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02\x1c\x01\x02\x03\x0b\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02\x1e\x01\x05\x03\x06\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02"\x01\x01\x03\x06\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02"\x01\x01\x03\x06\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02"\x01\x01\x03\x06\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02"\x01\x01\x03\x06\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02#\x01\x01\x03\x05\x01\x01\x02\xcb\x01\x01\x01\x03\x87\x01\x01\x01\x02#\x01\x05\x03\x01\x01\x01\x022\x01\x03\x02\x8f\x01\x01\x08\x03\x87\x01\x01\x01\x02\'\x01\x01\x03\x01\x01\x01\x022\x01\x01\x03\x01\x01\x01\x02\x92\x01\x01\x01\x03\x8b\x01\x01\x01\x02\'\x01\x04\x02\x0e\x01\x04\x02\x1f\x01\x03\x02\x92\x01\x01\x01\x03\x8b\x01\x01\x01\x02\'\x01\x11\x02\xb9\x01\x01\x01\x03\x8b\x01\x01\x01\x02\xbe\x01\x01\x01\x022\x01\x01\x03\x8b\x01\x01\x01\x02\xbe\x01\x01\x01\x022\x01\x02\x03\x8a\x01\x01\x01\x02\xbd\x01\x01\x04\x022\x01\x06\x03\x84\x01\x01\x01\x02\xac\x01\x01\x11\x03\x03\x01\x01\x027\x01\x01\x03\x84\x01\x01\x01\x02\xac\x01\x01\x01\x03\x13\x01\x01\x027\x01\x01\x03\x84\x01\x01\x01\x02\xac\x01\x01\x01\x03\x13\x01\x01\x027\x01\x01\x03\x84\x01\x01\x01\x02\xac\x01\x01\x01\x03\x13\x01\x03\x025\x01\x01\x03\x84\x01\x01\x01\x02\xa7\x01\x01\x06\x03\x14\x01\x02\x025\x01\x01\x03\x84\x01\x01\x01\x02\xab\x01\x01\x01\x03\x15\x01\x02\x025\x01\x01\x03\x84\x01\x01\x01\x02\xab\x01\x01\x01\x03\x15\x01\x02\x025\x01\x01\x03\x84\x01\x01\x01\x02\xab\x01\x01\x01\x03\x15\x01\x02\x025\x01\x01\x03\x84\x01\x01\x01\x02\xab\x01\x01\x01\x03\x15\x01\x02\x021\x01\x06\x03\x83\x01\x01\x01\x02\xab\x01\x01\x01\x03\x15\x01\x02\x021\x01\x0f\x03z\x01\x01\x02\xab\x01\x01\x01\x03\x0f\x01\x08\x02?\x01B\x039\x01\x02\x02\xaa\x01\x01\x01\x03\x0f\x01\x01\x02\x06\x01\x01\x02\x80\x01\x01\x01\x03:\x01\x01\x02\xaa\x01\x01\x01\x03\r\x01\x03\x02\x06\x01\x01\x02\x80\x01\x01\x01\x03:\x01\x01\x02\xaa\x01\x01\x01\x03\r\x01\x01\x02\x89\x01\x01\x01\x03:\x01\x17\x02\x94\x01\x01\x01\x03\r\x01\x01\x02\x89\x01\x01\x01\x03P\x01\x01\x02\x94\x01\x01\x01\x03\r\x01\x01\x02\x89\x01\x01\x01\x03P\x01\x01\x02\x94\x01\x01\x01\x03\r\x01\x01\x02\x89\x01\x01\x01\x03P\x01\x01\x02\x7f\x01\x16\x03\r\x01\x01\x02\x89\x01\x01\x01\x03P\x01\x07\x02y\x01\x01\x03"\x01\x01\x02\x89\x01\x01\x01\x03V\x01\x01\x02s\x01\x07\x03"\x01\x01\x02\x89\x01\x01\x01\x03V\x01\x0c\x02*\x01?\x03(\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\x18\x01;\x02\x15\x01\x05\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\r\x01\x0c\x035\x01\x02\x02\x1c\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\t\x01\x05\x03@\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\t\x01\x01\x03D\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\t\x01\x01\x03D\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\x07\x01\x03\x03D\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\x01\x02\x07\x01\x01\x03F\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03a\x01\t\x03F\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03\xb0\x01\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03\xb0\x01\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03\xb0\x01\x01\x01\x02\x1d\x01\x01\x03$\x01\x01\x02\x89\x01\x01\x01\x03\xb0\x01\x01\x05\x02\x15\x01\x05\x03$\x01\x01\x02\x89\x01\x01\x01\x03\xb4\x01\x01\x01\x02\x15\x01\x01\x03(\x01\x01\x02\x89\x01\x01\x01\x03\xb4\x01\x01\x17\x03(\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01\x01\x02\x89\x01\x01\x01\x03\xf3\x01\x01C\x02\x1a\x01.\x03\xb5\x02\x01\x01\x02\x1a\x01\x01\x03\xe2\x02\x01\x01\x02\x1a\x01\x01\x03\xe2\x02\x01\x01\x02\x1a\x01\x01\x03\xe2\x02\x01\x01\x02\x1a\x01\x01\x03\xe2\x02\x01\x02\x02\x19\x01\x01\x03\xe3\x02\x01\x01\x02\x19\x01,\x03\xb8\x02\x01\x01\x02D\x01\x01\x03\xb8\x02\x01\x01\x02D\x01\x01\x03\xb8\x02\x01\x01\x02D\x01\x01\x03\xb8\x02\x01\x01\x02D\x01\x01\x03\xb8\x02\x01\x02\x02C\x01\x01\x03\xb9\x02\x01\x01\x02C\x01\x01\x03\xb9\x02\x01\x01\x02A\x03\x01\x02\x01\x01\x01\x03\xb9\x02\x01\x01\x02A\x01\x03\x03\xb9\x02\x01\x01\x02>\x01\x04\x03\xbb\x02\x01\x01\x02>\x01\x01\x03\xbe\x02\x01\x01\x02>\x01\x01\x03\xbe\x02\x01\x01\x02>\x01\x01\x03\x0b\x01\x14\x03\x9f\x02\x01\x01\x02>\x01\x01\x03\t\x01\x02\x02\x13\x01\x01\x03\x9f\x02\x01\x02\x02=\x01\x01\x03\t\x01\x01\x02\x14\x01\x01\x03\xa0\x02\x01\x01\x02=\x01\x01\x03\t\x01\x01\x02\x14\x01\x01\x03\xa0\x02\x01\x01\x02=\x01\x01\x03\t\x01\x01\x02\x14\x01\x01\x03\xa0\x02\x01\x01\x02=\x01\x01\x03\x08\x01\x02\x02\x14\x01\x01\x03\xa0\x02\x01\x01\x02=\x01\t\x02\x16\x01\x01\x03\xa0\x02\x01\x01\x02\\\x01\x01\x03\xa0\x02\x01\x01\x02\\\x01\x01\x03\xa0\x02\x01\x01\x02\\\x01\x01\x03\xa0\x02\x01\x01\x02\\\x01\x01\x03\xa0\x02\x01\x02\x02[\x01\x01\x03\xa0\x02\x01\x02\x02[\x01\x01\x03\x9a\x02\x01\x08\x02[\x01\x01\x03\x9a\x02\x01\x01\x02\x06\x01\x01\x02[\x01\x01\x03\x9a\x02\x01\x01\x02\x06\x01\x01\x02[\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x9a\x02\x01\x01\x02b\x01\x01\x03\x94\x02\x01\x07\x02\x05\x01\x01\x02\\\x01\x01\x03\x94\x02\x01\x01\x02\x0b\x01\x01\x02\\\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02h\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x13\x02\x05\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x12\x02\x05\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x12\x02\x05\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x12\x02\x05\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x01\x02P\x01\x01\x03\x13\x02\x04\x01\x01\x03\x94\x02\x01\x02\x02O\x01\x01\x03\x14\x02\x03\x01\x01\x03\x95\x02\x01\x01\x02H\x01\x08\x03\x14\x02\x03\x01\x01\x03\x95\x02\x01\x01\x02H\x01\x01\x03\x1b\x02\x03\x01\x01\x03\x95\x02\x01\x01\x02@\x01\t\x03\x1b\x02\x03\x01\x01\x03\x95\x02\x01\x01\x02\t\x018\x03#\x02\x03\x01\x01\x03\x95\x02\x01\x01\x02\x06\x01\x04\x03[\x02\x02\x01\x01\x03\x95\x02\x01\x08\x03^\x02\x02\x01\x01\x03\xfb\x02\x02\x02\x01\x01\x03\xfb\x02\x02\x02\x01\x01\x03\xfb\x02\x02\x02\x01\x01\x03\xfc\x02\x02\x01\x01\x01\x03\xfc\x02\x02\x01\x01\x01\x03\xfc\x02\x02\x01\x01\x01\x03\xfc\x02\x02\x01\x01\x01\x03\xfd\x02\x01\x01\x03\xd4Y' + map_data0 = b'\x00\x00 \x06\x00\x00\x00 \x05\x00\x02\x00 \x05\x00\x05\x00\xa5 \x02\x00\x02\x00 \x03\x00\x00\x00\xa6 \x02\x00\x02\x00 \x02\x00\x04\x00 \x00\x00\x02\x00 \x00\x00\x00\x00 \x01\x00\xfe\xff \x00\x00\x00\x00 \x00\x00\x03\x00 \xff\xff\x01\x00\xa5 \x00\x00\xfe\xff \x00\x00\xfc\xff \xfe\xff\x00\x00\xa5 \xfd\xff\xfc\xff \xff\xff\xfb\xff\xa6\xa3 \xfb\xff\xfe\xff \xfb\xff\xfc\xff \xfc\xff\xfa\xff \xfe\xff\xf8\xff \xfb\xff\xfa\xff \xf9\xff\xfc\xff \xf9\xff\xfa\xff \xfa\xff\xf7\xff \xfc\xff\xf7\xff\xa7 \xff\xff\xf6\xff \xfd\xff\xf6\xff\xa1\xa7\xa4 \xfb\xff\xf9\xff\xa5 \xfa\xff\xf6\xff \xfc\xff\xf5\xff \xfe\xff\xf4\xff\xa0\xa7\xa2 \x00\x00\xf7\xff \x00\x00\xfb\xff \x02\x00\xfc\xff\xa0 \x06\x00\xfc\xff\xa6\xa7\xa7\xa0\xa0\xa4\xa3\xa7\xa0\xa4\xa2 \x08\x00\xfc\xff\xa4\xa2\xa4 \x05\x00\xfb\xff \x04\x00\xf9\xff \x04\x00\xf7\xff \x04\x00\xf5\xff\xa0\xa4\xa3\xa3\xa3 \x03\x00\xfb\xff\xa1\xa2 \x06\x00\xfc\xff\xa7\xa7\xa0\xa6\xa6 \t\x00\xf6\xff\xa6 \t\x00\xf3\xff\xa6\xa5\xa5\xa6\xa6\xa6\xa0\xa6 \x07\x00\xea\xff\xa6\xa4\xa2\xa3 \x03\x00\xeb\xff\xa4\xa4\xa4 \x00\x00\xe9\xff\xa6\xa5\xa5 \xfe\xff\xe4\xff \xfe\xff\xe2\xff\xa0\xa4\xa6\xa6 \xfe\xff\xde\xff\xa7\xa7\xa0\xa0 \x03\x00\xde\xff\xa0\xa2 \x04\x00\xe1\xff\xa2\xa1 \x06\x00\xe5\xff\xa6 \x06\x00\xe0\xff \x06\x00\xdd\xff \x06\x00\xd9\xff \x06\x00\xd6\xff \x06\x00\xd4\xff\xa6\xa4\xa6\xa6\xa0\xa6\xa7\xa6\xa6\xa0 \x08\x00\xcb\xff\xa5 \x07\x00\xc8\xff\xa6 \x08\x00\xc5\xff \x08\x00\xc2\xff \x08\x00\xbe\xff \x08\x00\xbc\xff\xa6 \x08\x00\xb9\xff\xa6\xa6 \x08\x00\xb5\xff \x08\x00\xb3\xff\xa6 \x08\x00\xb0\xff\xa6 \x08\x00\xad\xff\xa6 \x08\x00\xaa\xff\xa4 \x04\x00\xaa\xff \x00\x00\xaa\xff\xa4 \xff\xff\xac\xff\xa2\xa3 \xfe\xff\xb0\xff \xfe\xff\xb2\xff \xfe\xff\xb6\xff\xa3 \xfb\xff\xb7\xff \xf9\xff\xb7\xff \xf6\xff\xb7\xff \xf2\xff\xb7\xff \xee\xff\xb7\xff \xeb\xff\xb7\xff \xe7\xff\xb7\xff \xe5\xff\xb8\xff\xa2 \xe5\xff\xbb\xff \xe5\xff\xbd\xff \xe5\xff\xc1\xff \xe5\xff\xc5\xff \xe5\xff\xc9\xff \xe5\xff\xcd\xff \xe5\xff\xd1\xff \xe5\xff\xd5\xff \xe5\xff\xd9\xff \xe5\xff\xdd\xff \xe5\xff\xe1\xff \xe6\xff\xe3\xff \xe6\xff\xe5\xff \xe6\xff\xe7\xff \xe8\xff\xe8\xff \xe9\xff\xea\xff \xe9\xff\xec\xff\xa2 \xe9\xff\xf0\xff \xe9\xff\xf4\xff \xe9\xff\xf6\xff\xa1\xa2\xa0 \xed\xff\xf9\xff\xa0\xa2\xa2\xa2 \xee\xff\xfe\xff\xa2\xa2 \xee\xff\x02\x00 \xee\xff\x06\x00 \xee\xff\t\x00 \xef\xff\x0b\x00 \xef\xff\r\x00\xa6 \xef\xff\n\x00\xa5 \xee\xff\x07\x00 \xf0\xff\x06\x00 \xf2\xff\x06\x00 \xf3\xff\x08\x00 \xf2\xff\n\x00\xa1\xa0\xa7 \xf7\xff\t\x00\xa0 \xfa\xff\x0b\x00\xa2\xa3 \xfb\xff\r\x00 \xfe\xff\r\x00 \x01\x00\r\x00 \x03\x00\r\x00\xa7\xa6\xa2\xa7\xa2 \x06\x00\n\x00 \x06\x00\x08\x00 \x06\x00\x04\x00\xa7\xa4 \x04\x00\x01\x00 \x01\x00\xff\xff \x00\x00\xfd\xff \xfd\xff\xfa\xff \xfb\xff\xf8\xff\xa7 \xf2\xff\xfe\xff \xf5\xff\xfc\xff \xf7\xff\xf9\xff \xfa\xff\xf6\xff \xfd\xff\xf4\xff\xa7 \xff\xff\xf5\xff \x00\x00\xf8\xff \x03\x00\xfb\xff\xa0\xa0\xa0\xa7\xa0\xa7\xa6\xa6\xa6\xa6 \t\x00\xf3\xff \t\x00\xf1\xff\xa4\xa6\xa4 \x07\x00\xee\xff\xa6\xa0\xa6 \x07\x00\xea\xff \x07\x00\xe8\xff\xa3\xa3\xa2\xa2\xa1 \x06\x00\xef\xff \x06\x00\xf1\xff\xa3 \x03\x00\xf2\xff\xa4 \x00\x00\xf2\xff \x00\x00\xf0\xff \x00\x00\xee\xff \x00\x00\xec\xff\xa2 \x00\x00\xeb\xff \x02\x00\xeb\xff\xa0\xa0\xa0\xa2\xa6 \x03\x00\xeb\xff\xa4\xa4\xa4 \xfe\xff\xec\xff \xfc\xff\xec\xff \xf8\xff\xec\xff \xf5\xff\xec\xff \xf1\xff\xec\xff \xee\xff\xec\xff\xa4\xa2\xa2\xa2\xa0 \xf1\xff\xef\xff \xf5\xff\xef\xff \xf9\xff\xef\xff\xa0\xa0\xa2\xa2\xa2\xa4 \xf8\xff\xf2\xff \xf5\xff\xf2\xff \xf1\xff\xf2\xff \xee\xff\xf2\xff\xa4\xa2\xa2\xa2\xa0\xa0 \xf3\xff\xf5\xff\xff' # Ваши двоичные данные + data = list(map(int, map_data0)) + dp = [] + dpp = [] + dppp = [] + for i in data: + if i == 32: + dp.append(dpp) + dpp = [] + else: + dpp.append(i) + l_old = [250,250] + for i in range(1, len(dp)-1): + if dp[i][1] == 255: + intX = dp[i][0] - 256 + DEFAULT_IMAGE_WIDTH/2 + else: + intX = dp[i][0] + DEFAULT_IMAGE_WIDTH/2 + if dp[i][3] == 255: + intY = dp[i][2] - 256 + DEFAULT_IMAGE_WIDTH/2 + else: + intY = dp[i][2] + DEFAULT_IMAGE_WIDTH/2 + coord = [intX, intY] + dppp.append(coord) + for i in range(1, len(dppp)-1): + d.line((l_old[0],l_old[1],dppp[i][0],dppp[i][1]), fill='blue') + l_old = [dppp[i][0],dppp[i][1]] + + + + + # Сохранение изображения в байты + buffer = BytesIO() + +# img_width = 208 +# img_height = 152 +# img = Image.open(BytesIO(bytearray(bbuf, offset=4))).convert("RGB") +# img_width, img_height = img.size + + img.save(buffer, format="PNG") + self._image = buffer.getvalue() + + +# _LOGGER.debug("async_image %s", self._image) + self._last_update = True + return self._image + + async def async_update(self) -> None: + """Update the image (e.g., to trigger a new image generation).""" + self.async_update_token() +# self._image = None # Invalidate cached image, triggering async_image() to regenerate it + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + _LOGGER.debug("async_image") + if self._last_update: + self._last_update = False + self._image = self.image() + return self._image + return await super().async_image() + + + + @property + def available(self) -> bool: + """Return if the robot is available.""" + return self._available + + + # @property + # def extra_state_attributes(self) -> dict[str, Any]: + # """Return the state attributes of the vacuum cleaner.""" + # data: dict[str, Any] = {} + + # if self._generated_at is not None: + # data[ATTR_GENERATED_AT] = datetime.datetime.now() + # data["model_name"] = "polaris" + # return data + + + @callback + def async_update_token(self) -> None: + """Update the used token.""" + self.access_tokens.append(hex(_RND.getrandbits(256))[2:]) + + + def _read_file(self): + file_path = CUSTOM_SELECT_FILE_PATH + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + content = json.loads(file.read()) + else: + content = None + return content + + + def draw_quadrilaterals(self, draw, array, angle=0, color="grey"): + # Определяем смещение, чтобы центрировать фигуры + offset_x = DEFAULT_IMAGE_WIDTH / 2 + offset_y = DEFAULT_IMAGE_HEIGHT / 2 + center = (offset_x, offset_y) + # Преобразуем массив в список кортежей (x, y) + points = [(array[i], array[i+1]) for i in range(0, len(array), 2)] + # Смещаем координаты для центрирования + shifted_points = [(p[0] + offset_x, p[1] + offset_y) for p in points] + # Поворачиваем каждую точку + rotated_points = [self.rotate_point(p, angle, center) for p in shifted_points] + # Добавляем первую точку в конец, чтобы замкнуть четырехугольник + rotated_points.append(rotated_points[0]) + # Рисуем четырехугольник + draw.line(rotated_points, fill=color, width=3) + + + def rotate_point(self, point, angle_degrees, center): + # Переводим угол из градусов в радианы + angle_radians = math.radians(angle_degrees) + # Переносим точку в начало координат + translated_point = (point[0] - center[0], point[1] - center[1]) + # Применяем матрицу поворота + rotated_x = translated_point[0] * math.cos(angle_radians) - translated_point[1] * math.sin(angle_radians) + rotated_y = translated_point[0] * math.sin(angle_radians) + translated_point[1] * math.cos(angle_radians) + # Переносим точку обратно + rotated_point = (rotated_x + center[0], rotated_y + center[1]) + return rotated_point + + + def parse_no_go_area(self, no_go_area_array): + data = list(map(int, no_go_area_array)) + result = {"type_area": [], "coord_area": []} + for i in range(0, len(data), 17): + sublist = data[i:i+17] + if all(x == 0 for x in sublist): # Проверяем, что все элементы coord равны 0 + continue # Пропускаем такие подмассивы + if len(sublist) < 17: + break # Пропускаем неполные подмассивы (если длина не кратна 17) + result["type_area"].append(sublist[0]) + sublist_int16 = sublist[1:17] + coord_int16 = [] + for j in range(0, 16, 2): + two_bytes = bytes(sublist_int16[j:j+2]) + int16_value = struct.unpack(' None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + timeList = [] + + if (device_type in POLARIS_COOKER_TYPE): + TIME_COOKER_LC = copy.deepcopy(TIME_COOKER) + for description in TIME_COOKER_LC: + description.mqttTopicCurrentTime = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTime}" + description.mqttTopicCommandTime = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTime}" + description.device_prefix_topic = device_prefix_topic + timeList.append( + PolarisTime( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(timeList, update_before_add=True) + + +class PolarisTime(PolarisBaseEntity, TimeEntity): + + entity_description: PolarisTimeDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisTimeEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_available = False + self._attr_has_entity_name = True + self._attr_native_value = time(0, self.entity_description.default_time, 0) + + async def async_added_to_hass(self): + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + + async def async_set_value(self, value: time) -> None: + """Update the time.""" + value_hour=value.hour + value_minute=value.minute + value_second=0 + value_in_seconds = value.hour * 3600 + value.minute * 60 + if (value_in_seconds > (self.entity_description.max_time * 60)): + value_hour = int(self.entity_description.max_time / 60) - 1 + self._attr_native_value = time(value_hour,value_minute,value_second) + value_in_seconds = value_hour * 3600 + value_minute * 60 + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandTime, str(value_in_seconds)) + diff --git a/homeassistant/config/custom_components/polaris/light.py b/homeassistant/config/custom_components/polaris/light.py new file mode 100644 index 0000000..61eb289 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/light.py @@ -0,0 +1,260 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +from homeassistant.util import color as color_util +from homeassistant.components.light import ( + DOMAIN, + ATTR_BRIGHTNESS, + ATTR_RGB_COLOR, + ColorMode, + LightEntity, + LightEntityDescription, +) +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + LIGHTS, + LIGHTS_DOUBLE, + PolarisLightEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_KETTLE_WITH_NIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + lightList = [] + + if device_type in POLARIS_KETTLE_WITH_NIGHT_TYPE: + # Create water heater for kettle devices + LIGHTS_LC = copy.deepcopy(LIGHTS) + for description in LIGHTS_LC: + description.mqttTopicCurrentColor = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentColor}" + description.mqttTopicCommandColor = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandColor}" + description.mqttTopicCurrentState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentState}" + description.mqttTopicCommandState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandState}" + description.device_prefix_topic = device_prefix_topic + lightList.append( + PolarisLight( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if device_type == "835": + # Create humidifier double lights + LIGHTS_DOUBLE_LC = copy.deepcopy(LIGHTS_DOUBLE) + for description in LIGHTS_DOUBLE_LC: + description.mqttTopicCurrentColor = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentColor}" + description.mqttTopicCommandColor = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandColor}" + description.mqttTopicCurrentState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentState}" + description.mqttTopicCommandState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandState}" + description.device_prefix_topic = device_prefix_topic + lightList.append( + PolarisLight( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(lightList, update_before_add=True) + + +class PolarisLight(PolarisBaseEntity, LightEntity): + + entity_description: PolarisLightEntityDescription + def __init__( + self, + device_friendly_name: str, + description: PolarisLightEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_color_mode = ColorMode.RGB + self._attr_supported_color_modes = {ColorMode.RGB} + self._attr_has_entity_name = True + self._attr_rgb_color = [255, 255, 255] + self._attr_brightness = 100 + self._attr_is_on = False + self._attr_available = False + self._double_color = [[255, 255, 255],[255, 255, 255]] + self._double_state = ["0","0"] + + async def async_added_to_hass(self): + if self.device_type == "835": + @callback + def message_received_double_state(message): + if message.payload[1] == "0" and self.entity_description.key == "night_up": + self._attr_is_on = False +# self._double_state[0] = "0" + elif message.payload[3] == "0" and self.entity_description.key == "night_down": + self._attr_is_on = False +# self._double_state[1] = "0" + elif message.payload[1] in {"1","2","3"} and self.entity_description.key == "night_up": + self._attr_is_on = True + elif message.payload[3] in {"1","2","3"} and self.entity_description.key == "night_down": + self._attr_is_on = True + else: + self._attr_is_on = None + self._double_state[0] = message.payload[1] + self._double_state[1] = message.payload[3] + rgb_up = color_util.rgb_hex_to_rgb_list(message.payload[4:10]) + rgb_down = color_util.rgb_hex_to_rgb_list(message.payload[10:]) + self._double_color = [rgb_up, rgb_down] + if self.entity_description.key == "night_up": + rgb = rgb_up + else: + rgb = rgb_down + level = int(max(rgb)/255*100) + self._attr_brightness = level + rgb_color = rgb + bright_factor_old = max(rgb)/255 + bright_factor_new = level / 100 / bright_factor_old + self._attr_rgb_color = [int(value / bright_factor_new) for value in rgb_color] + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentState, + message_received_double_state, + 1, + ) + else: + @callback + def message_received_rgb(message): + rgb = color_util.rgb_hex_to_rgb_list(message.payload) + level = int(max(rgb)/255*100) + self._attr_brightness = level + if (self.device_type == "176" or self.device_type == "255"): + rgb_color = [rgb[0], rgb[1], rgb[2]] + else: + rgb_color = rgb + bright_factor_old = max(rgb)/255 + bright_factor_new = level / 100 / bright_factor_old + self._attr_rgb_color = [int(value / bright_factor_new) for value in rgb_color] + self.async_write_ha_state() + @callback + def message_received_state(message): + if str(message.payload).lower() in ("1", "true"): + self._attr_is_on = True + elif str(message.payload).lower() in ("0", "false"): + self._attr_is_on = False + else: + self._attr_is_on = None + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentColor, + message_received_rgb, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentState, + message_received_state, + 1, + ) + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + async def async_turn_on(self, **kwargs: Any) -> None: + topic = self.entity_description.mqttTopicCommandColor + if ATTR_RGB_COLOR in kwargs: + color = color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR]) + self._attr_rgb_color = color_util.rgb_hex_to_rgb_list(color) + elif ATTR_BRIGHTNESS in kwargs: + level = int((kwargs.get(ATTR_BRIGHTNESS, 100) * 100) / 255) + self._attr_brightness = level + bright_factor_old = max(self._attr_rgb_color)/255 + bright_factor_new = self._attr_brightness/ 100 / bright_factor_old + self._attr_rgb_color = [int(value * bright_factor_new) for value in self._attr_rgb_color] + if (self.device_type == "176" or self.device_type == "255"): + mqtt.publish(self.hass, topic, f"{self._attr_rgb_color[0]:02x}{self._attr_rgb_color[1]:02x}{self._attr_rgb_color[2]:02x}00") + elif self.device_type == "835": + if self.entity_description.key == "night_up": + mqtt.publish(self.hass, topic, f"030{self._double_state[1]}{color_util.color_rgb_to_hex(self._attr_rgb_color[0], self._attr_rgb_color[1],self._attr_rgb_color[2])}{self._double_color[1][0]:02x}{self._double_color[1][1]:02x}{self._double_color[1][2]:02x}") + if self.entity_description.key == "night_down": + mqtt.publish(self.hass, topic, f"0{self._double_state[0]}03{self._double_color[0][0]:02x}{self._double_color[0][1]:02x}{self._double_color[0][2]:02x}{color_util.color_rgb_to_hex(self._attr_rgb_color[0], self._attr_rgb_color[1],self._attr_rgb_color[2])}") + else: + mqtt.publish(self.hass, topic, color_util.color_rgb_to_hex(self._attr_rgb_color[0], self._attr_rgb_color[1],self._attr_rgb_color[2])) + if self.device_type != "835": + topic = self.entity_description.mqttTopicCommandState + mqtt.publish(self.hass, topic, "true") + self._attr_is_on = True + + async def async_turn_off(self, **kwargs: Any) -> None: + topic = self.entity_description.mqttTopicCommandState + if self.device_type == "835": + if self.entity_description.key == "night_up": + mess = f"000{self._double_state[1]}{self._double_color[0][0]:02x}{self._double_color[0][1]:02x}{self._double_color[0][2]:02x}{self._double_color[1][0]:02x}{self._double_color[1][1]:02x}{self._double_color[1][2]:02x}" + if self.entity_description.key == "night_down": + mess = f"0{self._double_state[0]}00{self._double_color[0][0]:02x}{self._double_color[0][1]:02x}{self._double_color[0][2]:02x}{self._double_color[1][0]:02x}{self._double_color[1][1]:02x}{self._double_color[1][2]:02x}" + mqtt.publish(self.hass, topic, mess) + else: + mqtt.publish(self.hass, topic, "false") + self._attr_is_on = False + + @property + def is_on(self) -> bool: + return self._attr_is_on + + @property + def brightness(self) -> int: + return int((self._attr_brightness * 255) / 100) + + @property + def rgb_color(self) -> tuple[int, int, int] | None: + return self._attr_rgb_color diff --git a/homeassistant/config/custom_components/polaris/manifest.json b/homeassistant/config/custom_components/polaris/manifest.json new file mode 100644 index 0000000..dd698dd --- /dev/null +++ b/homeassistant/config/custom_components/polaris/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "polaris", + "name": "Polaris IQ Home MQTT", + "codeowners": ["@samoswall"], + "config_flow": true, + "dependencies": ["mqtt"], + "documentation": "https://github.com/samoswall/polaris-mqtt", + "iot_class": "local_push", + "issue_tracker": "https://github.com/samoswall/polaris-mqtt/issues", + "mqtt": [ + "polaris/+/state/mac", + "/polaris/+/state/mac", + "polaris/+/+/state/mac", + "/polaris/+/+/state/mac", + "rusclimate/+/+/state/mac", + "/rusclimate/+/+/state/mac" + ], + "version": "1.0.13" + } diff --git a/homeassistant/config/custom_components/polaris/number.py b/homeassistant/config/custom_components/polaris/number.py new file mode 100644 index 0000000..d64e5ab --- /dev/null +++ b/homeassistant/config/custom_components/polaris/number.py @@ -0,0 +1,291 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.number import NumberEntity, DOMAIN +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +from homeassistant.const import STATE_UNAVAILABLE + +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + NUMBER_HUMIDIFIER, + NUMBER_RUSCLIMATE_HUMIDIFIER, + NUMBER_COOKER, + NUMBERS_COFFEEMAKER, + NUMBERS_COFFEEMAKER_ROG, + NUMBERS_AIRCLEANER, + NUMBERS_IRRIGATOR, + NUMBERS_HEATER, + NUMBERS_THERMOSTAT, + PolarisNumberEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_HUMIDDIFIER_LOW_FAN_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_AIRCLEANER_TYPE, + POLARIS_IRRIGATOR_TYPE, + POLARIS_HEATER_TYPE, + POLARIS_THERMOSTAT_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + numberList = [] + + if (device_type in POLARIS_HUMIDDIFIER_TYPE): + # Create humidifier + if device_type in {"835","881"}: + NUMBER_HUMIDIFIER_LC = copy.deepcopy(NUMBER_RUSCLIMATE_HUMIDIFIER) + else: + NUMBER_HUMIDIFIER_LC = copy.deepcopy(NUMBER_HUMIDIFIER) + for description in NUMBER_HUMIDIFIER_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COOKER_TYPE): + # Create cooker + NUMBER_COOKER_LC = copy.deepcopy(NUMBER_COOKER) + for description in NUMBER_COOKER_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_TYPE): + # Create cooker + NUMBERS_COFFEEMAKER_LC = copy.deepcopy(NUMBERS_COFFEEMAKER) + for description in NUMBERS_COFFEEMAKER_LC: +# description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + # Create cooker + NUMBERS_COFFEEMAKER_ROG_LC = copy.deepcopy(NUMBERS_COFFEEMAKER_ROG) + for description in NUMBERS_COFFEEMAKER_ROG_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_TYPE): + # Create humidifier + NUMBERS_AIRCLEANER_LC = copy.deepcopy(NUMBERS_AIRCLEANER) + for description in NUMBERS_AIRCLEANER_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_IRRIGATOR_TYPE): + # Create irrigator + NUMBERS_IRRIGATOR_LC = copy.deepcopy(NUMBERS_IRRIGATOR) + for description in NUMBERS_IRRIGATOR_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HEATER_TYPE): + # Create Heater + NUMBERS_HEATER_LC = copy.deepcopy(NUMBERS_HEATER) + for description in NUMBERS_HEATER_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_THERMOSTAT_TYPE): + # Create Heater + NUMBERS_THERMOSTAT_LC = copy.deepcopy(NUMBERS_THERMOSTAT) + for description in NUMBERS_THERMOSTAT_LC: + description.mqttTopicCurrent = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrent}" + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.device_prefix_topic = device_prefix_topic + numberList.append( + PolarisNumber( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(numberList, update_before_add=True) + + +class PolarisNumber(PolarisBaseEntity, NumberEntity): + + entity_description: PolarisNumberEntityDescription + def __init__( + self, + device_friendly_name: str, + description: PolarisNumberEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_available = False + self._attr_has_entity_name = True + self._attr_native_value = self.entity_description.native_value + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if self.entity_description.name != "display_time": + self._attr_native_value = STATE_UNAVAILABLE + if self.device_type in POLARIS_HUMIDDIFIER_LOW_FAN_TYPE: + self._attr_native_max_value = 3 + if self.device_type in POLARIS_THERMOSTAT_TYPE: + self._data_zero = "00000000000000000000000000000000000000" + + + def set_native_value(self, value: float) -> None: + if value % 1 > 0: + self._attr_native_value = STATE_UNAVAILABLE + elif ((self.entity_description.key == "display_time") or + (self.entity_description.key == "temperature_difference_antifrost") or + (self.entity_description.key == "temperature_difference_eco")): + self._attr_native_value = int(value) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, f"{int(value):02x}", 0, True) + elif self.entity_description.name == "time_timer": + self._attr_native_value = int(value) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, int(value)*3600) + + elif self.entity_description.translation_key == "power_cable": + self._attr_native_value = int(value) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, f"{int(value):04x}"[-2:]+f"{int(value):04x}"[:2]) + + elif self.entity_description.translation_key == "bright_backlight": + self._attr_native_value = int(value) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, f"{self._data_zero[:2]}{hex(int(value))[2:]}{self._data_zero[4:]}") + + else: + self._attr_native_value = int(value) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand, int(value)) + + @property + def get_state (self) -> int | None: + return self._attr_native_value + + async def async_added_to_hass(self): + @callback + def message_received_numb(message): + if self.entity_description.name == "display_time": + self._attr_native_value = int(message.payload, 16) + elif self.entity_description.name == "time_timer": + self._attr_native_value = int(int(message.payload) / 3600) + elif self.entity_description.translation_key == "bright_backlight": + self._data_zero = message.payload + self._attr_native_value = int(self._data_zero[2:4], 16) + elif self.entity_description.translation_key == "power_cable": + self._attr_native_value = int(message.payload[:2],16) + (int(message.payload[-2:],16)*256) + else: + if self.entity_description.native_min_value <= int(message.payload): + self._attr_native_value = message.payload + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrent, + message_received_numb, + 1, + ) + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + diff --git a/homeassistant/config/custom_components/polaris/select.py b/homeassistant/config/custom_components/polaris/select.py new file mode 100644 index 0000000..ec2982c --- /dev/null +++ b/homeassistant/config/custom_components/polaris/select.py @@ -0,0 +1,482 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy +import datetime +import os + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage + +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + CUSTOM_SELECT_FILE_PATH, + SELECT_KETTLE, + SELECT_COOKER, + SELECT_COFFEEMAKER, + SELECT_COFFEEMAKER_ROG, + SELECT_CLIMATE, + SELECT_VACUUM, + SELECT_IRRIGATOR, + SELECT_AIRCLEANER_EAP, + SELECT_AIRCONDITIONER_SWING_HORIZONTAL, + SELECT_AIRCONDITIONER_SWING_VERTICAL, + PolarisSelectEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_CLIMATE_TYPE, + POLARIS_VACUUM_TYPE, + POLARIS_IRRIGATOR_TYPE, + POLARIS_AIRCLEANER_EAP_TYPE, + POLARIS_AIRCONDITIONER_TYPE, +) + + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + selectList = [] + + if (device_type in POLARIS_KETTLE_TYPE) or (device_type in POLARIS_KETTLE_WITH_WEIGHT_TYPE): + SELECT_KETTLE_LC = copy.deepcopy(SELECT_KETTLE) + for description in SELECT_KETTLE_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COOKER_TYPE): + SELECT_COOKER_LC = copy.deepcopy(SELECT_COOKER) + for description in SELECT_COOKER_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_TYPE): + SELECT_COFFEEMAKER_LC = copy.deepcopy(SELECT_COFFEEMAKER) + for description in SELECT_COFFEEMAKER_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + SELECT_COFFEEMAKER_ROG_LC = copy.deepcopy(SELECT_COFFEEMAKER_ROG) + for description in SELECT_COFFEEMAKER_ROG_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_CLIMATE_TYPE): + SELECT_CLIMATE_LC = copy.deepcopy(SELECT_CLIMATE) + for description in SELECT_CLIMATE_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_VACUUM_TYPE): + SELECT_VACUUM_LC = copy.deepcopy(SELECT_VACUUM) + for description in SELECT_VACUUM_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_IRRIGATOR_TYPE): + SELECT_IRRIGATOR_LC = copy.deepcopy(SELECT_IRRIGATOR) + for description in SELECT_IRRIGATOR_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_EAP_TYPE): + SELECT_AIRCLEANER_EAP_LC = copy.deepcopy(SELECT_AIRCLEANER_EAP) + for description in SELECT_AIRCLEANER_EAP_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCONDITIONER_TYPE) and (device_type == "813"): + SELECT_AIRCONDITIONER_SWING_HORIZONTAL_LC = copy.deepcopy(SELECT_AIRCONDITIONER_SWING_HORIZONTAL) + for description in SELECT_AIRCONDITIONER_SWING_HORIZONTAL_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCONDITIONER_TYPE) and (device_type == "813"): + SELECT_AIRCONDITIONER_SWING_VERTICAL_LC = copy.deepcopy(SELECT_AIRCONDITIONER_SWING_VERTICAL) + for description in SELECT_AIRCONDITIONER_SWING_VERTICAL_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.device_prefix_topic = device_prefix_topic + selectList.append( + PolarisSelect( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(selectList, update_before_add=True) + + +class PolarisSelect(PolarisBaseEntity, SelectEntity): + + entity_description: PolarisSelectEntityDescription + def __init__( + self, + device_friendly_name: str, + description: PolarisSelectEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_has_entity_name = True + + + if POLARIS_DEVICE[int(self.device_type)]['class'] == "kettle" or POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker" or POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker" or POLARIS_DEVICE[int(self.device_type)]['class'] == "cleaner": + self._select_options = json.loads(json.dumps(SELECT_COFFEEMAKER[0].options)) + if device_type == "826": + self._EAP_data0 = "0000" + if device_type == "813": + self._conditioner_data0 = "0000" + + self._custom_data_select = self._read_file() + if self._custom_data_select is not None: + if POLARIS_DEVICE[int(self.device_type)]['class'] == "kettle" and "SELECT_KETTLE_options" in self._custom_data_select: +# self.entity_description.options = json.loads(json.dumps(self.entity_description.options)) + for key, value in self._custom_data_select["SELECT_KETTLE_options"].items(): + self.entity_description.options[key] = value +# _LOGGER.debug("kettle %s", self.entity_description.options) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker" and "SELECT_COOKER_options" in self._custom_data_select: +# self.entity_description.options = json.loads(json.dumps(self.entity_description.options)) + for key, value in self._custom_data_select["SELECT_COOKER_options"].items(): + self.entity_description.options[key] = json.dumps([value]) +# _LOGGER.debug("cooker %s", self.entity_description.options) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if int(self.device_type) == 45 and "SELECT_COFFEEMAKER_ROG_options" in self._custom_data_select: +# self.entity_description.options = json.loads(json.dumps(self.entity_description.options)) + for key, value in self._custom_data_select["SELECT_COFFEEMAKER_ROG_options"].items(): + self.entity_description.options[key] = json.dumps([value]) +# _LOGGER.debug("coffee_rog %s", self.entity_description.options) + elif "SELECT_COFFEEMAKER_options" in self._custom_data_select: +# self.entity_description.options = json.loads(json.dumps(self.entity_description.options)) + for key, value in self._custom_data_select["SELECT_COFFEEMAKER_options"].items(): + self.entity_description.options[key] = json.dumps([value]) +# _LOGGER.debug("coffee %s", self.entity_description.options) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cleaner" and "SELECT_VACUUM_rooms" in self._custom_data_select and self.entity_description.key == "select_room": +# self.entity_description.options = json.loads(json.dumps(self.entity_description.options)) + for key, value in self._custom_data_select["SELECT_VACUUM_rooms"].items(): + self.entity_description.options[key] = json.dumps([value]) + + self._attr_options = list(self.entity_description.options.keys()) + self._attr_current_option = self._attr_options[0] + self._attr_available = False + + def _read_file(self): + file_path = CUSTOM_SELECT_FILE_PATH + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + content = json.loads(file.read()) + else: + content = None + return content + +# @property +# def available(self): +# return self._attr_current_option is not None + + def key_from_option(self, option: str): + try: + return next( + key + for key, value in self.entity_description.options.items() + if json.loads(value)[0]["mode"] == option + ) + except StopIteration: + return None + + async def async_select_option(self, option: str) -> None: + self._attr_current_option = option + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker": + cook_time = json.loads(self.entity_description.options[option]) + service_data = {} + service_data["entity_id"] = f"time.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_cooking_time" + service_data["time"] = str(datetime.timedelta(seconds=cook_time[0]["time"])) + await self.hass.services.async_call("time", "set_value", service_data) + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_set_temperature" + service_data["value"] = cook_time[0]["temperature"] + await self.hass.services.async_call("number", "set_value", service_data) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "kettle": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandTemperature, self.entity_description.options[option]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, 3) + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if int(self.device_type) == 45: + coffee_mode = json.loads(self.entity_description.options[option]) + + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_tank" + if coffee_mode[0]["tank"] != 0: + service_data["value"] = str(coffee_mode[0]["tank"]) + else: + service_data["value"] = 7.777 + await self.hass.services.async_call("number", "set_value", service_data) + + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_amount" + if coffee_mode[0]["amount"] != 0: + service_data["value"] = str(coffee_mode[0]["amount"]) + else: + service_data["value"] = 100.777 + await self.hass.services.async_call("number", "set_value", service_data) + + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_temperature" + if coffee_mode[0]["temperature"] != 0: + service_data["value"] = str(coffee_mode[0]["temperature"]) + else: + service_data["value"] = 100.777 + await self.hass.services.async_call("number", "set_value", service_data) + + else: + coffee_mode = json.loads(self.entity_description.options[option]) + for key, val in coffee_mode[0].items(): + if key == "mode": + mode = val + elif val != 0: + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_{key}" + service_data["value"] = str(val) + await self.hass.services.async_call("number", "set_value", service_data) + else: + if key == "weight": + val = "7.777" + else: + val = "55.777" + service_data = {} + service_data["entity_id"] = f"number.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_{key}" + service_data["value"] = val + await self.hass.services.async_call("number", "set_value", service_data) + if self.device_type in POLARIS_CLIMATE_TYPE: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, self.entity_description.options[option]) + if self.device_type in POLARIS_AIRCONDITIONER_TYPE: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, self.entity_description.options[option]) + if (int(self.entity_description.options[option]) > 0): + if self.entity_description.key == "select_swing_horizontal": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode[:-1] + "0", self._conditioner_data0[:2] + "00" + self._conditioner_data0[-4:]) + if self.entity_description.key == "select_swing_vertical": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode[:-1] + "0", "00" + self._conditioner_data0[-6:]) + else: + if self.entity_description.key == "select_swing_horizontal": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode[:-1] + "0", self._conditioner_data0[:2] + "01" + self._conditioner_data0[-4:]) + if self.entity_description.key == "select_swing_vertical": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode[:-1] + "0", "01" + self._conditioner_data0[-6:]) + + if POLARIS_DEVICE[int(self.device_type)]['class'] == "irrigator": + if self._attr_current_option == "preset3": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_3[0] == "a" else self._preset_3[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_3[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_3[2]) + elif self._attr_current_option == "preset2": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_2[0] == "a" else self._preset_2[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_2[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_2[2]) + else: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_1[0] == "a" else self._preset_1[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_1[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_1[2]) + if (self.device_type == "826" and self._EAP_data0[:2] == "02"): + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, self._EAP_data0[:3] + str(self.entity_description.options[option])) + + async def async_added_to_hass(self): + @callback + def message_received_sel(message): + payload = message.payload + if payload in ("0", "[]"): + self._attr_current_option = self._attr_options[0] + self.async_write_ha_state() + elif POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker": + sel_mode = json.loads(payload)[0]["mode"] + # if int(sel_mode)>0: + # self.switch.set_available(True) + # else: + # self.switch.set_available(False) + sel_opt = self.key_from_option(sel_mode) + self._attr_current_option = sel_opt + self.async_write_ha_state() + elif int(self.device_type) == 45: + sel_opt = self.key_from_option(int(payload)) + self._attr_current_option = sel_opt + self.async_write_ha_state() + elif int(self.device_type) == 69: + self._attr_current_option = self._attr_options[int(payload)] + self.async_write_ha_state() + elif int(self.device_type) == 813: + self._attr_current_option = self._attr_options[int(payload)] + self.async_write_ha_state() + elif POLARIS_DEVICE[int(self.device_type)]['class'] == "irrigator": + self._preset_1 = [payload[1:2], payload[7:8], payload[13:14]] + self._preset_2 = [payload[3:4], payload[9:10], payload[15:16]] + self._preset_3 = [payload[5:6], payload[11:12], payload[17:]] + if self._attr_current_option == "preset3": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_3[0] == "a" else self._preset_3[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_3[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_3[2]) + elif self._attr_current_option == "preset2": + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_2[0] == "a" else self._preset_2[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_2[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_2[2]) + else: + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"speed", "10" if self._preset_1[0] == "a" else self._preset_1[0]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"ioniser", self._preset_1[1]) + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode+"smart_mode", self._preset_1[2]) + await mqtt.async_subscribe(self.hass, self.entity_description.mqttTopicCurrentMode, message_received_sel, 1) + + @callback + def EAP_data_message_received(message): + self._EAP_data0 = message.payload +# _LOGGER.debug("EAP data0 message select %s", self._EAP_data0) + + + if self.device_type == "826": + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentMode, + EAP_data_message_received, + 1, + ) + + @callback + def conditioner_data_message_received(message): + self._conditioner_data0 = message.payload + + if self.device_type == "813": + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentMode[:-1]+"0", + conditioner_data_message_received, + 1, + ) + + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + diff --git a/homeassistant/config/custom_components/polaris/sensor.py b/homeassistant/config/custom_components/polaris/sensor.py new file mode 100644 index 0000000..1f536ce --- /dev/null +++ b/homeassistant/config/custom_components/polaris/sensor.py @@ -0,0 +1,529 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import copy +import json +import logging +import struct + +from homeassistant.components import mqtt +from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .common import PolarisBaseEntity + +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + SENSORS_ALL_DEVICES, + SENSORS_WEIGHT, + SENSORS_HUMIDIFIER, + SENSORS_RUSCLIMATE_HUMIDIFIER, + SENSORS_COOKER, + SENSORS_COFFEEMAKER, + SENSORS_COFFEEMAKER_ROG, + SENSORS_CLIMATE, + SENSORS_CLIMATE_200, + SENSORS_AIRCLEANER, + SENSORS_AIRCLEANER_EAP, + SENSORS_VACUUM, + SENSORS_WATER_BOILER, + SENSORS_IRRIGATOR, + SENSORS_HEATER, + SENSORS_AIRCONDITIONER, + SENSORS_THERMOSTAT, + PolarisSensorEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_CLIMATE_TYPE, + POLARIS_AIRCLEANER_TYPE, + POLARIS_AIRCLEANER_EAP_TYPE, + POLARIS_VACUUM_TYPE, + POLARIS_BOILER_TYPE, + POLARIS_IRRIGATOR_TYPE, + POLARIS_HEATER_TYPE, + POLARIS_AIRCONDITIONER_TYPE, + POLARIS_THERMOSTAT_TYPE, + KETTLE_ERROR, + HUMIDDIFIER_ERROR, + COOKER_ERROR, + COFFEEMAKER_ERROR, + AIRCLEANER_ERROR, + VACUUM_ERROR, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + integrationUniqueID = config.unique_id + mqttRoot = config.data[MQTT_ROOT_TOPIC] + deviceID = config.data["DEVICEID"] + devicetype = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + if len(device_prefix_topic)>15: + mqtt.publish(hass, f"{mqttRoot}/{device_prefix_topic}/state/devtype", devicetype, 0, True) + sensorList = [] + #Kettle + if (devicetype in POLARIS_KETTLE_TYPE): + # Create sensors for all devices + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + #Kettle with weight + if (devicetype in POLARIS_KETTLE_WITH_WEIGHT_TYPE): + # Create sensors for all devices + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_WEIGHT_CP = copy.deepcopy(SENSORS_WEIGHT) + for description in SENSORS_WEIGHT_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + # Humidifier + if (devicetype in POLARIS_HUMIDDIFIER_TYPE): + if devicetype == "881": + SENSORS_RUSCLIMATE_HUMIDIFIER_CP = copy.deepcopy(SENSORS_RUSCLIMATE_HUMIDIFIER) + for description in SENSORS_RUSCLIMATE_HUMIDIFIER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + else: + # Create sensors for all devices + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_HUMIDIFIER_CP = copy.deepcopy(SENSORS_HUMIDIFIER) + for description in SENSORS_HUMIDIFIER_CP: + if (devicetype != "835" or description.translation_key != "clean_retain"): + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + # Cooker + if (devicetype in POLARIS_COOKER_TYPE): + # Create sensors for all devices + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_COOKER_CP = copy.deepcopy(SENSORS_COOKER) + for description in SENSORS_COOKER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + # Coffeemaker + if (devicetype in POLARIS_COFFEEMAKER_TYPE): + # Create sensors for all devices + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_COFFEEMAKER_CP = copy.deepcopy(SENSORS_COFFEEMAKER) + for description in SENSORS_COFFEEMAKER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_COFFEEMAKER_ROG_TYPE): + # Create sensors for coffeemaker + SENSORS_COFFEEMAKER_ROG_CP = copy.deepcopy(SENSORS_COFFEEMAKER_ROG) + for description in SENSORS_COFFEEMAKER_ROG_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_CLIMATE_TYPE): + # Create sensors for climate asp-200 or asp-100 + if (devicetype == "859"): + SENSORS_CLIMATE_CP = copy.deepcopy(SENSORS_CLIMATE_200) + else: + SENSORS_CLIMATE_CP = copy.deepcopy(SENSORS_CLIMATE) + for description in SENSORS_CLIMATE_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_AIRCLEANER_TYPE): + SENSORS_AIRCLEANER_CP = copy.deepcopy(SENSORS_AIRCLEANER) + for description in SENSORS_AIRCLEANER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_AIRCLEANER_EAP_TYPE): + SENSORS_AIRCLEANER_EAP_CP = copy.deepcopy(SENSORS_AIRCLEANER_EAP) + for description in SENSORS_AIRCLEANER_EAP_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_VACUUM_TYPE): + SENSORS_VACUUM_CP = copy.deepcopy(SENSORS_VACUUM) + for description in SENSORS_VACUUM_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_BOILER_TYPE): + SENSORS_WATER_BOILER_CP = copy.deepcopy(SENSORS_WATER_BOILER) + for description in SENSORS_WATER_BOILER_CP: + if (devicetype not in {"833","802"} or description.translation_key != "anode_retain"): + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_IRRIGATOR_TYPE): + SENSORS_IRRIGATOR_CP = copy.deepcopy(SENSORS_IRRIGATOR) + for description in SENSORS_IRRIGATOR_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_HEATER_TYPE): + SENSORS_HEATER_CP = copy.deepcopy(SENSORS_HEATER) + for description in SENSORS_HEATER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_AIRCONDITIONER_TYPE): + SENSORS_AIRCONDITIONER_CP = copy.deepcopy(SENSORS_AIRCONDITIONER) + for description in SENSORS_AIRCONDITIONER_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + if (devicetype in POLARIS_THERMOSTAT_TYPE): + SENSORS_ALL_DEVICES_CP = copy.deepcopy(SENSORS_ALL_DEVICES) + for description in SENSORS_ALL_DEVICES_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + SENSORS_THERMOSTAT_CP = copy.deepcopy(SENSORS_THERMOSTAT) + for description in SENSORS_THERMOSTAT_CP: + description.mqttTopicCurrentValue = (f"{mqttRoot}/{device_prefix_topic}/state/{description.key}") + description.device_prefix_topic = device_prefix_topic + sensorList.append( + PolarisSensor( + description=description, + device_friendly_name=deviceID, + mqtt_root=mqttRoot, + device_type=devicetype, + device_id=deviceID, + ) + ) + async_add_entities(sensorList) + + +class PolarisSensor(PolarisBaseEntity, SensorEntity): + + entity_description: PolarisSensorEntityDescription + + def __init__( + self, +# uniqueID: str | None, + device_friendly_name: str, + mqtt_root: str, + description: PolarisSensorEntityDescription, + device_type: str, + device_id: str, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_has_entity_name = True + self._attr_available = False + + + def bytes_to_int16_array(self, byte_data, byteorder='little'): + """ + Преобразует байтовую строку в массив int16. + Аргументы: + byte_data: Байтовая строка для преобразования (bytes). + byteorder: Порядок байтов ('little' или 'big'), по умолчанию 'little'. + Возвращает: + Массив int16 значений (list of int). + Вызывает исключение ValueError: + Если длина byte_data нечетная. + Если byteorder не является 'little' или 'big'. + """ + if len(byte_data) % 2 != 0: + raise ValueError("Длина байтовой строки должна быть четной для int16 преобразования.") + if byteorder not in ('little', 'big'): + raise ValueError("Неверный порядок байтов. Допустимые значения: 'little', 'big'.") + endian_prefix = '<' if byteorder == 'little' else '>' # '<' - little-endian, '>' - big-endian + format_string = endian_prefix + 'h' * (len(byte_data) // 2) # 'h' - signed short (2 bytes - int16) + return list(struct.unpack(format_string, byte_data)) + + + + + + + + + async def async_added_to_hass(self): + @callback + def message_received(message): + payload_message = message.payload + if self.entity_description.name == "error": + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cooker": + payload_message = COOKER_ERROR[payload_message] + if POLARIS_DEVICE[int(self.device_type)]['class'] == "kettle": + payload_message = KETTLE_ERROR[payload_message] + if POLARIS_DEVICE[int(self.device_type)]['class'] == "humidifier": + payload_message = HUMIDDIFIER_ERROR[payload_message] + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + payload_message = COFFEEMAKER_ERROR[payload_message] + if POLARIS_DEVICE[int(self.device_type)]['class'] == "air_cleaner": + payload_message = AIRCLEANER_ERROR[payload_message] + if POLARIS_DEVICE[int(self.device_type)]['class'] == "cleaner": + payload_message = VACUUM_ERROR[payload_message] + if self.entity_description.name == "filter_retain": + payload_message = payload_message.replace("[","",1).replace("]","",1).split(",")[0] + if self.entity_description.name == "pre_filter_retain": + payload_message = payload_message.replace("[","",1).replace("]","",1).split(",")[1] + if self.entity_description.name == "anode_retain": + payload_message = payload_message.replace("[","",1).replace("]","",1).split(",")[0] + if self.entity_description.name == "clean_retain": + payload_message = payload_message.replace("[","",1).replace("]","",1).split(",")[1] + if self.entity_description.name == "mode": + payload_message = self.entity_description.valueMap[payload_message] + if self.entity_description.name == "power_state": + payload_message = self.entity_description.valueMap[payload_message] + if self.entity_description.name == "go_area": +# _LOGGER.debug("go_area %s", payload_message) + list_dubleint = self.bytes_to_int16_array(payload_message) +# _LOGGER.debug("bytes_to_int16 %s",list_dubleint) +# list_dubleint = self.bytes_to_int16_array(payload_message, byteorder='big') +# _LOGGER.debug("list_integers %s",list_dubleint) + payload_message = list_dubleint + if self.entity_description.name == "quality": + payload_message = str( int(payload_message) / 100 ) + if self.entity_description.name == "сurrent_power": + if self.device_type == "806": + payload_message = str( int(payload_message[:2],16) * 20 ) + else: + payload_message = str( int(payload_message[:2],16) * 10 ) + + self._attr_native_value = payload_message + self.async_write_ha_state() + + if self.entity_description.name == "go_area": + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue, + message_received, + 1, + None, + ) + else: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue, + message_received, + 1, + ) + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) diff --git a/homeassistant/config/custom_components/polaris/services.yaml b/homeassistant/config/custom_components/polaris/services.yaml new file mode 100644 index 0000000..d3eb298 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/services.yaml @@ -0,0 +1,15 @@ +select_rooms: + target: + entity: + integration: "polaris" + domain: "vacuum" + name: Select rooms + description: Select rooms to clean + fields: + select_rooms: + name: Rooms + description: Input of available rooms to clean + required: true + example: Kitchen + selector: + text: diff --git a/homeassistant/config/custom_components/polaris/switch.py b/homeassistant/config/custom_components/polaris/switch.py new file mode 100644 index 0000000..45bc2c5 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/switch.py @@ -0,0 +1,752 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.switch import DOMAIN, SwitchEntity +#from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg + +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + SWITCHES_ALL_DEVICES, + SWITCH_KETTLE_BACKLIGHT, + SWITCH_HUMIDIFIER_BACKLIGHT, + SWITCH_HUMIDIFIER_IONISER, + SWITCH_HUMIDIFIER_WARM_STREAM, + SWITCH_HUMIDIFIER_ULTRAVIOLET, + SWITCHES_RUSCLIMATE_HUMIDIFIER, + SWITCHES_COOKER, + SWITCHES_COFFEEMAKER, + SWITCHES_COFFEEMAKER_ROG, + SWITCHES_CLIMATE, + SWITCHES_CLIMATE_200, + SWITCHES_AIRCLEANER, + SWITCHES_AIRCLEANER_EAP, + SWITCHES_VACUUM, + SWITCHES_WATER_BOILER, + SWITCHES_WATER_BOILER_NO_FROST, + SWITCHES_WATER_BOILER_BACKLIGHT, + SWITCHES_IRRIGATOR, + SWITCHES_HEATER, + SWITCHES_AIRCONDITIONER, + SWITCHES_AIRCONDITIONER_820, + SWITCHES_AIRCONDITIONER_882, + SWITCHES_THERMOSTAT, + PolarisSwitchEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_KETTLE_WITH_BACKLIGHT_TYPE, + POLARIS_HUMIDDIFIER_TYPE, + POLARIS_HUMIDDIFIER_WITH_IONISER_TYPE, + POLARIS_HUMIDDIFIER_WITH_WARM_STREAM_TYPE, + POLARIS_COOKER_TYPE, + POLARIS_COFFEEMAKER_TYPE, + POLARIS_COFFEEMAKER_ROG_TYPE, + POLARIS_CLIMATE_TYPE, + POLARIS_AIRCLEANER_TYPE, + POLARIS_AIRCLEANER_EAP_TYPE, + POLARIS_VACUUM_TYPE, + POLARIS_BOILER_TYPE, + POLARIS_IRRIGATOR_TYPE, + POLARIS_HEATER_TYPE, + POLARIS_AIRCONDITIONER_TYPE, + POLARIS_THERMOSTAT_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + switchList = [] + + if (device_type in POLARIS_KETTLE_TYPE) or (device_type in POLARIS_KETTLE_WITH_WEIGHT_TYPE): + # Create sensors for all devices + SWITCHES_ALL_DEVICES_LC = copy.deepcopy(SWITCHES_ALL_DEVICES) + for description in SWITCHES_ALL_DEVICES_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if device_type in POLARIS_KETTLE_WITH_BACKLIGHT_TYPE: + # Create sensors for backlight devices + SWITCH_KETTLE_BACKLIGHT_LC = copy.deepcopy(SWITCH_KETTLE_BACKLIGHT) + for description in SWITCH_KETTLE_BACKLIGHT_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HUMIDDIFIER_TYPE): + if device_type == "881": + SWITCHES_RUSCLIMATE_HUMIDIFIER_LC = copy.deepcopy(SWITCHES_RUSCLIMATE_HUMIDIFIER) + for description in SWITCHES_RUSCLIMATE_HUMIDIFIER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + else: + # Create switches for all devices + SWITCHES_ALL_DEVICES_LC = copy.deepcopy(SWITCHES_ALL_DEVICES) + for description in SWITCHES_ALL_DEVICES_LC: + if (device_type != "835" or description.translation_key != "child_lock_switch"): + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + SWITCH_HUMIDIFIER_BACKLIGHT_LC = copy.deepcopy(SWITCH_HUMIDIFIER_BACKLIGHT) + for description in SWITCH_HUMIDIFIER_BACKLIGHT_LC: + if (device_type != "835" or description.translation_key != "backlight_switch"): + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HUMIDDIFIER_WITH_IONISER_TYPE): + # Create switch ioniser for humidifiers + SWITCH_HUMIDIFIER_IONISER_LC = copy.deepcopy(SWITCH_HUMIDIFIER_IONISER) + for description in SWITCH_HUMIDIFIER_IONISER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HUMIDDIFIER_WITH_WARM_STREAM_TYPE): + # Create switch stream warm for humidifiers + SWITCH_HUMIDIFIER_WARM_STREAM_LC = copy.deepcopy(SWITCH_HUMIDIFIER_WARM_STREAM) + for description in SWITCH_HUMIDIFIER_WARM_STREAM_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in {"835","157"}): + # Create switch stream warm for humidifiers + SWITCH_HUMIDIFIER_ULTRAVIOLET_LC = copy.deepcopy(SWITCH_HUMIDIFIER_ULTRAVIOLET) + for description in SWITCH_HUMIDIFIER_ULTRAVIOLET_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COOKER_TYPE): + # Create switches for cooker + SWITCHES_COOKER_LC = copy.deepcopy(SWITCHES_COOKER) + for description in SWITCHES_COOKER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_TYPE): + # Create switches for coffeemaker + SWITCHES_COFFEEMAKER_LC = copy.deepcopy(SWITCHES_COFFEEMAKER) + for description in SWITCHES_COFFEEMAKER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_COFFEEMAKER_ROG_TYPE): + # Create switches for coffeemaker + SWITCHES_COFFEEMAKER_ROG_LC = copy.deepcopy(SWITCHES_COFFEEMAKER_ROG) + for description in SWITCHES_COFFEEMAKER_ROG_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_CLIMATE_TYPE): + # Create switches for climate + SWITCHES_CLIMATE_LC = copy.deepcopy(SWITCHES_CLIMATE) + for description in SWITCHES_CLIMATE_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "859"): + SWITCHES_CLIMATE_200_LC = copy.deepcopy(SWITCHES_CLIMATE_200) + for description in SWITCHES_CLIMATE_200_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_TYPE): + # Create switches for all devices + SWITCHES_ALL_DEVICES_LC = copy.deepcopy(SWITCHES_ALL_DEVICES) + for description in SWITCHES_ALL_DEVICES_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + SWITCHES_AIRCLEANER_LC = copy.deepcopy(SWITCHES_AIRCLEANER) + for description in SWITCHES_AIRCLEANER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_AIRCLEANER_EAP_TYPE): + SWITCHES_AIRCLEANER_EAP_LC = copy.deepcopy(SWITCHES_AIRCLEANER_EAP) + for description in SWITCHES_AIRCLEANER_EAP_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_VACUUM_TYPE): + # Create switches for vacuum + SWITCHES_VACUUM_LC = copy.deepcopy(SWITCHES_VACUUM) + for description in SWITCHES_VACUUM_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_BOILER_TYPE): + # Create switches for boiler + SWITCHES_WATER_BOILER_LC = copy.deepcopy(SWITCHES_WATER_BOILER) + for description in SWITCHES_WATER_BOILER_LC: + if (device_type not in {"802","833","844"} or description.translation_key != "child_lock_switch") and (device_type != "833" or description.translation_key != "smart_mode"): + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "844"): + # Create switches for boiler bright 50% + SWITCHES_WATER_BOILER_BACKLIGHT_LC = copy.deepcopy(SWITCHES_WATER_BOILER_BACKLIGHT) + for description in SWITCHES_WATER_BOILER_BACKLIGHT_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "802"): + # Create switches for boiler no frost + SWITCHES_WATER_BOILER_NO_FROST_LC = copy.deepcopy(SWITCHES_WATER_BOILER_NO_FROST) + for description in SWITCHES_WATER_BOILER_NO_FROST_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_IRRIGATOR_TYPE): + # Create switches for irrigator + SWITCHES_IRRIGATOR_LC = copy.deepcopy(SWITCHES_IRRIGATOR) + for description in SWITCHES_IRRIGATOR_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_HEATER_TYPE): + # Create switches for heater + SWITCHES_HEATER_LC = copy.deepcopy(SWITCHES_HEATER) + for description in SWITCHES_HEATER_LC: + if (device_type not in {"806","847"} or description.translation_key != "half_power_heater"): + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "820"): + SWITCHES_AIRCONDITIONER_820_LC = copy.deepcopy(SWITCHES_AIRCONDITIONER_820) + for description in SWITCHES_AIRCONDITIONER_820_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "813"): + SWITCHES_AIRCONDITIONER_LC = copy.deepcopy(SWITCHES_AIRCONDITIONER) + for description in SWITCHES_AIRCONDITIONER_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type == "882"): + SWITCHES_AIRCONDITIONER_882_LC = copy.deepcopy(SWITCHES_AIRCONDITIONER_882) + for description in SWITCHES_AIRCONDITIONER_882_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_THERMOSTAT_TYPE): + SWITCHES_THERMOSTAT_LC = copy.deepcopy(SWITCHES_THERMOSTAT) + for description in SWITCHES_THERMOSTAT_LC: + description.mqttTopicCommand = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommand}" + description.mqttTopicCurrentValue = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentValue}" + description.device_prefix_topic = device_prefix_topic + switchList.append( + PolarisSwitch( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(switchList, update_before_add=True) + +class PolarisSwitch(PolarisBaseEntity, SwitchEntity): + entity_description: PolarisSwitchEntityDescription + def __init__( + self, + device_friendly_name: str, + description: PolarisSwitchEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self.payload_on=description.payload_on + self.payload_off=description.payload_off + self._attr_has_entity_name = True +# self._old_mode = "0" + self._attr_available = False +# self._attr_assumed_state = False +# self._optimistic = False +# self._value_template = self.entity_description.mqttTopicCurrentValue + if device_type == "806": + self._heater_prog_data0 = "0000" + else: + self._heater_prog_data0 = "000000" + if device_type == "826": + self._EAP_data0 = "0000" + if device_type in POLARIS_AIRCONDITIONER_TYPE: + if device_type == "882": + self._swing_message = "000000000000" + else: + self._swing_message = "00000000" + self._aircond_data0 = "0000" + if self.entity_description.key == "turbo": + self._attr_available = False + if self.entity_description.key == "self_cleaning": + self._attr_available = False + if self.entity_description.key == "eco_mode_switch": + self._attr_available = False + if self.entity_description.key == "anti_fingus": + self._attr_available = False + if self.entity_description.key == "night": + self._attr_available = False + + async def async_added_to_hass(self): + @callback + def message_received(message): + if POLARIS_DEVICE[int(self.device_type)]['class'] == "coffeemaker": + if int(self.device_type) in POLARIS_COFFEEMAKER_ROG_TYPE: + if str(message.payload) in ("1", "2", "3", "4", "5", "6"): + self._attr_is_on = True + else: + self._attr_is_on = False + elif str(message.payload) in ("01", "1", "03", "3"): + self._attr_is_on = True + else: + self._attr_is_on = False + elif self.entity_description.key == "display_off_heater": + self._heater_prog_data0 = str(message.payload) + self._attr_is_on = True if (self._heater_prog_data0[2:4] == "01") else False + elif self.entity_description.key == "half_power_heater": + self._heater_prog_data0 = str(message.payload) + self._attr_is_on = True if (self._heater_prog_data0[-2:] == "01") else False + + elif self.entity_description.key == "quiet_mode": + self._aircond_data0 = str(message.payload) + self._attr_is_on = True if (self._aircond_data0[-2:] == "01") else False + elif self.entity_description.key == "self_cleaning": + self._aircond_data0 = str(message.payload) + self._attr_is_on = True if (self._aircond_data0[:2] == "01") else False + else: + if str(message.payload).lower() in ("1", "01", "2", "3", "4", "5", "true"): + self._attr_is_on = True + elif str(message.payload).lower() in ("0", "00", "false"): + self._attr_is_on = False + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue, + message_received, + 1, + ) + + @callback + def mode_message_received(message): + if self.entity_description.translation_key == "keepwarm_switch": + if str(message.payload) in ("[]", '[{"mode":1,"time":0,"temperature":0}]'): + self._attr_available = False +# self._attr_is_on = False + else: + self._attr_available = True + self._attr_is_on = True + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue.replace("keepwarm", "steps"), + mode_message_received, + 1, + ) + + @callback + def EAP_data_message_received(message): + self._EAP_data0 = message.payload + if self.device_type == "826": + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue.replace("backlight", "program_data/0"), + EAP_data_message_received, + 1, + ) + + @callback + def mode_conditioner_data_message_received(message): + if self.entity_description.key in ("auto_heater_switch", "eco_mode_switch", "turbo", "night", "self_cleaning", "anti_fingus"): + if (message.payload == "5"): #FAN + if self.entity_description.key == "night": + self._attr_available = False + elif self.entity_description.key == "night": + self._attr_available = True + if (message.payload == "4"): #HEAT + if self.entity_description.key in ("auto_heater_switch", "turbo"): + self._attr_available = True + elif self.entity_description.key == "auto_heater_switch": + self._attr_available = False + if (message.payload == "2"): #COOL + if self.entity_description.key in ("eco_mode_switch", "turbo"): + self._attr_available = True + elif self.entity_description.key == "eco_mode_switch": + self._attr_available = False + if (message.payload == "0"): #OFF + if self.entity_description.key in ("self_cleaning", "anti_fingus"): + self._attr_available = True + elif self.entity_description.key in ("self_cleaning", "anti_fingus"): + self._attr_available = False + if message.payload in ("0", "1", "3", "5"): + if self.entity_description.key == "turbo": + self._attr_available = False + self.async_write_ha_state() + if self.device_type in POLARIS_AIRCONDITIONER_TYPE: + await mqtt.async_subscribe( + self.hass, + f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/mode", + mode_conditioner_data_message_received, + 1, + ) + + @callback + def swing_data_message_received(message): + self._swing_message = message.payload + if self.entity_description.key == "eco_mode_switch": + if message.payload[4:6] == "01": + self._attr_is_on = True + else: + self._attr_is_on = False + self.async_write_ha_state() + if self.entity_description.key == "auto_heater_switch": + if message.payload[-2:] == "01": + self._attr_is_on = True + else: + self._attr_is_on = False + self.async_write_ha_state() + if self.entity_description.key == "anti_fingus": + if message.payload[-2:] == "01": + self._attr_is_on = True + else: + self._attr_is_on = False + self.async_write_ha_state() + if self.device_type in POLARIS_AIRCONDITIONER_TYPE: + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentValue, + swing_data_message_received, + 1, + ) + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + if self.entity_description.key != "keepwarm": + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + def turn_on(self, **kwargs): +# self._attr_is_on = True # optimistic mode + topic = f"{self.entity_description.mqttTopicCommand}" + if self.entity_description.key == "display_off_heater": + if self.device_type == "806": + send_message = self._heater_prog_data0[:2] + self.payload_on + else: + send_message = self._heater_prog_data0[:2] + self.payload_on + self._heater_prog_data0[-2:] + elif self.entity_description.key == "eco_mode_switch": + send_message = self._swing_message[:4] + self.payload_on + self._swing_message[-2:] + elif self.entity_description.key == "auto_heater_switch": + send_message = self._swing_message[:6] + self.payload_on + elif self.entity_description.key == "anti_fingus": + send_message = self._swing_message[:6] + self.payload_on + elif self.entity_description.key == "half_power_heater": + send_message = self._heater_prog_data0[:4] + self.payload_on + elif self.entity_description.key == "quiet_mode": + send_message = self._aircond_data0[:2] + self.payload_on + elif self.entity_description.key == "self_cleaning": + send_message = self.payload_on + self._aircond_data0[-2:] + elif (self.device_type == "826" and self.entity_description.key == "backlight"): + self._EAP_data0 = self._EAP_data0[:2] + "01" + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand.replace("backlight", "program_data/0"), self._EAP_data0) + send_message = self.payload_on + else: + send_message = self.payload_on + mqtt.publish(self.hass, topic, send_message) + + + def turn_off(self, **kwargs): +# self._attr_is_on = False # optimistic mode + topic = f"{self.entity_description.mqttTopicCommand}" + if self.entity_description.key == "display_off_heater": + if self.device_type == "806": + send_message = self._heater_prog_data0[:2] + self.payload_off + else: + send_message = self._heater_prog_data0[:2] + self.payload_off + self._heater_prog_data0[-2:] + elif self.entity_description.key == "eco_mode_switch": + send_message = self._swing_message[:4] + self.payload_off + self._swing_message[-2:] + elif self.entity_description.key == "auto_heater_switch": + send_message = self._swing_message[:6] + self.payload_off + elif self.entity_description.key == "anti_fingus": + send_message = self._swing_message[:6] + self.payload_off + elif self.entity_description.key == "half_power_heater": + send_message = self._heater_prog_data0[:4] + self.payload_off + elif self.entity_description.key == "quiet_mode": + send_message = self._aircond_data0[:2] + self.payload_off + elif self.entity_description.key == "self_cleaning": + send_message = self.payload_off + self._aircond_data0[-2:] + elif (self.device_type == "826" and self.entity_description.key == "backlight"): + self._EAP_data0 = self._EAP_data0[:2] + "00" + mqtt.publish(self.hass, self.entity_description.mqttTopicCommand.replace("backlight", "program_data/0"), self._EAP_data0) + send_message = self.payload_off + else: + send_message = self.payload_off + mqtt.publish(self.hass, topic, send_message) + + + diff --git a/homeassistant/config/custom_components/polaris/time.py b/homeassistant/config/custom_components/polaris/time.py new file mode 100644 index 0000000..923ba6d --- /dev/null +++ b/homeassistant/config/custom_components/polaris/time.py @@ -0,0 +1,114 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from datetime import time +from homeassistant.components.time import DOMAIN, TimeEntity, TimeEntityDescription +#from homeassistant.components.datetime import DOMAIN, DateTimeEntity, DateTimeEntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + TIME_COOKER, + PolarisTimeEntityDescription, + POLARIS_COOKER_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + timeList = [] + + if (device_type in POLARIS_COOKER_TYPE): + TIME_COOKER_LC = copy.deepcopy(TIME_COOKER) + for description in TIME_COOKER_LC: + description.mqttTopicCurrentTime = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTime}" + description.mqttTopicCommandTime = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTime}" + description.device_prefix_topic = device_prefix_topic + timeList.append( + PolarisTime( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(timeList, update_before_add=True) + + +class PolarisTime(PolarisBaseEntity, TimeEntity): + + entity_description: PolarisTimeDescription + + def __init__( + self, + device_friendly_name: str, + description: PolarisTimeEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_available = False + self._attr_has_entity_name = True + self._attr_native_value = time(0, self.entity_description.default_time, 0) + + async def async_added_to_hass(self): + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + + async def async_set_value(self, value: time) -> None: + """Update the time.""" + value_hour=value.hour + value_minute=value.minute + value_second=0 + value_in_seconds = value.hour * 3600 + value.minute * 60 + if (value_in_seconds > (self.entity_description.max_time * 60)): + value_hour = int(self.entity_description.max_time / 60) - 1 + self._attr_native_value = time(value_hour,value_minute,value_second) + value_in_seconds = value_hour * 3600 + value_minute * 60 + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandTime, str(value_in_seconds)) + diff --git a/homeassistant/config/custom_components/polaris/translations/en.json b/homeassistant/config/custom_components/polaris/translations/en.json new file mode 100644 index 0000000..289e46f --- /dev/null +++ b/homeassistant/config/custom_components/polaris/translations/en.json @@ -0,0 +1,669 @@ +{ + "config": { + "abort": { + "already_configured": "This device has already been configured.", + "not_supported": "Error. Not selected device type." + }, + "error": { + "cannot_connect": "Failed to connect, please try again.", + "invalid_auth": "Invalid authentication.", + "too_many_requests": "Too many requests, retry later.", + "unknown": "Unexpected error.", + "Unknown error occurred": "Unexpected error." + }, + "step": { + "user": { + "description": "Configuring Polaris Local MQTT.", + "data": { + "DEVICEID": "Select a device (or all devices)", + "MQTT_ROOT_TOPIC": "Topic Prefix (usually 'polaris')" + }, + "title": "Polaris Local MQTT" + }, + "undef": { + "description": "Configuring Polaris device.", + "data": { + "DEVICETYPE": "Select device type" + }, + "title": "Polaris Local MQTT" + } + } + }, + "entity": { + "switch": { + "sound_switch": { + "name": "Sound" + }, + "power_switch": { + "name": "Power" + }, + "child_lock_switch": { + "name": "Child lock" + }, + "backlight_switch": { + "name": "Backlight" + }, + "backlight_bottom_switch": { + "name": "Bottom backlight" + }, + "ioniser_switch": { + "name": "Ioniser" + }, + "ozonation_switch": { + "name": "Ozonation" + }, + "warm_stream_switch": { + "name": "Warm stream" + }, + "keepwarm_switch": { + "name": "Keep warm" + }, + "ultraviolet_switch": { + "name": "Ultraviolet" + }, + "smart_mode": { + "name": "Smart" + }, + "bss_mode": { + "name": "BSS" + }, + "smart_mode_switch": { + "name": "Массаж" + }, + "damper_switch": { + "name": "Recycling" + }, + "damper_heater": { + "name": "Detecting open window" + }, + "display_off_heater": { + "name": "Auto off display" + }, + "half_power_heater": { + "name": "Half power" + }, + "backlight_bright": { + "name": "Bright 50/100" + }, + "turbo_switch": { + "name": "Turbo mode" + }, + "night_switch": { + "name": "Night mode" + }, + "night_light_switch": { + "name": "Night light" + }, + "self_cleaning": { + "name": "Self cleaning" + }, + "delicate_blowing": { + "name": "Delicate blowing" + }, + "eco_mode_switch": { + "name": "Eco mode" + }, + "auto_heater_switch": { + "name": "Auto heating" + }, + "quiet_mode": { + "name": "Quiet mode" + }, + "no_frost": { + "name": "No frost" + }, + "anti_fingus_switch": { + "name": "Anti fingus" + } + }, + "water_heater": { + "water_heater": { + "name": "Water heater", + "state": { + "off": "Off", + "performance": "Boiling", + "high_demand": "Keep with warm up", + "electric": "Warm up", + "heat_pump": "Warm up with keep", + "eco": "IQ boiling", + "gas": "Tea Time" + } + }, + "water_boiler": { + "name": "Boiler", + "state": { + "off": "Off", + "performance": "Low", + "electric": "Mid", + "heat_pump": "Turbo", + "eco": "Eco" + } + } + }, + "humidifier": { + "humidifier": { + "name": "Humidifier", + "state_attributes": { + "my_available_modes": { + "state": { + "auto": "Auto", + "comfort": "Health", + "baby": "Baby mode", + "sleep": "Night", + "boost": "Intensity", + "home": "Custom", + "eco": "Humidity", + "fitnes": "Fitnes", + "yoga": "Yoga", + "meditation": "Meditation", + "prana_hand": "Prana hand", + "prana_auto": "Prana auto", + "aroma": "Aroma" + } + } + } + } + }, + "number": { + "intensity": { + "name": "Intensity" + }, + "evaporation_rate": { + "name": "Evaporation rate" + }, + "set_temperature": { + "name": "Set temperature" + }, + "amount": { + "name": "Amount" + }, + "weight": { + "name": "Weight" + }, + "tank": { + "name": "Water tank" + }, + "pressure": { + "name": "Pressure" + }, + "speed": { + "name": "Speed" + }, + "temperature": { + "name": "Temperature" + }, + "display_time": { + "name": "Display time" + }, + "time_timer": { + "name": "Timer" + }, + "speed_irrigator": { + "name": "Speed irrigator" + }, + "temperature_difference_eco": { + "name": "Temperature difference eco" + }, + "temperature_difference_antifrost": { + "name": "Temperature difference antifrost" + }, + "bright_backlight": { + "name": "Bright backlight" + }, + "power_cable": { + "name": "Power cable" + } + }, + "sensor": { + "temperature_sensor": { + "name": "Temperature" + }, + "firmware_sensor": { + "name": "Firmware Version" + }, + "type_sensor": { + "name": "Device Type" + }, + "humidity_sensor": { + "name": "Humidity" + }, + "time_to_end_sensor": { + "name": "Time to end" + }, + "time_to_end_sensor_turbo": { + "name": "Time to end Turbo" + }, + "quality": { + "name": "Качество чистки" + }, + "power_consume": { + "name": "Power consume" + }, + "error": { + "name": "Error", + "state": { + "no_error": "No", + "low_water": "Low water", + "kettle_out_of_base": "Kettle is out of base station", + "temperature_sensor_failure": "Temperature sensor failure", + "child_lock": "Child lock operated", + "recommended_to_change_water": "It is recommended to change the water in the kettle", + "changed_water_for_long_time": "You probably haven't changed water for a long time. It is desirable to boil it", + "replace_filter": "It is recommended to replace the filter", + "maximum_schedules": "Maximum number of schedules 4", + "clean_tank": "Clean the tank", + "cup_not_present": "Cup is not present", + "gesture_sensor_error": "Gesture sensor error", + "side_door_open": "Side door open", + "waste_container_not_installed": "Waste container not installed", + "drip_tray_not_installed": "Drip tray not installed", + "the_brewing_unit_not_installed": "The brewing unit isn't installed", + "missing_water_tank": "Missing water tank", + "waste_container_full": "Waste container is full", + "not_enough_coffee_beans": "Not enough coffee beans", + "water_supply_blocked": "Water supply is blocked", + "code_e001": "The coffee machine is faulty (code E001) Contact service center", + "code_e002": "The coffee machine is faulty (code E002) Contact service center", + "code_e003": "The coffee machine is faulty (code E003) Contact service center", + "code_e004": "The coffee machine is faulty (code E004) Contact service center", + "code_e005": "The coffee machine is faulty (code E005) Contact service center", + "decalcification_required": "Decalcification required", + "water_changed_for_long_time": "Water in the tank hasn't been changed for a long time. It's recommended to change the water", + "cleaning_milk_system": "Cleaning the milk system", + "cleaning_brewing_system": "Cleaning the brewing system", + "decalcification_progress": "Decalcification in progress", + "cleaning_hydraulic_system": "Cleaning the hydraulic system", + "check_water_tank": "Сheck the water tank", + "cappuccinator_false": "Cappuccinator false", + "nothing_is_selected": "Nothing is selected" + } + }, + "filter_retain": { + "name": "Filter retain" + }, + "pre_filter_retain": { + "name": "Pre-filter retain" + }, + "clean_retain": { + "name": "Clean retain" + }, + "anode_retain": { + "name": "Anode retain" + }, + "mode_sensor": { + "name": "Mode", + "state": { + "off": "Off", + "espresso": "Espresso", + "ristretto": "Ristretto", + "long_espresso": "Long espresso", + "americano": "Americano", + "heating": "Heating", + "cappuccino": "Cappuccino", + "latte": "Latte", + "flat_white": "Flat white", + "cortado": "Cortado", + "double_espresso": "Double espresso", + "double_cappuccino": "Double cappuccino", + "double_latte": "Double latte", + "double_long_espresso": "Double long espresso", + "double_macchiato": "Double macchiato", + "milk_coffee": "Milk coffee", + "macchiato": "Macchiato", + "latte_macchiato": "Latte macchiato", + "hot_water": "Hot water", + "hot_milk_foam": "Hot milk foam", + "hot_milk": "Hot milk" + } + }, + "power_state": { + "name": "Power", + "state": { + "power_on": "On", + "power_off": "Off", + "turns_on": "Turns on", + "turns_off": "Turns off" + } + }, + "current_power": { + "name": "Current power" + }, + "outdoor_unit_temperature": { + "name": "Temperature outdoor unit" + } + }, + "select": { + "select_mode_kettle": { + "name": "Preset", + "state": { + "not_selected": "Not selected", + "black_tea": "Black tea", + "baby_bottle": "Baby bottle", + "instant_coffee": "Instant coffee", + "green_tea": "Green tea", + "flower_tea": "Flower tea", + "tea_bag": "Tea bag", + "red_tea": "Red tea", + "puerh_tea": "Puerh tea", + "oolong_tea": "Oolong tea", + "white_tea": "White tea", + "herbal_tea": "Herbal tea" + } + }, + "select_mode_cooker": { + "name": "Recipes", + "state": { + "my_recipe_plus": "My recipe plus", + "reheat": "Reheat", + "cake": "Cake", + "soaked_rice": "Soaked rice", + "stew": "Stew", + "fry": "Fry", + "pilaf": "Pilaf", + "yogurt": "Yogurt", + "oatmeal": "Catmeal", + "milk_porridge": "Milk porridge", + "soup": "Soup", + "meat": "Meat", + "cottage_cheese": "Cottage cheese" + } + }, + "select_mode_cofeemaker": { + "name": "Напиток", + "state": { + "not_selected": "Not selected", + "espresso": "Espresso", + "ristretto": "Ristretto", + "long_espresso": "Long espresso", + "americano": "Americano", + "cappuccino": "Cappuccino", + "latte": "Latte", + "flat_white": "Flat white", + "cortado": "Cortado", + "double_espresso": "Double espresso", + "double_cappuccino": "Double cappuccino", + "double_latte": "Double latte", + "double_long_espresso": "Double long espresso", + "double_macchiato": "Double macchiato", + "milk_coffee": "Milk coffee", + "macchiato": "Macchiato", + "latte_macchiato": "Latte macchiato", + "hot_water": "Hot water", + "hot_milk_foam": "Hot milk foam", + "hot_milk": "Hot milk", + "doppio": "Doppio", + "lungo": "Lungo", + "clearing": "Clearing", + "heating": "Heating" + } + }, + "select_melody": { + "name": "Melody", + "state": { + "mute": "Without sound", + "rainstorm": "Rainstorm", + "surf": "Surf", + "forest": "In the forest", + "birdsong": "Birdsong", + "bonfire": "Bonfire" + } + }, + "select_irrigator": { + "name": "Personal modes", + "state": { + "preset1": "Mode 1", + "preset2": "Mode 2", + "preset3": "Mode 3" + } + }, + "select_night_backlight": { + "name": "Night backlight", + "state": { + "off": "Off", + "all_on": "All on", + "high_on": "Hight on", + "mid_on": "Mid on", + "low_on": "Low on" + } + }, + "select_swing_horizontal": { + "name": "Swing horizontal", + "state": { + "off": "Swing", + "top": "Top", + "high": "High", + "middle": "Middle", + "low": "Low", + "bottom": "Bottom" + } + }, + "select_swing_vertical": { + "name": "Swing vertical", + "state": { + "off": "Swing", + "left": "Left", + "center-left": "Center-left", + "center": "Center", + "center-right": "Center-right", + "right": "Right" + } + } + }, + "light": { + "night_light": { + "name": "Night light" + }, + "night_light_up": { + "name": "Top backlight" + }, + "night_light_down": { + "name": "Bottom backlight" + } + }, + "binary_sensor": { + "base_binary_sensor": { + "name": "Position", + "state": { + "on": "Not on the base", + "off": "On the base" + } + }, + "lid_binary_sensor": { + "name": "Lid", + "state": { + "on": "Open", + "off": "Close" + } + }, + "water_tank_binary_sensor": { + "name": "Tank is removed", + "state": { + "on": "Yes", + "off": "No" + } + }, + "cappuccinator_binary_sensor": { + "name": "Cappuccinator", + "state": { + "on": "Installed", + "off": "Not installed" + } + }, + "heating_binary_sensor": { + "name": "Heating", + "state": { + "on": "Heats", + "off": "Not heat" + } + }, + "available_binary_sensor": { + "name": "LAN", + "state": { + "on": "Online", + "off": "Offline" + } + } + }, + "time": { + "delay_start": { + "name": "Delay start" + }, + "cooking_time": { + "name": "Cooking time" + } + }, + "button": { + "button_stop": { + "name": "Stop" + }, + "button_start": { + "name": "Start" + }, + "button_stop_coffee": { + "name": "Stop" + }, + "button_start_coffee": { + "name": "Start" + }, + "button_reset_filter": { + "name": "Reset time filter" + }, + "button_reset_prefilter": { + "name": "Reset time pre-filter" + }, + "button_reset_tank": { + "name": "Reset time water tank" + } + }, + "climate": { + "climate": { + "name": "Climate", + "state_attributes": { + "fan_mode": { + "state": { + "1_speed": "1 speed", + "2_speed": "2 speed", + "3_speed": "3 speed", + "4_speed": "4 speed", + "5_speed": "5 speed", + "6_speed": "6 speed", + "7_speed": "7 speed", + "8_speed": "8 speed", + "9_speed": "9 speed" + } + }, + "preset_mode": { + "state": { + "hands": "Hands", + "auto": "Auto", + "night": "Night", + "turbo": "Turbo", + "passive": "Passive" + } + } + } + }, + "heater": { + "name": "Heater", + "state_attributes": { + "fan_mode": { + "name": "Heating power", + "state": { + "10_percent": "10 %", + "20_percent": "20 %", + "30_percent": "30 %", + "40_percent": "40 %", + "50_percent": "50 %", + "60_percent": "60 %", + "70_percent": "70 %", + "80_percent": "80 %", + "90_percent": "90 %", + "100_percent": "100 %", + "20_5_percent": "20 %", + "40_5_percent": "40 %", + "60_5_percent": "60 %", + "80_5_percent": "80 %", + "100_5_percent": "100 %" + } + } + } + }, + "aircleaner": { + "name": "Air cleaner", + "state": { + "dry": "Сlearing" + }, + "state_attributes": { + "preset_mode": { + "state": { + "hands": "Hands", + "auto": "Auto", + "night": "Night" + } + }, + "fan_mode": { + "state": { + "top": "Turbo" + } + } + } + }, + "conditioner": { + "name": "Conditioner", + "state_attributes": { + "fan_mode": { + "state": { + "min": "Max", + "max": "Min" + } + } + } + }, + "thermostat": { + "name": "Thermostat", + "state_attributes": { + "preset_mode": { + "state": { + "eco": "Eco", + "comfort": "Comfort", + "turbo": "Turbo", + "antifrost": "Antifrost", + "schedule": "Schedule", + "vacation": "Vacation", + "manual": "Manual" + } + } + } + } + } + }, + "common": { + "all": "All devices", + "kettle": "Kettle", + "humidifier": "Humidifier", + "cooker": "Cooker", + "coffeemaker": "Coffeemaker", + "air_cleaner": "Air cleaner", + "irrigator": "Irrigator", + "air_fryer": "Air Fryer", + "boiler": "Boiler", + "heater": "Heater", + "cleaner": "Cleaner", + "blender": "Blender", + "cooktop": "Cooktop", + "cordless_cleaner": "Cordless cleaner", + "fan": "Fan", + "grill": "Grill", + "hair_care": "Hair care", + "hood": "Hood", + "iron": "Iron", + "kitchen_machine": "Kitchen machine", + "meat_grinder": "Meat grinder", + "other": "Smart Lid", + "oven": "Oven", + "steamer": "Steamer", + "toothbrush": "Toothbrush", + "air_conditioner": "Conditioner", + "thermostat": "Thermostat" + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/polaris/translations/ru.json b/homeassistant/config/custom_components/polaris/translations/ru.json new file mode 100644 index 0000000..7a1453e --- /dev/null +++ b/homeassistant/config/custom_components/polaris/translations/ru.json @@ -0,0 +1,669 @@ +{ + "config": { + "abort": { + "already_configured": "Это устройство уже было настроено.", + "not_supported": "Ошибка. Не выбран тип устройства." + }, + "error": { + "cannot_connect": "Не удалось подключиться, пожалуйста, повторите попытку.", + "invalid_auth": "Неверная аутентификация.", + "too_many_requests": "Слишком много запросов, повторите попытку позже.", + "unknown": "Неожиданная ошибка.", + "Unknown error occurred": "Неожиданная ошибка." + }, + "step": { + "user": { + "description": "Конфигурация Polaris Local MQTT.", + "data": { + "DEVICEID": "Выберите устройство", + "MQTT_ROOT_TOPIC": "Префикс темы (обычно 'polaris')" + }, + "title": "Polaris Local MQTT" + }, + "undef": { + "description": "Конфигурация устройства Polaris.", + "data": { + "DEVICETYPE": "Выберите тип устройства" + }, + "title": "Polaris Local MQTT" + } + } + }, + "entity": { + "switch": { + "sound_switch": { + "name": "Звук" + }, + "power_switch": { + "name": "Включить" + }, + "child_lock_switch": { + "name": "Блокировка" + }, + "backlight_switch": { + "name": "Подсветка" + }, + "backlight_bottom_switch": { + "name": "Нижняя подсветка" + }, + "ioniser_switch": { + "name": "Ионизация" + }, + "ozonation_switch": { + "name": "Озонирование" + }, + "warm_stream_switch": { + "name": "Теплый пар" + }, + "keepwarm_switch": { + "name": "Поддержание температуры" + }, + "ultraviolet_switch": { + "name": "Ультрафиолет" + }, + "smart_mode": { + "name": "Умный режим" + }, + "bss_mode": { + "name": "Антибактериальная очистка" + }, + "smart_mode_switch": { + "name": "Массаж" + }, + "damper_switch": { + "name": "Рециркуляция" + }, + "damper_heater": { + "name": "Обнаружение открытого окна" + }, + "display_off_heater": { + "name": "Автоотключение дисплея" + }, + "half_power_heater": { + "name": "Половина мощности" + }, + "backlight_bright": { + "name": "Яркость 50/100" + }, + "turbo_switch": { + "name": "Турбо режим" + }, + "night_switch": { + "name": "Ночной режим" + }, + "night_light_switch": { + "name": "Ночная подсветка" + }, + "self_cleaning": { + "name": "Самоочистка" + }, + "delicate_blowing": { + "name": "Деликатный обдув" + }, + "eco_mode_switch": { + "name": "Экономичный режим" + }, + "auto_heater_switch": { + "name": "Автообогрев" + }, + "quiet_mode": { + "name": "Тихий режим" + }, + "no_frost": { + "name": "Антизамерзание" + }, + "anti_fingus_switch": { + "name": "Профилактика грибка" + } + }, + "water_heater": { + "water_heater": { + "name": "Нагрев", + "state": { + "off": "Выключен", + "performance": "Кипячение", + "high_demand": "Кипячение с удержанием", + "electric": "Разогрев", + "heat_pump": "Разогрев с удержанием", + "eco": "IQ Кипячение", + "gas": "Чайная церемония" + } + }, + "water_boiler": { + "name": "Бойлер", + "state": { + "off": "Выключен", + "performance": "Низкий", + "electric": "Средний", + "heat_pump": "Высокий", + "eco": "Экономичный" + } + } + }, + "humidifier": { + "humidifier": { + "name": "Увлажнитель", + "state_attributes": { + "mode": { + "state": { + "auto": "Авто", + "comfort": "Здоровье", + "baby": "Детский", + "sleep": "Ночной", + "boost": "Интенсивность", + "home": "Ручной", + "eco": "Увлажнение", + "fitnes": "Фитнес", + "yoga": "Йога", + "meditation": "Медитация", + "prana_hand": "Прана ручной", + "prana_auto": "Прана авто", + "aroma": "Арома" + } + } + } + } + }, + "number": { + "intensity": { + "name": "Интенсивность" + }, + "evaporation_rate": { + "name": "Скорость испарения" + }, + "set_temperature": { + "name": "Установка температуры" + }, + "amount": { + "name": "Объём кофе" + }, + "weight": { + "name": "Крепкость напитка" + }, + "tank": { + "name": "Объём напитка" + }, + "pressure": { + "name": "Объём пенки" + }, + "speed": { + "name": "Объём молока" + }, + "temperature": { + "name": "Температура напитка" + }, + "display_time": { + "name": "Отключение дисплея" + }, + "time_timer": { + "name": "Таймер отключения" + }, + "speed_irrigator": { + "name": "Скорость" + }, + "temperature_difference_eco": { + "name": "Разница температур в режиме ECO" + }, + "temperature_difference_antifrost": { + "name": "Разница температур в режиме Anti frost" + }, + "bright_backlight": { + "name": "Яркость подсветки" + }, + "power_cable": { + "name": "Мощность кабеля" + } + }, + "sensor": { + "temperature_sensor": { + "name": "Температура" + }, + "firmware_sensor": { + "name": "Версия прошивки" + }, + "type_sensor": { + "name": "Тип устройства" + }, + "humidity_sensor": { + "name": "Влажность" + }, + "time_to_end_sensor": { + "name": "Оставшееся время" + }, + "time_to_end_sensor_turbo": { + "name": "Оставшееся время Турбо" + }, + "quality": { + "name": "Качество чистки" + }, + "power_consume": { + "name": "Потребление энергии" + }, + "error": { + "name": "Ошибка", + "state": { + "no_error": "Нет", + "low_water": "Мало воды", + "kettle_out_of_base": "Чайник снят с базы", + "temperature_sensor_failure": "Ошибка термодатчика", + "child_lock": "Сработала защита от случайного нажатия", + "recommended_to_change_water": "Рекомендуется сменить воду в чайнике", + "changed_water_for_long_time": "Вероятно, вы давно не меняли воду. Ее желательно прокипятить", + "replace_filter": "Рекомендуется заменить фильтр", + "maximum_schedules": "Максимальное количество расписаний 4", + "clean_tank": "Почистите бак", + "cup_not_present": "Чаша не установлена", + "gesture_sensor_error": "Ошибка датчика жестов", + "side_door_open": "Открыта боковая дверца", + "waste_container_not_installed": "Контейнер для отходов не установлен", + "drip_tray_not_installed": "Каплесборник не установлен", + "the_brewing_unit_not_installed": "Заварочный узел не установлен", + "missing_water_tank": "Отсутствует резервуар для воды", + "waste_container_full": "Контейнер для отходов полон", + "not_enough_coffee_beans": "Недостаточно кофейных зерен", + "water_supply_blocked": "Подача воды заблокирована", + "code_e001": "Кофемашина неисправна (код Е001) Обратитесь в сервисный центр", + "code_e002": "Кофемашина неисправна (код Е002) Обратитесь в сервисный центр", + "code_e003": "Кофемашина неисправна (код Е003) Обратитесь в сервисный центр", + "code_e004": "Кофемашина неисправна (код Е004) Обратитесь в сервисный центр", + "code_e005": "Кофемашина неисправна (код Е005) Обратитесь в сервисный центр", + "decalcification_required": "Необходима декальцинация", + "water_changed_for_long_time": "Вода в резервуаре давно не менялась, рекомендуем сменить воду", + "cleaning_milk_system": "Очистка молочной системы", + "cleaning_brewing_system": "Очистка заварочной системы", + "decalcification_progress": "Идет декальцинация", + "cleaning_hydraulic_system": "Очистка гидросистемы", + "check_water_tank": "Проверьте бак с водой", + "cappuccinator_false": "Капучинатор не установлен", + "nothing_is_selected": "Не выбран напиток" + } + }, + "filter_retain": { + "name": "Остаток фильтра" + }, + "pre_filter_retain": { + "name": "Остаток предфильтра" + }, + "clean_retain": { + "name": "Остаток до очистки бака" + }, + "anode_retain": { + "name": "Остаток до замены анода" + }, + "mode_sensor": { + "name": "Режим работы", + "state": { + "off": "Выключен", + "espresso": "Эспрессо", + "ristretto": "Ристретто", + "long_espresso": "Лунго", + "americano": "Американо", + "heating": "Нагрев", + "cappuccino": "Капучино", + "latte": "Латте", + "flat_white": "Флэт уайт", + "cortado": "Кортадо", + "double_espresso": "Двойной эспрессо", + "double_cappuccino": "Двойной капучино", + "double_latte": "Двойной латте", + "double_long_espresso": "Двойной лунго", + "double_macchiato": "Двойной макиато", + "milk_coffee": "Кофе с молоком", + "macchiato": "Макиато", + "latte_macchiato": "Латте макиато", + "hot_water": "Горячая вода", + "hot_milk foam": "Молочная пена", + "hot_milk": "Горячее молоко" + } + }, + "power_state": { + "name": "Питание", + "state": { + "power_on": "Включено", + "power_off": "Выключено", + "turns_on": "Включается", + "turns_off": "Выключается" + } + }, + "сurrent_power": { + "name": "Текущая мощность" + }, + "outdoor_unit_temperature": { + "name": "Температура внешнего блока" + } + }, + "select": { + "select_mode_kettle": { + "name": "Предустановки", + "state": { + "not_selected": "Не выбрано", + "black_tea": "Черный чай", + "baby_bottle": "Детская смесь", + "instant_coffee": "Растворимый кофе", + "green_tea": "Зеленый чай", + "flower_tea": "Цветочный чай", + "tea_bag": "Пакетированный чай", + "red_tea": "Красный чай", + "puerh_tea": "Пуэр", + "oolong_tea": "Улун", + "white_tea": "Белый чай", + "herbal_tea": "Травяной чай" + } + }, + "select_mode_cooker": { + "name": "Рецепты", + "state": { + "my_recipe_plus": "Мой рецепт +", + "reheat": "Разогрев", + "cake": "Выпечка", + "soaked_rice": "Крупа", + "stew": "Тушение", + "fry": "Жарка", + "pilaf": "Плов", + "yogurt": "Йогурт", + "oatmeal": "Овсянка", + "milk_porridge": "Молочная каша", + "soup": "Суп", + "meat": "Холодец", + "cottage_cheese": "Творог" + } + }, + "select_mode_cofeemaker": { + "name": "Напиток", + "state": { + "not_selected": "Не выбрано", + "espresso": "Эспрессо", + "ristretto": "Ристретто", + "long_espresso": "Лунго", + "americano": "Американо", + "cappuccino": "Капучино", + "latte": "Латте", + "flat_white": "Флэт уайт", + "cortado": "Кортадо", + "double_espresso": "Двойной эспрессо", + "double_cappuccino": "Двойной капучино", + "double_latte": "Двойной латте", + "double_long_espresso": "Двойной лунго", + "double_macchiato": "Двойной макиато", + "milk_coffee": "Кофе с молоком", + "macchiato": "Макиато", + "latte_macchiato": "Латте макиато", + "hot_water": "Горячая вода", + "hot_milk_foam": "Молочная пена", + "hot_milk": "Горячее молоко", + "doppio": "Доппио", + "lungo": "Лунго", + "clearing": "Очистка", + "heating": "Подогрев" + } + }, + "select_melody": { + "name": "Мелодия", + "state": { + "mute": "Без звука", + "rainstorm": "Ливень", + "surf": "Прибой", + "forest": "В лесу", + "birdsong": "Пение птиц", + "bonfire": "Костер" + } + }, + "select_irrigator": { + "name": "Персональные режимы", + "state": { + "preset1": "Режим 1", + "preset2": "Режим 2", + "preset3": "Режим 3" + } + }, + "select_night_backlight": { + "name": "Ночная подсветка", + "state": { + "off": "Выключена", + "all_on": "Весь свет включен", + "high_on": "Верхний свет включен", + "mid_on": "Средний свет включен", + "low_on": "Нижний свет включен" + } + }, + "select_swing_horizontal": { + "name": "Вертикальный обдув", + "state": { + "off": "Качание", + "top": "Вверх", + "high": "Выше среднего", + "middle": "Среднее", + "low": "Ниже среднего", + "bottom": "Вниз" + } + }, + "select_swing_vertical": { + "name": "Горизонтальный обдув", + "state": { + "off": "Качание", + "left": "Влево", + "center-left": "Левее центра", + "center": "Центр", + "center-right": "Правее центра", + "right": "Вправо" + } + } + }, + "light": { + "night_light": { + "name": "Ночник" + }, + "night_light_up": { + "name": "Верхняя подсветка" + }, + "night_light_down": { + "name": "Нижняя подсветка" + } + }, + "binary_sensor": { + "base_binary_sensor": { + "name": "Положение", + "state": { + "on": "Снят с базы", + "off": "На базе" + } + }, + "lid_binary_sensor": { + "name": "Крышка", + "state": { + "on": "Открыта", + "off": "Закрыта" + } + }, + "water_tank_binary_sensor": { + "name": "Бак снят", + "state": { + "on": "Да", + "off": "Нет" + } + }, + "cappuccinator_binary_sensor": { + "name": "Капучинатор", + "state": { + "on": "Установлен", + "off": "Не установлен" + } + }, + "heating_binary_sensor": { + "name": "Нагрев", + "state": { + "on": "Греет", + "off": "Не греет" + } + }, + "available_binary_sensor": { + "name": "Локальная сеть", + "state": { + "on": "В сети", + "off": "Не в сети" + } + } + }, + "time": { + "delay_start": { + "name": "Отложенный старт" + }, + "cooking_time": { + "name": "Время приготовления" + } + }, + "button": { + "button_stop": { + "name": "Остановить" + }, + "button_start": { + "name": "Запустить" + }, + "button_stop_coffee": { + "name": "Остановить" + }, + "button_start_coffee": { + "name": "Приготовить" + }, + "button_reset_filter": { + "name": "Сброс таймера фильтра" + }, + "button_reset_prefilter": { + "name": "Сброс таймера префильтра" + }, + "button_reset_tank": { + "name": "Сброс таймера очистки бака" + } + }, + "climate": { + "climate": { + "name": "Климат", + "state_attributes": { + "fan_mode": { + "state": { + "1_speed": "1 скорость", + "2_speed": "2 скорость", + "3_speed": "3 скорость", + "4_speed": "4 скорость", + "5_speed": "5 скорость", + "6_speed": "6 скорость", + "7_speed": "7 скорость", + "8_speed": "8 скорость", + "9_speed": "9 скорость" + } + }, + "preset_mode": { + "state": { + "hands": "Ручной", + "auto": "Автоматический", + "night": "Ночной", + "turbo": "Турбо", + "passive": "Пассивный" + } + } + } + }, + "heater": { + "name": "Конвектор", + "state_attributes": { + "fan_mode": { + "name": "Мощность нагрева", + "state": { + "10_percent": "10 %", + "20_percent": "20 %", + "30_percent": "30 %", + "40_percent": "40 %", + "50_percent": "50 %", + "60_percent": "60 %", + "70_percent": "70 %", + "80_percent": "80 %", + "90_percent": "90 %", + "100_percent": "100 %", + "20_5_percent": "20 %", + "40_5_percent": "40 %", + "60_5_percent": "60 %", + "80_5_percent": "80 %", + "100_5_percent": "100 %" + } + } + } + }, + "aircleaner": { + "name": "Очиститель воздуха", + "state": { + "dry": "Очистка" + }, + "state_attributes": { + "preset_mode": { + "state": { + "hands": "Ручной", + "auto": "Автоматический", + "night": "Ночной" + } + }, + "fan_mode": { + "state": { + "top": "Турбо" + } + } + } + }, + "conditioner": { + "name": "Кондиционер", + "state_attributes": { + "fan_mode": { + "state": { + "min": "Минимум", + "max": "Максимум" + } + } + } + }, + "thermostat": { + "name": "Термостат", + "state_attributes": { + "preset_mode": { + "state": { + "eco": "Экономичный", + "comfort": "Комфорт", + "turbo": "Турбо", + "antifrost": "Антизамерзание", + "schedule": "Расписание", + "vacation": "Отпуск", + "manual": "Настройка" + } + } + } + } + } + }, + "common": { + "all": "Выберите тип устройства", + "kettle": "Чайник", + "humidifier": "Увлажнитель", + "cooker": "Мультиварка", + "coffeemaker": "Кофемашина", + "air_cleaner": "Очиститель воздуха", + "irrigator": "Ирригатор", + "air_fryer": "Воздухоочиститель", + "boiler": "Бойлер", + "heater": "Обогреватель", + "cleaner": "Робот пылесос", + "blender": "Блендер", + "cooktop": "Варочная панель", + "cordless_cleaner": "Ручной пылесос", + "fan": "Вентилятор", + "grill": "Гриль", + "hair_care": "Фен", + "hood": "Вытяжка", + "iron": "Утюг", + "kitchen_machine": "Кухонная машина", + "meat_grinder": "Мясорубка", + "other": "Умная крышка", + "oven": "Духовка", + "steamer": "Отпариватель", + "toothbrush": "Зубная щетка", + "air_conditioner": "Кондиционер", + "thermostat": "Термостат" + } +} \ No newline at end of file diff --git a/homeassistant/config/custom_components/polaris/vacuum.py b/homeassistant/config/custom_components/polaris/vacuum.py new file mode 100644 index 0000000..b7a0c83 --- /dev/null +++ b/homeassistant/config/custom_components/polaris/vacuum.py @@ -0,0 +1,523 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable, Final, Any +import copy +import datetime +import os +import voluptuous as vol +import struct + + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.vacuum import ( + DOMAIN, + ATTR_CLEANED_AREA, + StateVacuumEntity, +# VacuumActivity, + VacuumEntityFeature, +) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback, ServiceCall +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers import config_validation as cv, entity_platform +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + VACUUM, + PolarisSelectEntityDescription, + POLARIS_VACUUM_TYPE, + CUSTOM_SELECT_FILE_PATH, + SELECT_VACUUM, +) + +SERVICE_VACUUM_CLEANING_ROOM: Final = "vacuum_cleaning_room" +ATTR_SELECTED_ROOMS = "rooms" +SELECT_ROOMS = "select_rooms" + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + vacuumList = [] + + +# if rooms_js: +# available_rooms = list(rooms_js.keys()) + + file_path = CUSTOM_SELECT_FILE_PATH + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + custom_data_rooms = json.loads(file.read()) + else: + custom_data_rooms = None + + if custom_data_rooms is not None and "SELECT_VACUUM_rooms" in custom_data_rooms: + custom_data_rooms = json.loads(json.dumps(custom_data_rooms)) + rooms_js = custom_data_rooms["SELECT_VACUUM_rooms"] +# _LOGGER.debug("rooms_js %s", rooms_js) + else: + rooms_js = {"no_room": {"id": "00", "coordinates": []}} + available_rooms = list(rooms_js.keys()) + + + +# _LOGGER.debug("available_rooms read %s", available_rooms) + + + + + if (device_type in POLARIS_VACUUM_TYPE): + VACUUM_LC = copy.deepcopy(VACUUM) + for description in VACUUM_LC: + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicBatteryState = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicBatteryState}" + description.mqttTopicBatteryLevel = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicBatteryLevel}" + description.mqttTopicStateFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicStateFanMode}" + description.mqttTopicCommandFanMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFanMode}" + description.mqttTopicCommandFindMe = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandFindMe}" + description.mqttTopicCommandGoArea = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandGoArea}" + description.mqttTopicCommandTest = f"{mqtt_root}/{device_prefix_topic}/state" + description.device_prefix_topic = device_prefix_topic + vacuumList.append( + PolarisVacuum( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + available_rooms=available_rooms, + rooms_js=rooms_js + ) + ) + async_add_entities(vacuumList, update_before_add=True) + + + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + "select_rooms", + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(SELECT_ROOMS): vol.All(cv.string, vol.In(available_rooms)) + }, + "sweep_rooms_wrapper", + ) + + +class PolarisVacuum(PolarisBaseEntity, StateVacuumEntity): + + entity_description: PolarisVacuumEntityDescription +# _unrecorded_attributes = frozenset({ATTR_ROOMS}) + + + def __init__( + self, + device_friendly_name: str, + description: PolarisVacuumEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + available_rooms: list | None=None, + rooms_js: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self._attr_has_entity_name = True + + self._attr_fan_speed="min" + self._attr_fan_speed_list = ["min", "medium", "high", "max"] + self._attr_supported_features = ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.CLEAN_SPOT + | VacuumEntityFeature.STOP +# | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.START + | VacuumEntityFeature.LOCATE + | VacuumEntityFeature.STATE + | VacuumEntityFeature.SEND_COMMAND + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.STATUS + | VacuumEntityFeature.MAP + ) +# self._entity_component_unrecorded_attributes = frozenset({ATTR_FAN_SPEED_LIST}) + + self._attr_battery_icon="mdi:vacuum" + self._attr_battery_level=70 + self._attr_state = "idle" + + + + self._select_rooms = [] + self._available_rooms = available_rooms + self._rooms_js = rooms_js + + + + + @property + def select_rooms(self) -> list | None: + """Return a list of rooms available to clean.""" + if self._select_rooms: +# _LOGGER.debug("select_rooms : %s", self._select_rooms) + return self._rooms + return [] + + @property + def extra_state_attributes(self): + """Return a dictionary of device state attributes specific to sharkiq.""" + data = {} + if self._available_rooms is not None: + data["available_rooms"] = self._available_rooms + if self._select_rooms is not None: + data["select_rooms"] = self._select_rooms + return data + + + def int16_array_to_bytes(self, int16_array, byteorder='little'): + """ + Преобразует массив int16 обратно в байтовую строку. + Аргументы: + int16_array: Массив int16 значений (list of int). + byteorder: Порядок байтов ('little' или 'big'), по умолчанию 'little'. + Возвращает: + Байтовая строка (bytes). + Вызывает исключение ValueError: + Если byteorder не является 'little' или 'big'. + """ + if byteorder not in ('little', 'big'): + raise ValueError("Неверный порядок байтов. Допустимые значения: 'little', 'big'.") + endian_prefix = '<' if byteorder == 'little' else '>' + format_string = endian_prefix + 'h' * len(int16_array) + return struct.pack(format_string, *int16_array) # * распаковывает массив в аргументы для pack + + async def sweep_rooms_wrapper(self, select_rooms): +# for room in self._room_manager.rooms.keys(): +# self._room_manager.rooms[room] = False + _LOGGER.debug("room in service %s ", select_rooms) + #for entry in mysel_rooms: + # name = self.hass.states.get(entry).attributes['room_name'] + # _LOGGER.debug("target_rooms entry %s", entry) +# await self.sweep_rooms(target_rooms) +# self.async_schedule_update_ha_state(force_refresh=True) + + + def start(self) -> None: + """Start or resume the cleaning task.""" + if self._attr_state != "cleaning": + self._attr_state = "cleaning" + self.schedule_update_ha_state() + state_mode = self.hass.states.get(f"select.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_select_mode_vacuum").state + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, json.loads(json.dumps(SELECT_VACUUM[0].options[state_mode]))) + + def stop(self, **kwargs: Any) -> None: + """Stop the cleaning task, do not return to dock.""" + self._attr_state = "idle" + self.schedule_update_ha_state() + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, "0") + + def return_to_base(self, **kwargs: Any) -> None: + """Return dock to charging base.""" + self._attr_state = "returning" + self.schedule_update_ha_state() + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandMode, "5") + + def clean_spot(self, **kwargs: Any) -> None: + """Perform a spot clean-up.""" + self._attr_state = "cleaning" + self.schedule_update_ha_state() + select_room = self.hass.states.get(f"select.{POLARIS_DEVICE[int(self.device_type)]['class']}_{POLARIS_DEVICE[int(self.device_type)]['model'].replace('-', '_')}_select_room").state +# _LOGGER.debug("room in select %s ", select_room) +# _LOGGER.debug("room in select %s ", self._rooms_js[select_room]["coordinate"]) +# _LOGGER.debug("int16_to_bytes %s",self.int16_array_to_bytes(self._rooms_js[select_room]["coordinate"])) + mqtt.publish( + self.hass, self.entity_description.mqttTopicCommandGoArea, + self.int16_array_to_bytes(self._rooms_js[select_room]["coordinate"]), + 1, + None, + ) + + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set the vacuum's fan speed.""" + if fan_speed in self.fan_speed_list: + self._attr_fan_speed = fan_speed + self.schedule_update_ha_state() + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandFanMode, self._attr_fan_speed_list.index(fan_speed)+1) + + async def async_locate(self, **kwargs: Any) -> None: + """Locate the vacuum's position.""" + await self.hass.services.async_call( + "notify", + "persistent_notification", + service_data={"message": "I'm here!", "title": "Locate request"}, + ) + self._attr_state = "idle" + self.async_write_ha_state() + mqtt.publish(self.hass, self.entity_description.mqttTopicCommandFindMe, "true") + + + async def async_send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: + """Send a command to the vacuum.""" + self._attr_state = "idle" + self.async_write_ha_state() + + + +# def _save_log(self, message, topic) -> None: +# file_path = "vacuum_log.txt" +# with open(file_path, 'a+', encoding='utf-8') as file: +# file.write(f"{datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")} {topic} {message}\n") + + + + async def async_added_to_hass(self): + @callback + def message_received_batt_state(message): + payload = message.payload + self._attr_state = payload + await mqtt.async_subscribe(self.hass, self.entity_description.mqttTopicBatteryState, message_received_batt_state, 1) + + @callback + def message_received_batt_level(message): + payload = message.payload + self._attr_battery_level = int(payload) + self.async_write_ha_state() + await mqtt.async_subscribe(self.hass, self.entity_description.mqttTopicBatteryLevel, message_received_batt_level, 1) + + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + + + # @callback + # def message_received_contour(message): + # payload = message.payload + # self._save_log(payload, "Log contour:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/contour", message_received_contour, 1, None) + + # @callback + # def message_received_go_area(message): + # payload = message.payload + # self._save_log(payload, "Log go_area:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/go_area", message_received_go_area, 1, None) + + # @callback + # def message_received_mode(message): + # payload = message.payload + # self._save_log(payload, "Log mode:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/mode", message_received_mode, 1) + + # @callback + # def message_received_map_angle(message): + # payload = message.payload + # self._save_log(payload, "Log map_angle:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_angle", message_received_map_angle, 1) + + # @callback + # def message_received_clean_area(message): + # payload = message.payload + # self._save_log(payload, "Log clean_area:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/clean_area", message_received_clean_area, 1) + + # @callback + # def message_received_program_data_0(message): + # payload = message.payload + # self._save_log(payload, "Log program_data_0:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/program_data/0", message_received_program_data_0, 1) + + # @callback + # def message_received_program_data_1(message): + # payload = message.payload + # self._save_log(payload, "Log program_data_1:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/program_data/1", message_received_program_data_1, 1) + + # @callback + # def message_received_program_data_2(message): + # payload = message.payload + # self._save_log(payload, "Log program_data_2:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/program_data/2", message_received_program_data_2, 1) + + # @callback + # def message_received_program_data_3(message): + # payload = message.payload + # self._save_log(payload, "Log program_data_3:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/program_data/3", message_received_program_data_3, 1) + + # @callback + # def message_received_program_data_4(message): + # payload = message.payload + # self._save_log(payload, "Log program_data_4:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/program_data/4", message_received_program_data_4, 1) + + # @callback + # def message_received_location_current(message): + # payload = message.payload + # self._save_log(payload, "Log location_current:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/location_current", message_received_location_current, 1) + + # @callback + # def message_received_map_0(message): + # payload = message.payload + # self._save_log(payload, "Log map_0:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map/0", message_received_map_0, 1, None) + + # @callback + # def message_received_map_1(message): + # payload = message.payload + # self._save_log(payload, "Log map_1:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map/1", message_received_map_1, 1, None) + + # @callback + # def message_received_virtual_wall(message): + # payload = message.payload + # self._save_log(payload, "Log virtual_wall:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/virtual_wall", message_received_virtual_wall, 1, None) + + # @callback + # def message_received_no_go_area(message): + # payload = message.payload + # self._save_log(payload, "Log no_go_area:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/no_go_area", message_received_no_go_area, 1, None) + + # @callback + # def message_received_map_image(message): + # payload = message.payload + # self._save_log(payload, "Log map_image:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_image", message_received_map_image, 1, None) + + # @callback + # def message_received_map_long_0(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_0:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/0", message_received_map_long_0, 1, None) + + # @callback + # def message_received_map_long_1(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_1:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/1", message_received_map_long_1, 1, None) + + # @callback + # def message_received_map_long_2(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_2:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/2", message_received_map_long_2, 1, None) + + # @callback + # def message_received_map_long_3(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_3:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/3", message_received_map_long_3, 1, None) + + # @callback + # def message_received_map_long_4(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_4:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/4", message_received_map_long_4, 1, None) + + # @callback + # def message_received_map_long_5(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_5:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/5", message_received_map_long_5, 1, None) + + # @callback + # def message_received_map_long_6(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_6:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/6", message_received_map_long_6, 1, None) + + # @callback + # def message_received_map_long_7(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_7:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/7", message_received_map_long_7, 1, None) + + # @callback + # def message_received_map_long_8(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_8:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/8", message_received_map_long_8, 1, None) + + # @callback + # def message_received_map_long_9(message): + # payload = message.payload + # self._save_log(payload, "Log map_long_9:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_long/9", message_received_map_long_9, 1, None) + + # @callback + # def message_received_map_location_0(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_0:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/0", message_received_map_location_0, 1, None) + + # @callback + # def message_received_map_location_1(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_1:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/1", message_received_map_location_1, 1, None) + + # @callback + # def message_received_map_location_2(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_2:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/2", message_received_map_location_2, 1, None) + + # @callback + # def message_received_map_location_3(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_3:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/3", message_received_map_location_3, 1, None) + + # @callback + # def message_received_map_location_4(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_4:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/4", message_received_map_location_4, 1, None) + + # @callback + # def message_received_map_location_5(message): + # payload = message.payload + # self._save_log(payload, "Log map_location_5:") + # await mqtt.async_subscribe(self.hass, f"{self.entity_description.mqttTopicCommandTest}/map_location/5", message_received_map_location_5, 1, None) + \ No newline at end of file diff --git a/homeassistant/config/custom_components/polaris/water_heater.py b/homeassistant/config/custom_components/polaris/water_heater.py new file mode 100644 index 0000000..b3ac8cc --- /dev/null +++ b/homeassistant/config/custom_components/polaris/water_heater.py @@ -0,0 +1,244 @@ +"""The Polaris IQ Home component.""" +from __future__ import annotations + +import json +import re +import logging +from typing import Iterable +import copy + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.components.water_heater import DOMAIN, WaterHeaterEntity, WaterHeaterEntityFeature +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.util import slugify +from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import PolarisBaseEntity +# Import global values. +from .const import ( + MANUFACTURER, + MQTT_ROOT_TOPIC, + DEVICEID, + DEVICETYPE, + POLARIS_DEVICE, + WATER_HEATERS, + WATER_BOILERS, + PolarisWaterHeaterEntityDescription, + POLARIS_KETTLE_TYPE, + POLARIS_KETTLE_WITH_WEIGHT_TYPE, + POLARIS_BOILER_TYPE, + KETTLE_WITH_TEA_TIME_MODES, + KETTLE_WITH_KEEP_WITH_WARM_MODES, + POLARIS_KETTLE_WITH_TEA_TIME_MODE_TYPE, + POLARIS_KETTLE_WITH_KEEP_WITH_WARM_MODE_TYPE, +) + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + integrationUniqueID = config.unique_id + mqtt_root = config.data[MQTT_ROOT_TOPIC] + device_id = config.data["DEVICEID"] + device_type = config.data[DEVICETYPE] + device_prefix_topic = config.data["DEVPREFIXTOPIC"] + waterheaterList = [] + + if (device_type in POLARIS_KETTLE_TYPE) or (device_type in POLARIS_KETTLE_WITH_WEIGHT_TYPE): + # Create water heater for kettle devices + WATER_HEATERS_LC = copy.deepcopy(WATER_HEATERS) + for description in WATER_HEATERS_LC: + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicTargetTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicTargetTemperature}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.device_prefix_topic = device_prefix_topic + waterheaterList.append( + PolarisWaterHeater( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + if (device_type in POLARIS_BOILER_TYPE): + # Create water boiler + WATER_BOILERS_LC = copy.deepcopy(WATER_BOILERS) + for description in WATER_BOILERS_LC: + description.mqttTopicCommandTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandTemperature}" + description.mqttTopicCurrentTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentTemperature}" + description.mqttTopicTargetTemperature = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicTargetTemperature}" + description.mqttTopicCommandMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCommandMode}" + description.mqttTopicCurrentMode = f"{mqtt_root}/{device_prefix_topic}/{description.mqttTopicCurrentMode}" + description.device_prefix_topic = device_prefix_topic + waterheaterList.append( + PolarisWaterHeater( + description=description, + device_friendly_name=device_id, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id + ) + ) + async_add_entities(waterheaterList, update_before_add=True) + +class PolarisWaterHeater(PolarisBaseEntity, WaterHeaterEntity): + entity_description: PolarisWaterHeaterEntityDescription + def __init__( + self, + device_friendly_name: str, + description: PolarisWaterHeaterEntityDescription, + mqtt_root: str, + device_id: str | None=None, + device_type: str | None=None, + ) -> None: + super().__init__( + device_friendly_name=device_friendly_name, + mqtt_root=mqtt_root, + device_type=device_type, + device_id=device_id, + ) + self.entity_description = description + self._attr_unique_id = slugify(f"{device_id}_{description.name}") + self.entity_id = f"{DOMAIN}.{POLARIS_DEVICE[int(device_type)]['class']}_{POLARIS_DEVICE[int(device_type)]['model']}_{description.name}" + self.payload_on = description.payload_on + self.payload_off = description.payload_off + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + + self._attr_min_temp = description.min_temp + self._attr_max_temp = description.max_temp + self._attr_target_temperature_high = description.max_temp + self._attr_target_temperature_low = description.min_temp + self._attr_target_temperature = description.max_temp + self._attr_target_temperature_step = 1.0 + + self._attr_is_away_mode_on = None + self._attr_is_on = True + self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.ON_OFF + + self._attr_current_operation = description.mode + self._modes = {} + if (self.device_type in POLARIS_KETTLE_WITH_KEEP_WITH_WARM_MODE_TYPE): + self._modes = KETTLE_WITH_KEEP_WITH_WARM_MODES +# self._attr_operation_list = list(KETTLE_WITH_KEEP_WITH_WARM_MODES.keys()) + elif (self.device_type in POLARIS_KETTLE_WITH_TEA_TIME_MODE_TYPE): + self._modes = KETTLE_WITH_TEA_TIME_MODES +# self._attr_operation_list = list(KETTLE_WITH_TEA_TIME_MODES.keys()) + elif (self.device_type in {"802","844"}): + self._modes = {"off": "0", "performance": "1", "electric": "2", "heat_pump": "3"} + else: + self._modes = description.operation_list + self._attr_operation_list = list(self._modes.keys()) + + #self.entity_picture = "https://images.cdn.polaris-iot.com/a/8c/aad08-4d13-489c-9b0f-028486297ac1/60.webp" + self._attr_has_entity_name = True + self._attr_available = False + +# _attr_precision: float +# _attr_state: None = None + + + + async def async_added_to_hass(self): + @callback + def message_received_temp(message): + self._attr_current_temperature = float(message.payload) + self.async_write_ha_state() + @callback + def message_received_mode(message): + self._attr_current_operation = list(self._modes.keys())[list(self._modes.values()).index(message.payload)] + self.async_write_ha_state() + @callback + def message_received_targtemp(message): + if float(message.payload) < self._attr_min_temp: + self._attr_target_temperature = self._attr_min_temp + else: + self._attr_target_temperature = float(message.payload) + self.async_write_ha_state() + + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentTemperature, + message_received_temp, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicCurrentMode, + message_received_mode, + 1, + ) + await mqtt.async_subscribe( + self.hass, + self.entity_description.mqttTopicTargetTemperature, + message_received_targtemp, + 1, + ) + @callback + async def entity_availability(message): + if self.entity_description.name != "available": + if str(message.payload).lower() in ("1", "true"): + self._attr_available = False + else: + self._attr_available = True + self.async_write_ha_state() + + await mqtt.async_subscribe(self.hass, f"{self.mqtt_root}/{self.entity_description.device_prefix_topic}/state/error/connection", entity_availability, 1) + + + def async_turn_on(self, **kwargs): + self._attr_is_on = self.payload_on + self.publishToMQTT() + + def async_turn_off(self, **kwargs): + self._attr_is_on = self.payload_off + self.publishToMQTT() + + def publishToMQTT(self): + topic = f"{self.entity_description.mqttTopicCommandMode}" + mqtt.publish(self.hass, topic, str(self._attr_is_on)) + + def set_operation_mode(self, operation_mode): + topic = f"{self.entity_description.mqttTopicCommandTemperature}" + mqtt.publish(self.hass, topic, int(self._attr_target_temperature)) + topic = f"{self.entity_description.mqttTopicCommandMode}" + self._attr_current_operation = operation_mode + mqtt.publish(self.hass, topic, int(self._modes[operation_mode])) + self.schedule_update_ha_state() + + def set_temperature(self, **kwargs): + self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) + topic = f"{self.entity_description.mqttTopicCommandTemperature}" + mqtt.publish(self.hass, topic, int(self._attr_target_temperature)) + self.schedule_update_ha_state() + + @property + def state(self) -> str | None: + return self.current_operation + + @property + def operation_list(self) -> list[str] | None: + return self._attr_operation_list + + @property + def current_operation(self) -> str | None: + return self._attr_current_operation + + @property + def supported_features(self) -> WaterHeaterEntityFeature: + return self._attr_supported_features + + @property + def min_temp(self) -> float: + return self._attr_min_temp + + @property + def max_temp(self) -> float: + return self._attr_max_temp diff --git a/homeassistant/config/custom_components/prometheus_sensor/__init__.py b/homeassistant/config/custom_components/prometheus_sensor/__init__.py new file mode 100644 index 0000000..a2c7f1e --- /dev/null +++ b/homeassistant/config/custom_components/prometheus_sensor/__init__.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +import logging +from typing import Final, Optional +from urllib.parse import urljoin + +import aiohttp + +from homeassistant.const import STATE_PROBLEM, STATE_UNKNOWN + +_LOGGER: Final = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class QueryResult: + value: Optional[float] = None + error: Optional[str] = None + + +class Prometheus: + """Wrapper for Prometheus API Requests.""" + + def __init__(self, url: str, session: aiohttp.ClientSession) -> None: + """Initialize the Prometheus API wrapper.""" + self._session = session + self._url = urljoin(f"{url}/", "api/v1/query") + + async def query(self, expr: str) -> QueryResult: + """Query expression response.""" + try: + response = await self._session.get(self._url, params={"query": expr}) + except aiohttp.ClientError as error: + _LOGGER.error("Error querying %s: %s", self._url, error) + return QueryResult(error=STATE_PROBLEM) + + if response.status != 200: + _LOGGER.error( + "Unexpected HTTP status code %s for expression '%s'", + response.status, + expr, + ) + return QueryResult(error=STATE_UNKNOWN) + + try: + result = (await response.json())["data"]["result"] + except (ValueError, KeyError) as error: + _LOGGER.error("Invalid query response: %s", error) + return QueryResult(error=STATE_UNKNOWN) + + if not result: + _LOGGER.error("Expression '%s' yielded no result", expr) + return QueryResult(error=STATE_PROBLEM) + elif len(result) > 1: + _LOGGER.error("Expression '%s' yielded multiple metrics", expr) + return QueryResult(error=STATE_PROBLEM) + + value = float(result[0]["value"][1]) + + _LOGGER.debug("Expression '%s' yields result %f", expr, value) + + return QueryResult(value) diff --git a/homeassistant/config/custom_components/prometheus_sensor/binary_sensor.py b/homeassistant/config/custom_components/prometheus_sensor/binary_sensor.py new file mode 100644 index 0000000..a0cfb00 --- /dev/null +++ b/homeassistant/config/custom_components/prometheus_sensor/binary_sensor.py @@ -0,0 +1,125 @@ +"""Prometheus Binary Sensor component.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_URL, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from . import Prometheus + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.template import Template + from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + from . import QueryResult + +from .const import CONF_EXPR, CONF_QUERIES, DEFAULT_URL, SCAN_INTERVAL as SCAN_INTERVAL + +_QUERY_SCHEMA: Final = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(CONF_EXPR): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS): vol.Coerce(BinarySensorDeviceClass), + } +) + +PLATFORM_SCHEMA: Final = BINARY_SENSOR_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, + vol.Required(CONF_QUERIES): [_QUERY_SCHEMA], + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +): + """Set up the sensor platform.""" + session = async_get_clientsession(hass) + url = config[CONF_URL] + prometheus = Prometheus(url, session) + + async_add_entities( + new_entities=[ + PrometheusBinarySensor( + prometheus=prometheus, + unique_id=query.get(CONF_UNIQUE_ID), + device_name=query[CONF_NAME], + expression=query[CONF_EXPR], + value_template=query.get(CONF_VALUE_TEMPLATE), + device_class=query.get(CONF_DEVICE_CLASS), + ) + for query in config[CONF_QUERIES] + ], + update_before_add=True, + ) + + +class PrometheusBinarySensor(BinarySensorEntity): + """Sensor entity representing the result of a PromQL expression.""" + + def __init__( + self, + *, + prometheus: Prometheus, + unique_id: str | None, + device_name: str, + expression: str, + value_template: Template, + device_class: BinarySensorDeviceClass | None, + ) -> None: + """Initialize the sensor.""" + self._prometheus: Prometheus = prometheus + self._expression = expression + self._value_template = value_template + + self._attr_device_class = device_class + self._attr_name = device_name + self._attr_unique_id = unique_id + + async def async_update(self) -> None: + """Update state by executing query.""" + result: QueryResult = await self._prometheus.query(self._expression) + self._attr_available = result.error is None + + # Nuke value if sensor becomes unavailable + if not self._attr_available: + self._attr_is_on = None + + # Naive bool cast without template + elif self._value_template is None: + self._attr_is_on = bool(result.value) + + # Evaluate template + else: + render_result = self._value_template.async_render( + variables=dict(value=result.value) + ) + + if render_result is not None: + self._attr_is_on = bool(render_result) + else: + self._attr_is_on = None diff --git a/homeassistant/config/custom_components/prometheus_sensor/const.py b/homeassistant/config/custom_components/prometheus_sensor/const.py new file mode 100644 index 0000000..e550b44 --- /dev/null +++ b/homeassistant/config/custom_components/prometheus_sensor/const.py @@ -0,0 +1,10 @@ +from datetime import timedelta +from typing import Final + +# Match the default scrape_interval in Prometheus +SCAN_INTERVAL: Final = timedelta(seconds=15) + +DEFAULT_URL: Final = "http://localhost:9090" + +CONF_QUERIES: Final = "queries" +CONF_EXPR: Final = "expr" diff --git a/homeassistant/config/custom_components/prometheus_sensor/manifest.json b/homeassistant/config/custom_components/prometheus_sensor/manifest.json new file mode 100644 index 0000000..1961e8d --- /dev/null +++ b/homeassistant/config/custom_components/prometheus_sensor/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "prometheus_sensor", + "name": "Prometheus Sensor", + "codeowners": ["@mweinelt"], + "documentation": "https://github.com/mweinelt/ha-prometheus-sensor", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/mweinelt/ha-prometheus-sensor/issues", + "version": "1.2.1" +} diff --git a/homeassistant/config/custom_components/prometheus_sensor/sensor.py b/homeassistant/config/custom_components/prometheus_sensor/sensor.py new file mode 100644 index 0000000..4a9bdba --- /dev/null +++ b/homeassistant/config/custom_components/prometheus_sensor/sensor.py @@ -0,0 +1,112 @@ +"""Prometheus Sensor component.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +import voluptuous as vol + +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_URL, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from . import Prometheus + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + from . import QueryResult + +from .const import CONF_EXPR, CONF_QUERIES, DEFAULT_URL, SCAN_INTERVAL as SCAN_INTERVAL + +_QUERY_SCHEMA: Final = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Required(CONF_EXPR): cv.string, + vol.Optional(CONF_DEVICE_CLASS): vol.Coerce(SensorDeviceClass), + vol.Optional(CONF_STATE_CLASS): vol.Coerce(SensorStateClass), + } +) + +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, + vol.Required(CONF_QUERIES): [_QUERY_SCHEMA], + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +): + """Set up the sensor platform.""" + session = async_get_clientsession(hass) + url = config[CONF_URL] + prometheus = Prometheus(url, session) + + async_add_entities( + new_entities=[ + PrometheusSensor( + prometheus=prometheus, + expression=query[CONF_EXPR], + unique_id=query.get(CONF_UNIQUE_ID), + device_name=query[CONF_NAME], + device_class=query.get(CONF_DEVICE_CLASS), + state_class=query.get(CONF_STATE_CLASS), + unit_of_measurement=query.get(CONF_UNIT_OF_MEASUREMENT), + ) + for query in config[CONF_QUERIES] + ], + update_before_add=True, + ) + + +class PrometheusSensor(SensorEntity): + """Sensor entity representing the result of a PromQL expression.""" + + def __init__( + self, + *, + prometheus: Prometheus, + expression: str, + unique_id: str | None, + device_name: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit_of_measurement: str | None, + ) -> None: + """Initialize the sensor.""" + self._prometheus: Prometheus = prometheus + self._expression = expression + + self._attr_device_class = device_class + self._attr_name = device_name + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_state_class = state_class + self._attr_unique_id = unique_id + + async def async_update(self) -> None: + """Update state by executing query.""" + result: QueryResult = await self._prometheus.query(self._expression) + self._attr_available = result.error is None + self._attr_native_value = result.value diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/.gitignore b/homeassistant/config/custom_components/ui_lovelace_minimalist/.gitignore new file mode 100644 index 0000000..08066e1 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/.gitignore @@ -0,0 +1,6 @@ +# Folder used for combining card templates +# - from HACS +# - from /config/ui_lovelace_minimalist/cards +# - Setting correct language file +__ui_minimalist__/ +.installed diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/__init__.py b/homeassistant/config/custom_components/ui_lovelace_minimalist/__init__.py new file mode 100644 index 0000000..ba3d6bf --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/__init__.py @@ -0,0 +1,215 @@ +"""Custom Integration to setup UI Lovelace Minimalist.""" + +from __future__ import annotations + +import logging +from pathlib import Path +import shutil +from typing import TYPE_CHECKING, Any, cast + +from aiogithubapi import AIOGitHubAPIException, GitHubAPI, GitHubClientKwarg +from homeassistant.components.frontend import async_remove_panel +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.loader import async_get_integration +import voluptuous as vol + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + +from .base import UlmBase +from .const import DOMAIN, NAME +from .enums import ConfigurationType, UlmDisabledReason + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_initialize_integration( + hass: HomeAssistant, + *, + config_entry: ConfigEntry | None = None, + config: dict[str, Any] | None = None, +) -> bool: + """Initialize the integration.""" + hass.data[DOMAIN] = ulm = UlmBase() + ulm.enable_ulm() + + if config is not None: + if DOMAIN not in config: + return True + if ulm.configuration.config_type == ConfigurationType.CONFIG_ENTRY: + return True + ulm.configuration.update_from_dict( + { + "config_type": ConfigurationType.YAML, + **config[DOMAIN], + "config": config[DOMAIN], + } + ) + + if config_entry is not None: + if config_entry.source == SOURCE_IMPORT: + # not sure about this one + hass.async_create_task( + hass.config_entries.async_remove(config_entry.entry_id) + ) + return False + + ulm.configuration.update_from_dict( + { + "config_entry": config_entry, + "config_type": ConfigurationType.CONFIG_ENTRY, + **config_entry.data, + **config_entry.options, + } + ) + + integration = await async_get_integration(hass, DOMAIN) + + clientsession = async_get_clientsession(hass) + + ulm.integration = integration + ulm.version = integration.version + ulm.hass = hass + ulm.system.running = True + ulm.githubapi = GitHubAPI( + token=ulm.configuration.token, + session=clientsession, + **cast("dict[GitHubClientKwarg, Any]", {"client_name": "ULM"}), + ) + + async def async_startup() -> bool: + """ULM Startup tasks.""" + if ( + ulm.configuration.community_cards_enabled + and ulm.configuration.token is None + ): + ulm.disable_ulm(UlmDisabledReason.INVALID_TOKEN) + ulm.log.error( + "Github token is not set up yet, please reconfigure the integration." + ) + return False + if ulm.configuration.community_cards_enabled: + await ulm.fetch_cards() + await ulm.configure_community_cards() + + response_configure = await ulm.configure_ulm() + response_plugins = await ulm.configure_plugins() + response_dashboard = await ulm.configure_dashboard() + if not response_configure or not response_plugins or not response_dashboard: + return False + + ulm.enable_ulm() + + return not ulm.system.disabled + + try: + startup_result = await async_startup() + except AIOGitHubAPIException: + startup_result = False + if not startup_result: + return False + + ulm.enable_ulm() + + return True + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up this integration using UI.""" + return await async_initialize_integration(hass=hass, config=config) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + config_entry.async_on_unload( + config_entry.add_update_listener(config_entry_update_listener) + ) + return await async_initialize_integration(hass=hass, config_entry=config_entry) + + +async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Remove Integration.""" + _LOGGER.debug("%s is now uninstalled", NAME) + + # Remove the Frontend Panels + async_remove_panel(hass, "ui-lovelace-minimalist") + + dashboard_url = "ui-lovelace-minimalist" + is_registered = dashboard_url in hass.data.get("frontend_panels", {}) + sidepanel_enabled = config_entry.options.get("sidepanel_enabled", False) + + # Check config entry options and if sidepanel is enabled + if is_registered or sidepanel_enabled: + _LOGGER.debug("Removing Minimalist dashboard panel: %s", dashboard_url) + + # Remove minimalist panel from the sidebar + async_remove_panel(hass, dashboard_url) + + # Clean up the Lovelace YAML object if it exists in memory + if ( + "lovelace" in hass.data + and dashboard_url in hass.data["lovelace"].dashboards + ): + hass.data["lovelace"].dashboards.pop(dashboard_url) + + adaptive_url = "adaptive-dash" + is_registered = adaptive_url in hass.data.get("frontend_panels", {}) + adaptive_enabled = config_entry.options.get("adaptive_ui_enabled", False) + + # Check config entry options and if sidepanel is enabled + if is_registered or adaptive_enabled: + _LOGGER.debug("Removing Minimalist adaptive panel: %s", adaptive_url) + + # Remove adaptive panel from the sidebar + async_remove_panel(hass, adaptive_url) + + # Clean up the Lovelace YAML object if it exists in memory + if "lovelace" in hass.data and adaptive_url in hass.data["lovelace"].dashboards: + hass.data["lovelace"].dashboards.pop(adaptive_url) + + # Identify theme and blueprint paths for cleanup + theme_path = config_entry.options.get("theme_path", "themes") + + paths_to_remove = [ + Path(hass.config.path(theme_path)) / "minimalist-desktop", + Path(hass.config.path(theme_path)) / "minimalist-mobile", + Path(hass.config.path(theme_path)) / "minimalist-ios-tapbar", + Path(hass.config.path(theme_path)) / "minimalist-mobile-tapbar", + Path( + hass.config.path( + "custom_components/ui_lovelace_minimalist/__ui_minimalist__" + ) + ), + ] + + def _cleanup_files(paths: list[Path]): + """Sync cleanup task for the executor.""" + + for path in paths: + if path.exists(): + if path.is_dir(): + shutil.rmtree(path, ignore_errors=True) + else: + path.unlink(missing_ok=True) + + # Run cleanup in executor + await hass.async_add_executor_job(_cleanup_files, paths_to_remove) + + +async def config_entry_update_listener( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Update listener, called when the config entry options are changed.""" + _LOGGER.debug("Start config_entry_update async_reload") + + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Integration.""" + _LOGGER.debug("Unload the config entry") + + return True diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/base.py b/homeassistant/config/custom_components/ui_lovelace_minimalist/base.py new file mode 100644 index 0000000..ab8be40 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/base.py @@ -0,0 +1,611 @@ +"""Base UI Lovelace Minimalist class.""" + +from __future__ import annotations + +import asyncio +from dataclasses import asdict, dataclass, field +import logging +from pathlib import Path +import shutil +from typing import TYPE_CHECKING, Any + +from aiogithubapi import ( + GitHubAPI, + GitHubAuthenticationException, + GitHubContentsModel, + GitHubException, + GitHubNotModifiedException, + GitHubRatelimitException, +) +from homeassistant.components.frontend import add_extra_js_url, async_remove_panel +from homeassistant.components.http import StaticPathConfig +from homeassistant.components.lovelace import _register_panel +from homeassistant.components.lovelace.dashboard import LovelaceYAML +from homeassistant.config_entries import ConfigEntry, ConfigEntryState + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + from homeassistant.core import HomeAssistant + from homeassistant.loader import Integration + +from .const import ( + COMMUNITY_CARDS_FOLDER, + DEFAULT_COMMUNITY_CARDS_ENABLED, + DEFAULT_INCLUDE_OTHER_CARDS, + DEFAULT_LANGUAGE, + DEFAULT_SIDEPANEL_ENABLED, + DEFAULT_SIDEPANEL_ICON, + DEFAULT_SIDEPANEL_TITLE, + DEFAULT_THEME, + DEFAULT_THEME_PATH, + DOMAIN, + GITHUB_REPO, + LANGUAGES, + TV, +) +from .enums import ConfigurationType, UlmDisabledReason +from .utils.decode import decode_content + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class MinimalistException(Exception): + """Base exception for UI Lovelace Minimalist.""" + + +class InvalidConfigurationError(MinimalistException): + """Raised when the configuration is not a dictionary.""" + + +@dataclass +class UlmSystem: + """ULM System info.""" + + disabled_reason: UlmDisabledReason | None = None + running: bool = False + + @property + def disabled(self) -> bool: + """Return if ULM is disabled.""" + return self.disabled_reason is not None + + +@dataclass +class UlmConfiguration: + """UlmConfiguration class.""" + + config: dict[str, Any] = field(default_factory=dict) + config_entry: ConfigEntry | None = None + config_type: ConfigurationType | None = None + sidepanel_enabled: bool = DEFAULT_SIDEPANEL_ENABLED + sidepanel_icon: str = DEFAULT_SIDEPANEL_ICON + sidepanel_title: str = DEFAULT_SIDEPANEL_TITLE + adaptive_ui_enabled: bool = DEFAULT_SIDEPANEL_ENABLED + adaptive_ui_icon: str = DEFAULT_SIDEPANEL_ICON + adaptive_ui_title: str = DEFAULT_SIDEPANEL_TITLE + theme_path: str = DEFAULT_THEME_PATH + theme: str = DEFAULT_THEME + plugin_path: str = "www/community/" + include_other_cards: bool = DEFAULT_INCLUDE_OTHER_CARDS + language: str = DEFAULT_LANGUAGE + community_cards_enabled = bool = DEFAULT_COMMUNITY_CARDS_ENABLED + community_cards: list = field(default_factory=list) + all_community_cards: list = field(default_factory=list) + token: str = "" + + def to_dict(self) -> dict: + """Return Dict.""" + return self.__dict__ + + def to_json(self) -> str: + """Return a json string.""" + return str(asdict(self)) + + def update_from_dict(self, data: dict) -> None: + """Set attributes from dicts.""" + if not isinstance(data, dict): + raise InvalidConfigurationError("Configuration is not valid.") + + for key, value in data.items(): + self.__setattr__(key, value) + + +class UlmBase: + """Base UI Lovelace Minimalist.""" + + integration: Integration | None = None + configuration = UlmConfiguration() + hass: HomeAssistant | None = None + log: logging.Logger = _LOGGER + githubapi: GitHubAPI | None = None + system = UlmSystem() + version: str | None = None + + @property + def integration_dir(self) -> Path: + """Return the ULM integration dir.""" + return self.integration.file_path + + @property + def templates_dir(self) -> Path: + """Return the Button Cards Template dir.""" + return Path(f"{self.integration_dir}/__ui_minimalist__/ulm_templates") + + @property + def community_cards_dir(self) -> Path: + """Return the Comminty cards dir inside Template dir.""" + return Path(f"{self.templates_dir}/community_cards") + + def disable_ulm(self, reason: UlmDisabledReason) -> None: + """Disable Ulm.""" + if self.system.disabled_reason == reason: + return + + self.system.disabled_reason = reason + if reason == UlmDisabledReason.INVALID_TOKEN: + self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR + self.configuration.config_entry.reason = "Authentiation Failed" + self.hass.add_job( + self.configuration.config_entry.async_start_reauth, self.hass + ) + + def enable_ulm(self) -> None: + """Enable Ulm.""" + if self.system.disabled_reason is not None: + self.system.disabled_reason = None + self.log.info("ULM is enabled") + + async def async_save_file(self, file_path: str, content: Any) -> bool: + """Save a file.""" + self.log.debug("Saving file: %s", file_path) + + def _write_file() -> bool: + path = Path(file_path) + try: + path.parent.mkdir(parents=True, exist_ok=True) + if isinstance(content, str): + path.write_text(content, encoding="utf-8", errors="ignore") + else: + path.write_bytes(content) + return path.exists() + + except OSError: + self.log.exception("Could not write data to %s", file_path) + return False + + return await self.hass.async_add_executor_job(_write_file) + + async def async_github_get_file(self, filename: str) -> str: + """Get the content of a file.""" + self.log.debug("Fetching github file: %s", filename) + response = await self.async_github_api_method( + method=self.githubapi.repos.contents.get, + repository=GITHUB_REPO, + path=filename, + ) + if response and hasattr(response, "data"): + if isinstance(response.data, GitHubContentsModel) and response.data.content: + return decode_content(response.data.content) + return "" + + async def async_github_get_tree(self, path: str) -> list[GitHubContentsModel]: + """Get the content of a directory.""" + self.log.debug("Fetching github tree: %s", path) + response = await self.async_github_api_method( + method=self.githubapi.repos.contents.get, repository=GITHUB_REPO, path=path + ) + if response and hasattr(response, "data"): + if isinstance(response.data, list) and response.data: + return response.data + return [] + + async def async_github_api_method( + self, + method: Callable[[], Awaitable[TV]], + *args, + raise_exception: bool = True, + **kwargs, + ) -> TV | None: + """Call a GitHub API method.""" + _exception = None + + try: + return await method(*args, **kwargs) + except GitHubAuthenticationException as exception: + self.disable_ulm(UlmDisabledReason.INVALID_TOKEN) + _exception = exception + except GitHubRatelimitException as exception: + _exception = exception + except GitHubNotModifiedException: + raise + except GitHubException as exception: + _exception = exception + except MinimalistException as exception: + _exception = exception + + if raise_exception and _exception is not None: + raise MinimalistException(_exception) + return None + + def list_dirs(self) -> list[Path]: + """Return a list of directory Path objects.""" + self.log.debug("Listing directories in %s", self.community_cards_dir) + + if not self.community_cards_dir.is_dir(): + return [] + + return [path for path in self.community_cards_dir.iterdir() if path.is_dir()] + + async def fetch_cards(self) -> None: + """Fetch list of cards.""" + response = await self.async_github_api_method( + method=self.githubapi.repos.contents.get, + repository=GITHUB_REPO, + path=COMMUNITY_CARDS_FOLDER, + ) + if response and hasattr(response, "data"): + if isinstance(response.data, list) and response.data: + self.configuration.all_community_cards = [ + c.name for c in response.data if c.type == "dir" + ] + + async def download_and_save(self, github_path, local_path): + """Download and save selected community cards.""" + content = await self.async_github_get_file(filename=github_path) + await self.async_save_file(file_path=str(local_path), content=content) + + async def configure_community_cards(self) -> None: + """Configure selected community cards.""" + self.log.info("Configuring selected community cards") + + # Handle full cleanup if disabled or no cards selected + if ( + not self.configuration.community_cards_enabled + or self.configuration.community_cards == [] + ): + if self.community_cards_dir.exists(): + await self.hass.async_add_executor_job( + shutil.rmtree, str(self.community_cards_dir), True + ) + return + + # Ensure base directory exists for next steps + self.community_cards_dir.mkdir(parents=True, exist_ok=True) + language = LANGUAGES[self.configuration.language] + + # Identify folders to delete (Unselected or missing from GitHub) + existing_dirs = await self.hass.async_add_executor_job(self.list_dirs) + all_github_cards = self.configuration.all_community_cards + + delete_tasks = [] + for path_str in existing_dirs: + path = Path(path_str) + card_name = path.name + + if card_name not in self.configuration.community_cards: + self.log.debug( + "Deleting community card folder %s, not selected anymore.", + card_name, + ) + delete_tasks.append(path) + elif card_name not in all_github_cards: + self.log.debug( + "Deleting community card folder %s, that is not existing anymore on Github.", + card_name, + ) + delete_tasks.append(path) + + # Batch delete unneeded folders to minimize executor overhead + if delete_tasks: + + def _batch_delete(paths): + for p in paths: + shutil.rmtree(str(p), ignore_errors=True) + + await self.hass.async_add_executor_job(_batch_delete, delete_tasks) + + # Download selected cards + if self.configuration.community_cards_enabled: + for card in self.configuration.community_cards: + if card not in self.configuration.all_community_cards: + self.configuration.community_cards.remove(card) + else: + card_files = await self.async_github_get_tree( + path=f"{COMMUNITY_CARDS_FOLDER}/{card}" + ) + download_tasks = [] + for f in card_files: + if f.type == "file": + target_path: Path = self.community_cards_dir / card / f.name + + # Pathlib check for existence and size + if ( + not target_path.exists() + or target_path.stat().st_size != f.size + ): + download_tasks.append( + self.download_and_save(f.path, target_path) + ) + + elif f.type == "dir" and f.name == "languages": + language_files = await self.async_github_get_tree( + path=f.path + ) + + for lang in language_files: + # Only download if the stem matches the target language + if Path(lang.name).stem == language: + target_path: Path = ( + self.community_cards_dir + / card + / "languages" + / lang.name + ) + if ( + not target_path.exists() + or target_path.stat().st_size != lang.size + ): + download_tasks.append( + self.download_and_save( + lang.path, target_path + ) + ) + + # Execute all downloads concurrently + if download_tasks: + await asyncio.gather(*download_tasks) + + async def configure_plugins(self) -> bool: + """Configure the Plugins ULM depends on.""" + self.log.debug("Checking Dependencies.") + self.log.info("Setup ULM Plugins") + + try: + browser_mod_path = Path( + self.hass.config.path("custom_components/browser_mod") + ) + if not browser_mod_path.exists(): + self.log.error('HACS Integration repo "Browser Mod" is not installed.') + + depenceny_resource_paths = [ + "button-card", + "light-entity-card", + "lovelace-card-mod", + "lovelace-auto-entities", + "mini-graph-card", + "mini-media-player", + "my-cards", + "simple-weather-card", + "lovelace-layout-card", + "lovelace-state-switch", + "weather-radar-card", + ] + for p in depenceny_resource_paths: + frontend_repo_path = Path(self.hass.config.path(f"www/community/{p}")) + if not self.configuration.include_other_cards: + if not frontend_repo_path.exists(): + self.log.error( + 'HACS Frontend repo "%s" is not installed, ' + "See Integration Configuration.", + p, + ) + elif frontend_repo_path.exists(): + self.log.error( + 'HACS Frontend repo "%s" is already installed, ' + "Remove it or disable include custom cards.", + p, + ) + + if self.configuration.include_other_cards: + for c in depenceny_resource_paths: + add_extra_js_url( + self.hass, f"/ui_lovelace_minimalist/cards/{c}/{c}.js" + ) + + # Register + await self.hass.http.async_register_static_paths( + [ + StaticPathConfig( + "/ui_lovelace_minimalist/cards", + self.hass.config.path(f"{self.integration_dir}/cards"), + True, + ) + ] + ) + + except MinimalistException as exception: + self.log.error(exception) + self.disable_ulm(UlmDisabledReason.LOAD_ULM) + return False + + return True + + async def configure_dashboard(self) -> bool: + """Configure the ULM Dashboards.""" + self.log.info("Setup ULM Dashboard") + + dashboard_url = "ui-lovelace-minimalist" + dashboard_config = { + "mode": "yaml", + "icon": self.configuration.sidepanel_icon, + "title": self.configuration.sidepanel_title, + "filename": "ui_lovelace_minimalist/dashboard/ui-lovelace.yaml", + "show_in_sidebar": True, + "require_admin": False, + } + + adv_dashboard_url = "adaptive-dash" + adv_dashboard_config = { + "mode": "yaml", + "icon": self.configuration.adaptive_ui_icon, + "title": self.configuration.adaptive_ui_title, + "filename": "ui_lovelace_minimalist/dashboard/adaptive-dash/adaptive-ui.yaml", + "show_in_sidebar": True, + "require_admin": False, + } + # Optoinal override can be done with config_flow? + # if not dashboard_url in hass.data["lovelace"].dashboards: + try: + if self.configuration.sidepanel_enabled: + self.hass.data["lovelace"].dashboards[dashboard_url] = LovelaceYAML( + self.hass, dashboard_url, dashboard_config + ) + + _register_panel( + self.hass, dashboard_url, "yaml", dashboard_config, True + ) + elif dashboard_url in self.hass.data["lovelace"].dashboards: + async_remove_panel(self.hass, "ui-lovelace-minimalist") + + if self.configuration.adaptive_ui_enabled: + self.hass.data["lovelace"].dashboards[adv_dashboard_url] = LovelaceYAML( + self.hass, adv_dashboard_url, adv_dashboard_config + ) + + _register_panel( + self.hass, adv_dashboard_url, "yaml", adv_dashboard_config, True + ) + elif adv_dashboard_url in self.hass.data["lovelace"].dashboards: + async_remove_panel(self.hass, "adaptive-dash") + + except MinimalistException as exception: + self.log.error(exception) + self.disable_ulm(UlmDisabledReason.LOAD_ULM) + return False + + return True + + async def configure_ulm(self) -> bool: + """Configure initial dashboard & cards directory.""" + self.log.info("Setup ULM Configuration") + + # Define Path objects + base_dir = Path(self.hass.config.path(DOMAIN)) + integration_lovelace = Path(self.integration_dir) / "lovelace" + dashboard_file = base_dir / "dashboard" / "ui-lovelace.yaml" + adaptive_dir = base_dir / "dashboard" / "adaptive-dash" + actions_file = base_dir / "custom_actions" / "custom_actions.yaml" + + def _sync_file_operations(): + """Grouped synchronous I/O to run in one executor job.""" + # Cleanup legacy folders + for folder in ["configs", "addons"]: + shutil.rmtree(base_dir / folder, ignore_errors=True) + + # Create necessary directories + for folder in ["dashboard", "custom_cards", "custom_actions"]: + (base_dir / folder).mkdir(parents=True, exist_ok=True) + + # Proceed if dashboard dir exists (it should, we just created it) + if (base_dir / "dashboard").exists(): + self.templates_dir.mkdir(parents=True, exist_ok=True) + + # Translations + language = LANGUAGES[self.configuration.language] + + # Copy default language file over to config dir + shutil.copy2( + integration_lovelace / "translations" / "default.yaml", + self.templates_dir / "default.yaml", + ) + + # Copy chosen language file over to config dir + shutil.copy2( + integration_lovelace / "translations" / f"{language}.yaml", + self.templates_dir / "language.yaml", + ) + + # Copy example dashboard file over to user config dir if not exists + if self.configuration.sidepanel_enabled and not dashboard_file.exists(): + shutil.copy2( + integration_lovelace / "ui-lovelace.yaml", dashboard_file + ) + + if self.configuration.adaptive_ui_enabled and not adaptive_dir.exists(): + shutil.copytree( + integration_lovelace / "adaptive-dash", adaptive_dir + ) + + # Copy example custom actions file over to user config dir if not exists + if not actions_file.exists(): + shutil.copy2( + integration_lovelace / "custom_actions.yaml", actions_file + ) + + # Copy over cards from integration + shutil.copytree( + integration_lovelace / "ulm_templates", + self.templates_dir, + dirs_exist_ok=True, + ) + + # Copy over manually installed custom_cards from user + shutil.copytree( + base_dir / "custom_cards", + self.templates_dir / "custom_cards", + dirs_exist_ok=True, + ) + + # Copy over manually installed custom_actions from user + shutil.copytree( + base_dir / "custom_actions", + self.templates_dir / "custom_actions", + dirs_exist_ok=True, + ) + + # Copy over themes to defined themes folder + theme_target = Path( + self.hass.config.path(self.configuration.theme_path) + ) + shutil.copytree( + integration_lovelace / "themefiles", + theme_target, + dirs_exist_ok=True, + ) + + try: + # Run all disk I/O in a single block + await self.hass.async_add_executor_job(_sync_file_operations) + + # UI Reload and Service Registration + self.hass.bus.async_fire("ui_lovelace_minimalist_reload") + + async def handle_reload(call): + self.log.debug("Reload UI Lovelace Minimalist Configuration") + await self.reload_configuration() + + # Register servcie ui_lovelace_minimalist.reload + self.hass.services.async_register(DOMAIN, "reload", handle_reload) + + except MinimalistException as exception: + self.log.error(exception) + self.disable_ulm(UlmDisabledReason.LOAD_ULM) + return False + + return True + + async def reload_configuration(self): + """Reload Configuration.""" + self.log.info("Reloading ULM Configuration") + + # Define Path objects + base_path = Path(self.hass.config.path(DOMAIN)) + + def _sync_custom_folders(): + """Internal helper to group I/O operations.""" + + # Copy over manually installed custom_cards from user + folders = ["custom_cards", "custom_actions"] + for folder in folders: + source = base_path / folder + if source.exists(): + shutil.copytree( + source, self.templates_dir / folder, dirs_exist_ok=True + ) + + # Run all I/O in one executor thread + await self.hass.async_add_executor_job(_sync_custom_folders) + + # Notify the system + self.hass.bus.async_fire("ui_lovelace_minimalist_reload") diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/blueprints/set_theme.yaml b/homeassistant/config/custom_components/ui_lovelace_minimalist/blueprints/set_theme.yaml new file mode 100644 index 0000000..4726dbd --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/blueprints/set_theme.yaml @@ -0,0 +1,40 @@ +--- +blueprint: + name: "System Set Default Theme" + description: >- + Set the default themes on start. + domain: "automation" + input: + theme: + name: "Which Theme" + description: "Which theme would you like to set as default on reload / start-up?" + selector: + select: + options: + - "minimalist-desktop" + - "minimalist-mobile" + - "minimalist-mobile-tapbar" + + mode: + name: "Which Mode should it be default in" + description: "Default in Dark or Light mode?" + selector: + select: + options: + # based on sun can be added later + - "light" + - "dark" + +mode: "single" + +trigger: + - platform: "homeassistant" + event: "start" + - platform: "event" + event_type: "automation_reload" + +action: + - service: "frontend.set_theme" + data: + name: !input "theme" + mode: !input "mode" diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/button-card/button-card.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/button-card/button-card.js new file mode 100644 index 0000000..037d6bd --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/button-card/button-card.js @@ -0,0 +1,56 @@ +var t,e,n,i,o,r,a,u,s,c,l,d,h,f,v,p,m,g,_,y,b,D,A,w,C,E,k,F,O,j,S,T;function B(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}function $(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function x(t,e,n){return(e=nt(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function M(t,e,n,i){var o=P(Z(1&i?t.prototype:t),e,n);return 2&i&&"function"==typeof o?function(t){return o.apply(n,t)}:o}function P(){return P="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(t,e,n){var i=function(t,e){for(;!{}.hasOwnProperty.call(t,e)&&null!==(t=Z(t)););return t}(t,e);if(i){var o=Object.getOwnPropertyDescriptor(i,e);return o.get?o.get.call(arguments.length<3?t:n):o.value}},P.apply(null,arguments)}function H(t){return function(t){if(Array.isArray(t))return J(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||X(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function I(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,o,r,a,u=[],s=!0,c=!1;try{if(r=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;s=!1}else for(;!(s=(i=r.call(n)).done)&&(u.push(i.value),u.length!==e);s=!0);}catch(t){c=!0,o=t}finally{try{if(!s&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(c)throw o}}return u}}(t,e)||X(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function R(){/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */var t,e,n="function"==typeof Symbol?Symbol:{},i=n.iterator||"@@iterator",o=n.toStringTag||"@@toStringTag";function r(n,i,o,r){var s=i&&i.prototype instanceof u?i:u,c=Object.create(s.prototype);return N(c,"_invoke",function(n,i,o){var r,u,s,c=0,l=o||[],d=!1,h={p:0,n:0,v:t,a:f,f:f.bind(t,4),d:function(e,n){return r=e,u=0,s=t,h.n=n,a}};function f(n,i){for(u=n,s=i,e=0;!d&&c&&!o&&e3?(o=v===i)&&(s=r[(u=r[4])?5:(u=3,3)],r[4]=r[5]=t):r[0]<=f&&((o=n<2&&fi||i>v)&&(r[4]=n,r[5]=i,h.n=v,u=0))}if(o||n>1)return a;throw d=!0,i}return function(o,l,v){if(c>1)throw TypeError("Generator is already running");for(d&&1===l&&f(l,v),u=l,s=v;(e=u<2?t:s)||!d;){r||(u?u<3?(u>1&&(h.n=-1),f(u,s)):h.n=s:h.v=s);try{if(c=2,r){if(u||(o="next"),e=r[o]){if(!(e=e.call(r,s)))throw TypeError("iterator result is not an object");if(!e.done)return e;s=e.value,u<2&&(u=0)}else 1===u&&(e=r.return)&&e.call(r),u<2&&(s=TypeError("The iterator does not provide a '"+o+"' method"),u=1);r=t}else if((e=(d=h.n<0)?s:n.call(i,h))!==a)break}catch(e){r=t,u=1,s=e}finally{c=1}}return{value:e,done:d}}}(n,o,r),!0),c}var a={};function u(){}function s(){}function c(){}e=Object.getPrototypeOf;var l=[][i]?e(e([][i]())):(N(e={},i,(function(){return this})),e),d=c.prototype=u.prototype=Object.create(l);function h(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,c):(t.__proto__=c,N(t,o,"GeneratorFunction")),t.prototype=Object.create(d),t}return s.prototype=c,N(d,"constructor",c),N(c,"constructor",s),s.displayName="GeneratorFunction",N(c,o,"GeneratorFunction"),N(d),N(d,o,"Generator"),N(d,i,(function(){return this})),N(d,"toString",(function(){return"[object Generator]"})),(R=function(){return{w:r,m:h}})()}function N(t,e,n,i){var o=Object.defineProperty;try{o({},"",{})}catch(t){o=0}N=function(t,e,n,i){function r(e,n){N(t,e,(function(t){return this._invoke(e,n,t)}))}e?o?o(t,e,{value:n,enumerable:!i,configurable:!i,writable:!i}):t[e]=n:(r("next",0),r("throw",1),r("return",2))},N(t,e,n,i)}function V(t,e,n,i,o,r,a){try{var u=t[r](a),s=u.value}catch(t){return void n(t)}u.done?e(s):Promise.resolve(s).then(i,o)}function z(t){return function(){var e=this,n=arguments;return new Promise((function(i,o){var r=t.apply(e,n);function a(t){V(r,i,o,a,u,"next",t)}function u(t){V(r,i,o,a,u,"throw",t)}a(void 0)}))}}function L(t,e,n){return e=Z(e),function(t,e){if(e&&("object"==it(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return U(t)}(t,W()?Reflect.construct(e,n||[],Z(t).constructor):e.apply(t,n))}function U(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function G(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&Y(t,e)}function q(t){var e="function"==typeof Map?new Map:void 0;return q=function(t){if(null===t||!function(t){try{return-1!==Function.toString.call(t).indexOf("[native code]")}catch(Te){return"function"==typeof t}}(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,n)}function n(){return function(t,e,n){if(W())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,e);var o=new(t.bind.apply(t,i));return n&&Y(o,n.prototype),o}(t,arguments,Z(this).constructor)}return n.prototype=Object.create(t.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),Y(n,t)},q(t)}function W(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(W=function(){return!!t})()}function Y(t,e){return Y=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},Y(t,e)}function Z(t){return Z=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},Z(t)}function K(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=X(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0,o=function(){};return{s:o,n:function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,a=!0,u=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){u=!0,r=t},f:function(){try{a||null==n.return||n.return()}finally{if(u)throw r}}}}function X(t,e){if(t){if("string"==typeof t)return J(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?J(t,e):void 0}}function J(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=0;u--)(o=t[u])&&(a=(r<3?o(a):r>3?o(e,n,a):o(e,n))||a);return r>3&&a&&Object.defineProperty(e,n,a),a}"function"==typeof SuppressedError&&SuppressedError; +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var rt=globalThis,at=rt.ShadowRoot&&(void 0===rt.ShadyCSS||rt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ut=Symbol(),st=new WeakMap,ct=function(){return et((function t(e,n,i){if(Q(this,t),this._$cssResult$=!0,i!==ut)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=n}),[{key:"styleSheet",get:function(){var t=this.o,e=this.t;if(at&&void 0===t){var n=void 0!==e&&1===e.length;n&&(t=st.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),n&&st.set(e,t))}return t}},{key:"toString",value:function(){return this.cssText}}])}(),lt=at?function(t){return t}:function(t){return t instanceof CSSStyleSheet?function(t){var e,n="",i=K(t.cssRules);try{for(i.s();!(e=i.n()).done;){n+=e.value.cssText}}catch(o){i.e(o)}finally{i.f()}return function(t){return new ct("string"==typeof t?t:t+"",void 0,ut)}(n)}(t):t},dt=Object.is,ht=Object.defineProperty,ft=Object.getOwnPropertyDescriptor,vt=Object.getOwnPropertyNames,pt=Object.getOwnPropertySymbols,mt=Object.getPrototypeOf,gt=globalThis,_t=gt.trustedTypes,yt=_t?_t.emptyScript:"",bt=gt.reactiveElementPolyfillSupport,Dt=function(t,e){return t},At={toAttribute:function(t,e){switch(e){case Boolean:t=t?yt:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute:function(t,e){var n=t;switch(e){case Boolean:n=null!==t;break;case Number:n=null===t?null:Number(t);break;case Object:case Array:try{n=JSON.parse(t)}catch(t){n=null}}return n}},wt=function(t,e){return!dt(t,e)},Ct={attribute:!0,type:String,converter:At,reflect:!1,useDefault:!1,hasChanged:wt};null!==(t=Symbol.metadata)&&void 0!==t||(Symbol.metadata=Symbol("metadata")),null!==(e=gt.litPropertyMetadata)&&void 0!==e||(gt.litPropertyMetadata=new WeakMap);var Et=function(t){function e(){var t;return Q(this,e),(t=L(this,e))._$Ep=void 0,t.isUpdatePending=!1,t.hasUpdated=!1,t._$Em=null,t._$Ev(),t}return G(e,q(HTMLElement)),et(e,[{key:"_$Ev",value:function(){var t,e=this;this._$ES=new Promise((function(t){return e.enableUpdating=t})),this._$AL=new Map,this._$E_(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((function(t){return t(e)}))}},{key:"addController",value:function(t){var e,n;(null!==(e=this._$EO)&&void 0!==e?e:this._$EO=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&(null===(n=t.hostConnected)||void 0===n||n.call(t))}},{key:"removeController",value:function(t){var e;null===(e=this._$EO)||void 0===e||e.delete(t)}},{key:"_$E_",value:function(){var t,e=new Map,n=K(this.constructor.elementProperties.keys());try{for(n.s();!(t=n.n()).done;){var i=t.value;this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i])}}catch(o){n.e(o)}finally{n.f()}e.size>0&&(this._$Ep=e)}},{key:"createRenderRoot",value:function(){var t,e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return function(t,e){if(at)t.adoptedStyleSheets=e.map((function(t){return t instanceof CSSStyleSheet?t:t.styleSheet}));else{var n,i=K(e);try{for(i.s();!(n=i.n()).done;){var o=n.value,r=document.createElement("style"),a=rt.litNonce;void 0!==a&&r.setAttribute("nonce",a),r.textContent=o.cssText,t.appendChild(r)}}catch(u){i.e(u)}finally{i.f()}}}(e,this.constructor.elementStyles),e}},{key:"connectedCallback",value:function(){var t,e;null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$EO)||void 0===e||e.forEach((function(t){var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}},{key:"enableUpdating",value:function(t){}},{key:"disconnectedCallback",value:function(){var t;null===(t=this._$EO)||void 0===t||t.forEach((function(t){var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}},{key:"attributeChangedCallback",value:function(t,e,n){this._$AK(t,n)}},{key:"_$ET",value:function(t,e){var n=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,n);if(void 0!==i&&!0===n.reflect){var o,r=(void 0!==(null===(o=n.converter)||void 0===o?void 0:o.toAttribute)?n.converter:At).toAttribute(e,n.type);this._$Em=t,null==r?this.removeAttribute(i):this.setAttribute(i,r),this._$Em=null}}},{key:"_$AK",value:function(t,e){var n=this.constructor,i=n._$Eh.get(t);if(void 0!==i&&this._$Em!==i){var o,r,a,u=n.getPropertyOptions(i),s="function"==typeof u.converter?{fromAttribute:u.converter}:void 0!==(null===(o=u.converter)||void 0===o?void 0:o.fromAttribute)?u.converter:At;this._$Em=i;var c=s.fromAttribute(e,u.type);this[i]=null!==(r=null!=c?c:null===(a=this._$Ej)||void 0===a?void 0:a.get(i))&&void 0!==r?r:c,this._$Em=null}}},{key:"requestUpdate",value:function(t,e,n){if(void 0!==t){var i,o,r=this.constructor,a=this[t];if(null!=n||(n=r.getPropertyOptions(t)),!((null!==(i=n.hasChanged)&&void 0!==i?i:wt)(a,e)||n.useDefault&&n.reflect&&a===(null===(o=this._$Ej)||void 0===o?void 0:o.get(t))&&!this.hasAttribute(r._$Eu(t,n))))return;this.C(t,e,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}},{key:"C",value:function(t,e,n,i){var o,r,a,u=n.useDefault,s=n.reflect,c=n.wrapped;u&&!(null!==(o=this._$Ej)&&void 0!==o?o:this._$Ej=new Map).has(t)&&(this._$Ej.set(t,null!==(r=null!=i?i:e)&&void 0!==r?r:this[t]),!0!==c||void 0!==i)||(this._$AL.has(t)||(this.hasUpdated||u||(e=void 0),this._$AL.set(t,e)),!0===s&&this._$Em!==t&&(null!==(a=this._$Eq)&&void 0!==a?a:this._$Eq=new Set).add(t))}},{key:"_$EP",value:(n=z(R().m((function t(){var e,n;return R().w((function(t){for(;;)switch(t.p=t.n){case 0:return this.isUpdatePending=!0,t.p=1,t.n=2,this._$ES;case 2:t.n=4;break;case 3:t.p=3,n=t.v,Promise.reject(n);case 4:if(null==(e=this.scheduleUpdate())){t.n=5;break}return t.n=5,e;case 5:return t.a(2,!this.isUpdatePending)}}),t,this,[[1,3]])}))),function(){return n.apply(this,arguments)})},{key:"scheduleUpdate",value:function(){return this.performUpdate()}},{key:"performUpdate",value:function(){if(this.isUpdatePending){if(!this.hasUpdated){var t;if(null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this._$Ep){var e,n=K(this._$Ep);try{for(n.s();!(e=n.n()).done;){var i=I(e.value,2),o=i[0],r=i[1];this[o]=r}}catch(g){n.e(g)}finally{n.f()}this._$Ep=void 0}var a=this.constructor.elementProperties;if(a.size>0){var u,s=K(a);try{for(s.s();!(u=s.n()).done;){var c=I(u.value,2),l=c[0],d=c[1],h=d.wrapped,f=this[l];!0!==h||this._$AL.has(l)||void 0===f||this.C(l,void 0,d,f)}}catch(g){s.e(g)}finally{s.f()}}}var v=!1,p=this._$AL;try{var m;(v=this.shouldUpdate(p))?(this.willUpdate(p),null!==(m=this._$EO)&&void 0!==m&&m.forEach((function(t){var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(p)):this._$EM()}catch(p){throw v=!1,this._$EM(),p}v&&this._$AE(p)}}},{key:"willUpdate",value:function(t){}},{key:"_$AE",value:function(t){var e;null!==(e=this._$EO)&&void 0!==e&&e.forEach((function(t){var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}},{key:"_$EM",value:function(){this._$AL=new Map,this.isUpdatePending=!1}},{key:"updateComplete",get:function(){return this.getUpdateComplete()}},{key:"getUpdateComplete",value:function(){return this._$ES}},{key:"shouldUpdate",value:function(t){return!0}},{key:"update",value:function(t){var e=this;this._$Eq&&(this._$Eq=this._$Eq.forEach((function(t){return e._$ET(t,e[t])}))),this._$EM()}},{key:"updated",value:function(t){}},{key:"firstUpdated",value:function(t){}}],[{key:"addInitializer",value:function(t){var e;this._$Ei(),(null!==(e=this.l)&&void 0!==e?e:this.l=[]).push(t)}},{key:"observedAttributes",get:function(){return this.finalize(),this._$Eh&&H(this._$Eh.keys())}},{key:"createProperty",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Ct;if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){var n=Symbol(),i=this.getPropertyDescriptor(t,n,e);void 0!==i&&ht(this.prototype,t,i)}}},{key:"getPropertyDescriptor",value:function(t,e,n){var i,o=null!==(i=ft(this.prototype,t))&&void 0!==i?i:{get:function(){return this[e]},set:function(t){this[e]=t}},r=o.get,a=o.set;return{get:r,set:function(e){var i=null==r?void 0:r.call(this);null!=a&&a.call(this,e),this.requestUpdate(t,i,n)},configurable:!0,enumerable:!0}}},{key:"getPropertyOptions",value:function(t){var e;return null!==(e=this.elementProperties.get(t))&&void 0!==e?e:Ct}},{key:"_$Ei",value:function(){if(!this.hasOwnProperty(Dt("elementProperties"))){var t=mt(this);t.finalize(),void 0!==t.l&&(this.l=H(t.l)),this.elementProperties=new Map(t.elementProperties)}}},{key:"finalize",value:function(){if(!this.hasOwnProperty(Dt("finalized"))){if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(Dt("properties"))){var t,e=this.properties,n=K([].concat(H(vt(e)),H(pt(e))));try{for(n.s();!(t=n.n()).done;){var i=t.value;this.createProperty(i,e[i])}}catch(g){n.e(g)}finally{n.f()}}var o=this[Symbol.metadata];if(null!==o){var r=litPropertyMetadata.get(o);if(void 0!==r){var a,u=K(r);try{for(u.s();!(a=u.n()).done;){var s=I(a.value,2),c=s[0],l=s[1];this.elementProperties.set(c,l)}}catch(g){u.e(g)}finally{u.f()}}}this._$Eh=new Map;var d,h=K(this.elementProperties);try{for(h.s();!(d=h.n()).done;){var f=I(d.value,2),v=f[0],p=f[1],m=this._$Eu(v,p);void 0!==m&&this._$Eh.set(m,v)}}catch(g){h.e(g)}finally{h.f()}this.elementStyles=this.finalizeStyles(this.styles)}}},{key:"finalizeStyles",value:function(t){var e=[];if(Array.isArray(t)){var n,i=K(new Set(t.flat(1/0).reverse()));try{for(i.s();!(n=i.n()).done;){var o=n.value;e.unshift(lt(o))}}catch(r){i.e(r)}finally{i.f()}}else void 0!==t&&e.push(lt(t));return e}},{key:"_$Eu",value:function(t,e){var n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof t?t.toLowerCase():void 0}}]);var n}();Et.elementStyles=[],Et.shadowRootOptions={mode:"open"},Et[Dt("elementProperties")]=new Map,Et[Dt("finalized")]=new Map,null!=bt&&bt({ReactiveElement:Et}),(null!==(n=gt.reactiveElementVersions)&&void 0!==n?n:gt.reactiveElementVersions=[]).push("2.1.1"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var kt=globalThis,Ft=kt.trustedTypes,Ot=Ft?Ft.createPolicy("lit-html",{createHTML:function(t){return t}}):void 0,jt="$lit$",St="lit$".concat(Math.random().toFixed(9).slice(2),"$"),Tt="?"+St,Bt="<".concat(Tt,">"),$t=document,xt=function(){return $t.createComment("")},Mt=function(t){return null===t||"object"!=it(t)&&"function"!=typeof t},Pt=Array.isArray,Ht="[ \t\n\f\r]",It=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Rt=/-->/g,Nt=/>/g,Vt=RegExp(">|".concat(Ht,"(?:([^\\s\"'>=/]+)(").concat(Ht,"*=").concat(Ht,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),zt=/'/g,Lt=/"/g,Ut=/^(?:script|style|textarea|title)$/i,Gt=function(t){return function(e){for(var n=arguments.length,i=new Array(n>1?n-1:0),o=1;o":3===e?"":"",a=It,u=0;u"===l[0]?(a=null!=n?n:It,d=-1):void 0===l[1]?d=-2:(d=a.lastIndex-l[2].length,c=l[1],a=void 0===l[3]?Vt:'"'===l[3]?Lt:zt):a===Lt||a===zt?a=Vt:a===Rt||a===Nt?a=It:(a=Vt,n=void 0);var f=a===Vt&&t[u+1].startsWith("/>")?" ":"";r+=a===It?s+Bt:d>=0?(o.push(c),s.slice(0,d)+jt+s.slice(d)+St+f):s+St+(-2===d?u:f)}return[Kt(t,r+(t[i]||"")+(2===e?"":3===e?"":"")),o]},Jt=function(){return et((function t(e,n){var i,o=e.strings,r=e._$litType$;Q(this,t),this.parts=[];var a=0,u=0,s=o.length-1,c=this.parts,l=I(Xt(o,r),2),d=l[0],h=l[1];if(this.el=t.createElement(d,n),Zt.currentNode=this.el.content,2===r||3===r){var f=this.el.content.firstChild;f.replaceWith.apply(f,H(f.childNodes))}for(;null!==(i=Zt.nextNode())&&c.length0){i.textContent=Ft?Ft.emptyScript:"";for(var A=0;A2&&void 0!==arguments[2]?arguments[2]:t,s=arguments.length>3?arguments[3]:void 0;if(e===qt)return e;var c=void 0!==s?null===(n=u._$Co)||void 0===n?void 0:n[s]:u._$Cl,l=Mt(e)?void 0:e._$litDirective$;return(null===(i=c)||void 0===i?void 0:i.constructor)!==l&&(null!==(o=c)&&void 0!==o&&null!==(r=o._$AO)&&void 0!==r&&r.call(o,!1),void 0===l?c=void 0:(c=new l(t))._$AT(t,u,s),void 0!==s?(null!==(a=u._$Co)&&void 0!==a?a:u._$Co=[])[s]=c:u._$Cl=c),void 0!==c&&(e=Qt(t,c._$AS(t,e.values),c,s)),e}var te=function(){return et((function t(e,n){Q(this,t),this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=n}),[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(t){var e,n=this._$AD,i=n.el.content,o=n.parts,r=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:$t).importNode(i,!0);Zt.currentNode=r;for(var a=Zt.nextNode(),u=0,s=0,c=o[0];void 0!==c;){var l;if(u===c.index){var d=void 0;2===c.type?d=new ee(a,a.nextSibling,this,t):1===c.type?d=new c.ctor(a,c.name,c.strings,this,t):6===c.type&&(d=new ae(a,this,t)),this._$AV.push(d),c=o[++s]}u!==(null===(l=c)||void 0===l?void 0:l.index)&&(a=Zt.nextNode(),u++)}return Zt.currentNode=$t,r}},{key:"p",value:function(t){var e,n=0,i=K(this._$AV);try{for(i.s();!(e=i.n()).done;){var o=e.value;void 0!==o&&(void 0!==o.strings?(o._$AI(t,o,n),n+=o.strings.length-2):o._$AI(t[n])),n++}}catch(r){i.e(r)}finally{i.f()}}}])}(),ee=function(){function t(e,n,i,o){var r;Q(this,t),this.type=2,this._$AH=Wt,this._$AN=void 0,this._$AA=e,this._$AB=n,this._$AM=i,this.options=o,this._$Cv=null===(r=null==o?void 0:o.isConnected)||void 0===r||r}return et(t,[{key:"_$AU",get:function(){var t,e;return null!==(t=null===(e=this._$AM)||void 0===e?void 0:e._$AU)&&void 0!==t?t:this._$Cv}},{key:"parentNode",get:function(){var t,e=this._$AA.parentNode,n=this._$AM;return void 0!==n&&11===(null===(t=e)||void 0===t?void 0:t.nodeType)&&(e=n.parentNode),e}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){t=Qt(this,t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),Mt(t)?t===Wt||null==t||""===t?(this._$AH!==Wt&&this._$AR(),this._$AH=Wt):t!==this._$AH&&t!==qt&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):function(t){return Pt(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])}(t)?this.k(t):this._(t)}},{key:"O",value:function(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}},{key:"T",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}},{key:"_",value:function(t){this._$AH!==Wt&&Mt(this._$AH)?this._$AA.nextSibling.data=t:this.T($t.createTextNode(t)),this._$AH=t}},{key:"$",value:function(t){var e,n=t.values,i=t._$litType$,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=Jt.createElement(Kt(i.h,i.h[0]),this.options)),i);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===o)this._$AH.p(n);else{var r=new te(o,this),a=r.u(this.options);r.p(n),this.T(a),this._$AH=r}}},{key:"_$AC",value:function(t){var e=Yt.get(t.strings);return void 0===e&&Yt.set(t.strings,e=new Jt(t)),e}},{key:"k",value:function(e){Pt(this._$AH)||(this._$AH=[],this._$AR());var n,i,o=this._$AH,r=0,a=K(e);try{for(a.s();!(i=a.n()).done;){var u=i.value;r===o.length?o.push(n=new t(this.O(xt()),this.O(xt()),this,this.options)):n=o[r],n._$AI(u),r++}}catch(s){a.e(s)}finally{a.f()}r0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,e=arguments.length>1?arguments[1]:void 0;for(null===(n=this._$AP)||void 0===n||n.call(this,!1,!0,e);t!==this._$AB;){var n,i=t.nextSibling;t.remove(),t=i}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cv=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}])}(),ne=function(){return et((function t(e,n,i,o,r){Q(this,t),this.type=1,this._$AH=Wt,this._$AN=void 0,this.element=e,this.name=n,this._$AM=o,this.options=r,i.length>2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Wt}),[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,n=arguments.length>2?arguments[2]:void 0,i=arguments.length>3?arguments[3]:void 0,o=this.strings,r=!1;if(void 0===o)t=Qt(this,t,e,0),(r=!Mt(t)||t!==this._$AH&&t!==qt)&&(this._$AH=t);else{var a,u,s=t;for(t=o[0],a=0;a1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==e?e:Wt)!==qt){var n=this._$AH,i=t===Wt&&n!==Wt||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,o=t!==Wt&&(n===Wt||i);i&&this.element.removeEventListener(this.name,this,n),o&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,n;"function"==typeof this._$AH?this._$AH.call(null!==(e=null===(n=this.options)||void 0===n?void 0:n.host)&&void 0!==e?e:this.element,t):this._$AH.handleEvent(t)}}])}(),ae=function(){return et((function t(e,n,i){Q(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=n,this.options=i}),[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){Qt(this,t)}}])}(),ue=kt.litHtmlPolyfillSupport;null!=ue&&ue(Jt,ee),(null!==(i=kt.litHtmlVersions)&&void 0!==i?i:kt.litHtmlVersions=[]).push("3.3.1");var se=globalThis,ce=function(t){function e(){var t;return Q(this,e),(t=L(this,e,arguments)).renderOptions={host:U(t)},t._$Do=void 0,t}return G(e,Et),et(e,[{key:"createRenderRoot",value:function(){var t,n,i=M(e,"createRenderRoot",this,3)([]);return null!==(n=(t=this.renderOptions).renderBefore)&&void 0!==n||(t.renderBefore=i.firstChild),i}},{key:"update",value:function(t){var n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),M(e,"update",this,3)([t]),this._$Do=function(t,e,n){var i,o=null!==(i=null==n?void 0:n.renderBefore)&&void 0!==i?i:e,r=o._$litPart$;if(void 0===r){var a,u=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;o._$litPart$=r=new ee(e.insertBefore(xt(),u),u,void 0,null!=n?n:{})}return r._$AI(t),r}(n,this.renderRoot,this.renderOptions)}},{key:"connectedCallback",value:function(){var t;M(e,"connectedCallback",this,3)([]),null===(t=this._$Do)||void 0===t||t.setConnected(!0)}},{key:"disconnectedCallback",value:function(){var t;M(e,"disconnectedCallback",this,3)([]),null===(t=this._$Do)||void 0===t||t.setConnected(!1)}},{key:"render",value:function(){return qt}}])}(); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ce._$litElement$=!0,ce.finalized=!0,null===(o=se.litElementHydrateSupport)||void 0===o||o.call(se,{LitElement:ce});var le=se.litElementPolyfillSupport;null==le||le({LitElement:ce}),(null!==(r=se.litElementVersions)&&void 0!==r?r:se.litElementVersions=[]).push("4.2.1"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var de={attribute:!0,type:String,converter:At,reflect:!1,hasChanged:wt}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function he(t){return function(e,n){return"object"==it(n)?function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:de,e=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,i=n.kind,o=n.metadata,r=globalThis.litPropertyMetadata.get(o);if(void 0===r&&globalThis.litPropertyMetadata.set(o,r=new Map),"setter"===i&&((t=Object.create(t)).wrapped=!0),r.set(n.name,t),"accessor"===i){var a=n.name;return{set:function(n){var i=e.get.call(this);e.set.call(this,n),this.requestUpdate(a,i,t)},init:function(e){return void 0!==e&&this.C(a,void 0,t,e),e}}}if("setter"===i){var u=n.name;return function(n){var i=this[u];e.call(this,n),this.requestUpdate(u,i,t)}}throw Error("Unsupported decorator location: "+i)}(t,e,n):function(t,e,n){var i=e.hasOwnProperty(n);return e.constructor.createProperty(n,t),i?Object.getOwnPropertyDescriptor(e,n):void 0}(t,e,n)}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var fe=function(t){return null!=t?t:Wt},ve=1,pe=2,me=function(t){return function(){for(var e=arguments.length,n=new Array(e),i=0;i1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,i=this._$AH,o=this._$AN;if(void 0!==o&&0!==o.size)if(e)if(Array.isArray(i))for(var r=n;r1&&void 0!==arguments[1])||arguments[1];t!==this.isConnected&&(this.isConnected=t,t?null===(e=this.reconnected)||void 0===e||e.call(this):null===(n=this.disconnected)||void 0===n||n.call(this)),i&&(_e(this,t),ye(this))}},{key:"setValue",value:function(t){if(function(t){return void 0===t.strings}(this._$Ct))this._$Ct._$AI(t,this);else{var e=H(this._$Ct._$AH);e[this._$Ci]=t,this._$Ct._$AI(e,this,0)}}},{key:"disconnected",value:function(){}},{key:"reconnected",value:function(){}}])}(),Ee=function(){return et((function t(e){Q(this,t),this.G=e}),[{key:"disconnect",value:function(){this.G=void 0}},{key:"reconnect",value:function(t){this.G=t}},{key:"deref",value:function(){return this.G}}])}(),ke=function(){return et((function t(){Q(this,t),this.Y=void 0,this.Z=void 0}),[{key:"get",value:function(){return this.Y}},{key:"pause",value:function(){var t,e=this;null!==(t=this.Y)&&void 0!==t||(this.Y=new Promise((function(t){return e.Z=t})))}},{key:"resume",value:function(){var t;null!==(t=this.Z)&&void 0!==t&&t.call(this),this.Y=this.Z=void 0}}])}(),Fe=function(t){return!function(t){return null===t||"object"!=it(t)&&"function"!=typeof t}(t)&&"function"==typeof t.then},Oe=1073741823,je=function(t){function e(){var t;return Q(this,e),(t=L(this,e,arguments))._$Cwt=Oe,t._$Cbt=[],t._$CK=new Ee(U(t)),t._$CX=new ke,t}return G(e,Ce),et(e,[{key:"render",value:function(){for(var t,e=arguments.length,n=new Array(e),i=0;i-1&&othis._$Cwt);c++)if(u=s())return u.v;return qt}},{key:"disconnected",value:function(){this._$CK.disconnect(),this._$CX.pause()}},{key:"reconnected",value:function(){this._$CK.reconnect(this),this._$CX.resume()}}])}(),Se=me(je),Te="important",Be=" !"+Te,$e=me(function(t){function e(t){var n,i;if(Q(this,e),i=L(this,e,[t]),t.type!==ve||"style"!==t.name||(null===(n=t.strings)||void 0===n?void 0:n.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.");return i}return G(e,ge),et(e,[{key:"render",value:function(t){return Object.keys(t).reduce((function(e,n){var i=t[n];return null==i?e:e+"".concat(n=n.includes("-")?n:n.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase(),":").concat(i,";")}),"")}},{key:"update",value:function(t,e){var n=I(e,1)[0],i=t.element.style;if(void 0===this.ft)return this.ft=new Set(Object.keys(n)),this.render(n);var o,r=K(this.ft);try{for(r.s();!(o=r.n()).done;){var a=o.value;null==n[a]&&(this.ft.delete(a),a.includes("-")?i.removeProperty(a):i[a]=null)}}catch(l){r.e(l)}finally{r.f()}for(var u in n){var s=n[u];if(null!=s){this.ft.add(u);var c="string"==typeof s&&s.endsWith(Be);u.includes("-")||c?i.setProperty(u,c?s.slice(0,-11):s,c?Te:""):i[u]=s}}return qt}}])}()),xe=function(t){function e(t){var n;if(Q(this,e),(n=L(this,e,[t])).it=Wt,t.type!==pe)throw Error(n.constructor.directiveName+"() can only be used in child bindings");return n}return G(e,ge),et(e,[{key:"render",value:function(t){if(t===Wt||null==t)return this._t=void 0,this.it=t;if(t===qt)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.it)return this._t;this.it=t;var e=[t];return e.raw=e,this._t={_$litType$:this.constructor.resultType,strings:e,values:[]}}}])}();xe.directiveName="unsafeHTML",xe.resultType=1;var Me=me(xe),Pe=me(function(t){function e(t){var n,i;if(Q(this,e),i=L(this,e,[t]),t.type!==ve||"class"!==t.name||(null===(n=t.strings)||void 0===n?void 0:n.length)>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.");return i}return G(e,ge),et(e,[{key:"render",value:function(t){return" "+Object.keys(t).filter((function(e){return t[e]})).join(" ")+" "}},{key:"update",value:function(t,e){var n=I(e,1)[0];if(void 0===this.st){for(var i in this.st=new Set,void 0!==t.strings&&(this.nt=new Set(t.strings.join(" ").split(/\s/).filter((function(t){return""!==t})))),n){var o;n[i]&&(null===(o=this.nt)||void 0===o||!o.has(i))&&this.st.add(i)}return this.render(n)}var r,a=t.element.classList,u=K(this.st);try{for(u.s();!(r=u.n()).done;){var s=r.value;s in n||(a.remove(s),this.st.delete(s))}}catch(h){u.e(h)}finally{u.f()}for(var c in n){var l,d=!!n[c];d===this.st.has(c)||(null===(l=this.nt)||void 0===l?void 0:l.has(c))||(d?(a.add(c),this.st.add(c)):(a.remove(c),this.st.delete(c)))}return qt}}])}()),He=function(t,e,n,i){i=i||{},n=null==n?{}:n;var o=new Event(e,{bubbles:void 0===i.bubbles||i.bubbles,cancelable:Boolean(i.cancelable),composed:void 0===i.composed||i.composed});return o.detail=n,t.dispatchEvent(o),o},Ie=function(t,e){if(t===e)return!0;if(t&&e&&"object"===it(t)&&"object"===it(e)){if(t.constructor!==e.constructor)return!1;var n,i;if(Array.isArray(t)){if((i=t.length)!==e.length)return!1;for(n=i;0!=n--;)if(!Ie(t[n],e[n]))return!1;return!0}if(t instanceof Map&&e instanceof Map){if(t.size!==e.size)return!1;var o,r=K(t.entries());try{for(r.s();!(o=r.n()).done;)if(n=o.value,!e.has(n[0]))return!1}catch(h){r.e(h)}finally{r.f()}var a,u=K(t.entries());try{for(u.s();!(a=u.n()).done;)if(n=a.value,!Ie(n[1],e.get(n[0])))return!1}catch(h){u.e(h)}finally{u.f()}return!0}if(t instanceof Set&&e instanceof Set){if(t.size!==e.size)return!1;var s,c=K(t.entries());try{for(c.s();!(s=c.n()).done;)if(n=s.value,!e.has(n[0]))return!1}catch(h){c.e(h)}finally{c.f()}return!0}if(ArrayBuffer.isView(t)&&ArrayBuffer.isView(e)){if((i=t.length)!==e.length)return!1;for(n=i;0!=n--;)if(t[n]!==e[n])return!1;return!0}if(t.constructor===RegExp)return t.source===e.source&&t.flags===e.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===e.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===e.toString();var l=Object.keys(t);if((i=l.length)!==Object.keys(e).length)return!1;for(n=i;0!=n--;)if(!Object.prototype.hasOwnProperty.call(e,l[n]))return!1;for(n=i;0!=n--;){var d=l[n];if(!Ie(t[d],e[d]))return!1}return!0}return t!=t&&e!=e},Re="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,Ne=function(t){function e(){var t;return Q(this,e),(t=L(this,e,arguments)).holdTime=500,t.held=!1,t.cancelled=!1,t.isRepeating=!1,t.repeatCount=0,t}return G(e,q(HTMLElement)),et(e,[{key:"connectedCallback",value:function(){var t=this;Object.assign(this.style,{position:"fixed",width:Re?"100px":"50px",height:Re?"100px":"50px",transform:"translate(-50%, -50%) scale(0)",pointerEvents:"none",zIndex:"999",background:"var(--primary-color)",display:null,opacity:"0.2",borderRadius:"50%",transition:"transform 180ms ease-in-out"}),["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach((function(e){document.addEventListener(e,(function(){t.cancelled=!0,t.timer&&(t.stopAnimation(),clearTimeout(t.timer),t.timer=void 0,t.isRepeating&&t.repeatTimeout&&(clearInterval(t.repeatTimeout),t.isRepeating=!1))}),{passive:!0})}))}},{key:"bind",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t.actionHandler&&Ie(n,t.actionHandler.options)||(t.actionHandler?(t.removeEventListener("touchstart",t.actionHandler.start),t.removeEventListener("touchend",t.actionHandler.end),t.removeEventListener("touchcancel",t.actionHandler.end),t.removeEventListener("mousedown",t.actionHandler.start),t.removeEventListener("click",t.actionHandler.end),t.removeEventListener("keydown",t.actionHandler.handleKeyDown)):t.addEventListener("contextmenu",(function(t){var e=t||window.event;return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0,e.returnValue=!1,!1})),t.actionHandler={options:n},n.disabled||(t.actionHandler.start=function(i){var o,r,a;if(!(null===(o=i.detail)||void 0===o?void 0:o.ignore))if(e.cancelled=!1,i.touches?(r=i.touches[0].clientX,a=i.touches[0].clientY):(r=i.clientX,a=i.clientY),n.isMomentary){if(!i.touches&&0!==i.button)return;He(t,"action",{action:"press"})}else n.hasHold&&(e.held=!1,e.timer=window.setTimeout((function(){e.startAnimation(r,a),e.held=!0,n.repeat&&!e.isRepeating&&(e.repeatCount=0,e.isRepeating=!0,e.repeatTimeout=setInterval((function(){He(t,"action",{action:"hold"}),e.repeatCount++,e.repeatTimeout&&n.repeatLimit&&e.repeatCount>=n.repeatLimit&&(clearInterval(e.repeatTimeout),e.isRepeating=!1)}),n.repeat))}),e.holdTime))},t.actionHandler.end=function(i){var o;if(!(null===(o=i.detail)||void 0===o?void 0:o.ignore)){if(["touchend","touchcancel"].includes(i.type)&&e.cancelled)return e.isRepeating&&e.repeatTimeout&&(clearInterval(e.repeatTimeout),e.isRepeating=!1),void(n.isMomentary&&He(t,"action",{action:"release"}));if("touchcancel"!=i.type){var r=i.target;i.cancelable&&i.preventDefault(),n.isMomentary?He(t,"action",{action:"release"}):(n.hasHold&&(clearTimeout(e.timer),e.isRepeating&&e.repeatTimeout&&clearInterval(e.repeatTimeout),e.isRepeating=!1,e.stopAnimation(),e.timer=void 0),n.hasHold&&e.held?n.repeat||He(r,"action",{action:"hold"}):n.hasDoubleClick?"click"===i.type&&i.detail<2||!e.dblClickTimeout?e.dblClickTimeout=window.setTimeout((function(){e.dblClickTimeout=void 0,He(r,"action",{action:"tap"})}),250):(clearTimeout(e.dblClickTimeout),e.dblClickTimeout=void 0,He(r,"action",{action:"double_tap"})):He(r,"action",{action:"tap"}))}}},t.actionHandler.handleTouchMove=function(t){"touchmove"==t.type&&n.hasHold&&e.held&&(t.stopPropagation(),t.preventDefault())},t.actionHandler.handleKeyDown=function(t){n.disableKbd||["Enter"," "].includes(t.key)&&t.currentTarget.actionHandler.end(t)},t.addEventListener("touchstart",t.actionHandler.start,{passive:!0}),t.addEventListener("touchmove",t.actionHandler.handleTouchMove),t.addEventListener("touchend",t.actionHandler.end),t.addEventListener("touchcancel",t.actionHandler.end),t.addEventListener("mousedown",t.actionHandler.start,{passive:!0}),t.addEventListener("click",t.actionHandler.end),t.addEventListener("keydown",t.actionHandler.handleKeyDown)))}},{key:"startAnimation",value:function(t,e){Object.assign(this.style,{left:"".concat(t,"px"),top:"".concat(e,"px"),transform:"translate(-50%, -50%) scale(1)"})}},{key:"stopAnimation",value:function(){Object.assign(this.style,{left:null,top:null,transform:"translate(-50%, -50%) scale(0)"})}}])}(); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */customElements.define("button-card-action-handler",Ne);var Ve=function(t,e){var n=function(){var t=document.body;if(t.querySelector("button-card-action-handler"))return t.querySelector("button-card-action-handler");var e=document.createElement("button-card-action-handler");return t.appendChild(e),e}();n&&n.bind(t,e)},ze=me(function(t){function e(){return Q(this,e),L(this,e,arguments)}return G(e,ge),et(e,[{key:"update",value:function(t,e){var n=I(e,1)[0];return Ve(t.element,n),qt}},{key:"render",value:function(t){}}])}());function Le(t,e){(function(t){return"string"==typeof t&&-1!==t.indexOf(".")&&1===parseFloat(t)})(t)&&(t="100%");var n=function(t){return"string"==typeof t&&-1!==t.indexOf("%")}(t);return t=360===e?t:Math.min(e,Math.max(0,parseFloat(t))),n&&(t=parseInt(String(t*e),10)/100),Math.abs(t-e)<1e-6?1:t=360===e?(t<0?t%e+e:t%e)/parseFloat(String(e)):t%e/parseFloat(String(e))}function Ue(t){return Math.min(1,Math.max(0,t))}function Ge(t){return t=parseFloat(t),(isNaN(t)||t<0||t>1)&&(t=1),t}function qe(t){return Number(t)<=1?"".concat(100*Number(t),"%"):t}function We(t){return 1===t.length?"0"+t:String(t)}function Ye(t,e,n){t=Le(t,255),e=Le(e,255),n=Le(n,255);var i=Math.max(t,e,n),o=Math.min(t,e,n),r=0,a=0,u=(i+o)/2;if(i===o)a=0,r=0;else{var s=i-o;switch(a=u>.5?s/(2-i-o):s/(i+o),i){case t:r=(e-n)/s+(e1&&(n-=1),n<1/6?t+6*n*(e-t):n<.5?e:n<2/3?t+(e-t)*(2/3-n)*6:t}function Ke(t,e,n){t=Le(t,255),e=Le(e,255),n=Le(n,255);var i=Math.max(t,e,n),o=Math.min(t,e,n),r=0,a=i,u=i-o,s=0===i?0:u/i;if(i===o)r=0;else{switch(i){case t:r=(e-n)/u+(e0&&void 0!==arguments[0]?arguments[0]:"",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(Q(this,t),n instanceof t)return n;"number"==typeof n&&(n=function(t){return{r:t>>16,g:(65280&t)>>8,b:255&t}}(n)),this.originalInput=n;var o=on(n);this.originalInput=n,this.r=o.r,this.g=o.g,this.b=o.b,this.a=o.a,this.roundA=Math.round(100*this.a)/100,this.format=null!==(e=i.format)&&void 0!==e?e:o.format,this.gradientType=i.gradientType,this.r<1&&(this.r=Math.round(this.r)),this.g<1&&(this.g=Math.round(this.g)),this.b<1&&(this.b=Math.round(this.b)),this.isValid=o.ok}return et(t,[{key:"isDark",value:function(){return this.getBrightness()<128}},{key:"isLight",value:function(){return!this.isDark()}},{key:"getBrightness",value:function(){var t=this.toRgb();return(299*t.r+587*t.g+114*t.b)/1e3}},{key:"getLuminance",value:function(){var t=this.toRgb(),e=t.r/255,n=t.g/255,i=t.b/255;return.2126*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))+.7152*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))+.0722*(i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4))}},{key:"getAlpha",value:function(){return this.a}},{key:"setAlpha",value:function(t){return this.a=Ge(t),this.roundA=Math.round(100*this.a)/100,this}},{key:"isMonochrome",value:function(){return 0===this.toHsl().s}},{key:"toHsv",value:function(){var t=Ke(this.r,this.g,this.b);return{h:360*t.h,s:t.s,v:t.v,a:this.a}}},{key:"toHsvString",value:function(){var t=Ke(this.r,this.g,this.b),e=Math.round(360*t.h),n=Math.round(100*t.s),i=Math.round(100*t.v);return 1===this.a?"hsv(".concat(e,", ").concat(n,"%, ").concat(i,"%)"):"hsva(".concat(e,", ").concat(n,"%, ").concat(i,"%, ").concat(this.roundA,")")}},{key:"toHsl",value:function(){var t=Ye(this.r,this.g,this.b);return{h:360*t.h,s:t.s,l:t.l,a:this.a}}},{key:"toHslString",value:function(){var t=Ye(this.r,this.g,this.b),e=Math.round(360*t.h),n=Math.round(100*t.s),i=Math.round(100*t.l);return 1===this.a?"hsl(".concat(e,", ").concat(n,"%, ").concat(i,"%)"):"hsla(".concat(e,", ").concat(n,"%, ").concat(i,"%, ").concat(this.roundA,")")}},{key:"toHex",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return Xe(this.r,this.g,this.b,t)}},{key:"toHexString",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return"#"+this.toHex(t)}},{key:"toHex8",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return function(t,e,n,i,o){var r=[We(Math.round(t).toString(16)),We(Math.round(e).toString(16)),We(Math.round(n).toString(16)),We(Qe(i))];return o&&r[0].startsWith(r[0].charAt(1))&&r[1].startsWith(r[1].charAt(1))&&r[2].startsWith(r[2].charAt(1))&&r[3].startsWith(r[3].charAt(1))?r[0].charAt(0)+r[1].charAt(0)+r[2].charAt(0)+r[3].charAt(0):r.join("")}(this.r,this.g,this.b,this.a,t)}},{key:"toHex8String",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return"#"+this.toHex8(t)}},{key:"toHexShortString",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return 1===this.a?this.toHexString(t):this.toHex8String(t)}},{key:"toRgb",value:function(){return{r:Math.round(this.r),g:Math.round(this.g),b:Math.round(this.b),a:this.a}}},{key:"toRgbString",value:function(){var t=Math.round(this.r),e=Math.round(this.g),n=Math.round(this.b);return 1===this.a?"rgb(".concat(t,", ").concat(e,", ").concat(n,")"):"rgba(".concat(t,", ").concat(e,", ").concat(n,", ").concat(this.roundA,")")}},{key:"toPercentageRgb",value:function(){var t=function(t){return"".concat(Math.round(100*Le(t,255)),"%")};return{r:t(this.r),g:t(this.g),b:t(this.b),a:this.a}}},{key:"toPercentageRgbString",value:function(){var t=function(t){return Math.round(100*Le(t,255))};return 1===this.a?"rgb(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%)"):"rgba(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%, ").concat(this.roundA,")")}},{key:"toCmyk",value:function(){return function(t){for(var e=1;e=0;return e||!i||!t.startsWith("hex")&&"name"!==t?("rgb"===t&&(n=this.toRgbString()),"prgb"===t&&(n=this.toPercentageRgbString()),"hex"!==t&&"hex6"!==t||(n=this.toHexString()),"hex3"===t&&(n=this.toHexString(!0)),"hex4"===t&&(n=this.toHex8String(!0)),"hex8"===t&&(n=this.toHex8String()),"name"===t&&(n=this.toName()),"hsl"===t&&(n=this.toHslString()),"hsv"===t&&(n=this.toHsvString()),"cmyk"===t&&(n=this.toCmykString()),n||this.toHexString()):"name"===t&&0===this.a?this.toName():this.toRgbString()}},{key:"toNumber",value:function(){return(Math.round(this.r)<<16)+(Math.round(this.g)<<8)+Math.round(this.b)}},{key:"clone",value:function(){return new t(this.toString())}},{key:"lighten",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,n=this.toHsl();return n.l+=e/100,n.l=Ue(n.l),new t(n)}},{key:"brighten",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,n=this.toRgb();return n.r=Math.max(0,Math.min(255,n.r-Math.round(-e/100*255))),n.g=Math.max(0,Math.min(255,n.g-Math.round(-e/100*255))),n.b=Math.max(0,Math.min(255,n.b-Math.round(-e/100*255))),new t(n)}},{key:"darken",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,n=this.toHsl();return n.l-=e/100,n.l=Ue(n.l),new t(n)}},{key:"tint",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;return this.mix("white",t)}},{key:"shade",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;return this.mix("black",t)}},{key:"desaturate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,n=this.toHsl();return n.s-=e/100,n.s=Ue(n.s),new t(n)}},{key:"saturate",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,n=this.toHsl();return n.s+=e/100,n.s=Ue(n.s),new t(n)}},{key:"greyscale",value:function(){return this.desaturate(100)}},{key:"spin",value:function(e){var n=this.toHsl(),i=(n.h+e)%360;return n.h=i<0?360+i:i,new t(n)}},{key:"mix",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:50,i=this.toRgb(),o=new t(e).toRgb(),r=n/100;return new t({r:(o.r-i.r)*r+i.r,g:(o.g-i.g)*r+i.g,b:(o.b-i.b)*r+i.b,a:(o.a-i.a)*r+i.a})}},{key:"analogous",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:6,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:30,i=this.toHsl(),o=360/n,r=[this];for(i.h=(i.h-(o*e>>1)+720)%360;--e;)i.h=(i.h+o)%360,r.push(new t(i));return r}},{key:"complement",value:function(){var e=this.toHsl();return e.h=(e.h+180)%360,new t(e)}},{key:"monochromatic",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:6,n=this.toHsv(),i=n.h,o=n.s,r=n.v,a=[],u=1/e;e--;)a.push(new t({h:i,s:o,v:r})),r=(r+u)%1;return a}},{key:"splitcomplement",value:function(){var e=this.toHsl(),n=e.h;return[this,new t({h:(n+72)%360,s:e.s,l:e.l}),new t({h:(n+216)%360,s:e.s,l:e.l})]}},{key:"onBackground",value:function(e){var n=this.toRgb(),i=new t(e).toRgb(),o=n.a+i.a*(1-n.a);return new t({r:(n.r*n.a+i.r*i.a*(1-n.a))/o,g:(n.g*n.a+i.g*i.a*(1-n.a))/o,b:(n.b*n.a+i.b*i.a*(1-n.a))/o,a:o})}},{key:"triad",value:function(){return this.polyad(3)}},{key:"tetrad",value:function(){return this.polyad(4)}},{key:"polyad",value:function(e){for(var n=this.toHsl(),i=n.h,o=[this],r=360/e,a=1;at.length)&&(n=t.length);for(var i=[],o=0;o1?e-1:0),i=1;i :first-child {\n width: 100%;\n }\n [style*='--aspect-ratio'] > img {\n height: auto;\n }\n @supports (--custom: property) {\n [style*='--aspect-ratio'] {\n position: relative;\n }\n [style*='--aspect-ratio']::before {\n content: '';\n display: block;\n padding-bottom: calc(100% / (var(--aspect-ratio)));\n }\n [style*='--aspect-ratio'] > :first-child {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n }\n }\n"])));!function(t){t.language="language",t.system="system",t.comma_decimal="comma_decimal",t.decimal_comma="decimal_comma",t.space_comma="space_comma",t.none="none"}(Mn||(Mn={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(Pn||(Pn={})),function(t){t.local="local",t.server="server"}(Hn||(Hn={})),function(t){t.language="language",t.system="system",t.DMY="DMY",t.MDY="MDY",t.YMD="YMD"}(In||(In={})),function(t){t.language="language",t.monday="monday",t.tuesday="tuesday",t.wednesday="wednesday",t.thursday="thursday",t.friday="friday",t.saturday="saturday",t.sunday="sunday"}(Rn||(Rn={}));var qn,Wn,Yn,Zn,Kn,Xn=function(t,e,n){var i=e?function(t){switch(t.number_format){case Mn.comma_decimal:return["en-US","en"];case Mn.decimal_comma:return["de","es","it"];case Mn.space_comma:return["fr","sv","cs"];case Mn.system:return;default:return t.language}}(e):void 0;if(Number.isNaN=Number.isNaN||function t(e){return"number"==typeof e&&t(e)},(null==e?void 0:e.number_format)!==Mn.none&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(i,Qn(t,n)).format(Number(t))}catch(o){return console.error(o),new Intl.NumberFormat(void 0,Qn(t,n)).format(Number(t))}return"string"==typeof t?t:"".concat(function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2;return Math.round(t*Math.pow(10,e))/Math.pow(10,e)}(t,null==n?void 0:n.maximumFractionDigits).toString()).concat("currency"===(null==n?void 0:n.style)?" ".concat(n.currency):"")},Jn=function(t,e,n){var i,o=null==n?void 0:n.display_precision;return void 0!==e&&(o=e),null!=o?{maximumFractionDigits:o,minimumFractionDigits:o}:Number.isInteger(Number(null===(i=t.attributes)||void 0===i?void 0:i.step))&&Number.isInteger(Number(t.state))?{maximumFractionDigits:0}:null!=t.attributes.step?{maximumFractionDigits:Math.ceil(Math.log10(1/t.attributes.step))}:void 0},Qn=function(t,e){var n=Object.assign({maximumFractionDigits:2},e);if("string"!=typeof t)return n;if(!e||void 0===e.minimumFractionDigits&&void 0===e.maximumFractionDigits){var i=t.indexOf(".")>-1?t.split(".")[1].length:0;n.minimumFractionDigits=i,n.maximumFractionDigits=i}return n};!function(t){t.language="language",t.system="system",t.comma_decimal="comma_decimal",t.decimal_comma="decimal_comma",t.space_comma="space_comma",t.none="none"}(qn||(qn={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(Wn||(Wn={})),function(t){t.local="local",t.server="server"}(Yn||(Yn={})),function(t){t.language="language",t.system="system",t.DMY="DMY",t.MDY="MDY",t.YMD="YMD"}(Zn||(Zn={})),function(t){t.language="language",t.monday="monday",t.tuesday="tuesday",t.wednesday="wednesday",t.thursday="thursday",t.friday="friday",t.saturday="saturday",t.sunday="sunday"}(Kn||(Kn={}));var ti=function(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2,n=""+t,i=1;i0?"".concat(e,":").concat(ti(n),":").concat(ti(i)):n>0?"".concat(n,":").concat(ti(i)):i>0||o>0?"".concat(i).concat(o>0?".".concat(ti(o,3)):""):null}(parseFloat(t)*ei[e])||"0"},ii=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function oi(t,e){if(t.length!==e.length)return!1;for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:"_",n="àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;",i="aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz".concat(e).concat(e).concat(e).concat(e).concat(e).concat(e),o=new RegExp(n.split("").join("|"),"g");return t.toString().toLowerCase().replace(/\s+/g,e).replace(o,(function(t){return i.charAt(n.indexOf(t))})).replace(/&/g,"".concat(e,"and").concat(e)).replace(/[^\w-]+/g,"").replace(/-/g,e).replace(new RegExp("(".concat(e,")\\1+"),"g"),"$1").replace(new RegExp("^".concat(e,"+")),"").replace(new RegExp("".concat(e,"+$")),"")}(o,"_"),s=r?"active":"inactive";if(i&&yn.includes(i)&&"inactive"==s)return bn;var c=e.attributes.device_class;return c&&a.push("--state-".concat(t,"-").concat(c,"-").concat(u,"-color")),a.push("--state-".concat(t,"-").concat(u,"-color"),"--state-".concat(t,"-").concat(s,"-color"),"--state-".concat(s,"-color")),a},vo=function(t,e,n){var i=void 0!==e?e:null==t?void 0:t.state,o=Cn(t.entity_id),r=t.attributes.device_class;if("sensor"===o&&"battery"===r){var a=function(t){var e=Number(t);if(!isNaN(e))return e>=70?"--state-sensor-battery-high-color":e>=30?"--state-sensor-battery-medium-color":"--state-sensor-battery-low-color"}(i);if(a)return[a]}if("group"===o){var u=function(t){var e=t.attributes.entity_id||[],n=H(new Set(e.map((function(t){return Cn(t)}))));return 1===n.length?n[0]:void 0}(t);if(u&&lo.has(u))return fo(u,t,e,n)}return lo.has(o)?fo(o,t,e,n):n&&yn.includes(n)?bn:void 0},po=function(){var t=z(R().m((function t(e,n,i,o){return R().w((function(t){for(;;)switch(t.n){case 0:He(e,"hass-action",{config:i,action:o});case 1:return t.a(2)}}),t)})));return function(e,n,i,o){return t.apply(this,arguments)}}(),mo=Object.create(null),go=36e5,_o=24*go,yo=365.25*_o;mo.year=mo.yr=mo.y=yo,mo.month=mo.mo=mo.mth=yo/12,mo.week=mo.wk=mo.w=7*_o,mo.day=mo.d=_o,mo.hour=mo.hr=mo.h=go,mo.minute=mo.min=mo.m=6e4,mo.second=mo.sec=mo.s=1e3,mo.millisecond=mo.millisec=mo.ms=1,mo.microsecond=mo.microsec=mo.us=mo.µs=.001,mo.nanosecond=mo.nanosec=mo.ns=1e-6,mo.group=",",mo.decimal=".",mo.placeholder=" _";var bo=/((?:[0-9]{1,16}(?:\.[0-9]{1,16})?|\.[0-9]{1,16})(?:[Ee][\+\x2D]?[0-9]{1,4})?)[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]?((?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088F\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5C\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDC-\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C8A\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7DC\uA7F1-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDDC0-\uDDF3\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD40-\uDD59\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDD4A-\uDD65\uDD6F-\uDD85\uDE80-\uDEA9\uDEB0\uDEB1\uDEC2-\uDEC7\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61\uDF80-\uDF89\uDF8B\uDF8E\uDF90-\uDFB5\uDFB7\uDFD1\uDFD3]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8\uDFC0-\uDFE0]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDB0-\uDDDB\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD80E\uD80F\uD81C-\uD822\uD840-\uD868\uD86A-\uD86D\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD88C][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46\uDC60-\uDFFF]|\uD810[\uDC00-\uDFFA]|\uD811[\uDC00-\uDE46]|\uD818[\uDD00-\uDD1D]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDD40-\uDD6C\uDE40-\uDE7F\uDEA0-\uDEB8\uDEBB-\uDED3\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3\uDFF2\uDFF3]|\uD823[\uDC00-\uDCD5\uDCFF-\uDD1E\uDD80-\uDDF2]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDDD0-\uDDED\uDDF0\uDEC0-\uDEDE\uDEE0-\uDEE2\uDEE4\uDEE5\uDEE7-\uDEED\uDEF0-\uDEF4\uDEFE\uDEFF\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEAD\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD88D[\uDC00-\uDC79]){0,14})/g;function Do(){var t,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"ms",i=null;return String(e).replace(new RegExp("(\\d)[".concat(Do.unit.placeholder).concat(Do.unit.group,"](\\d)"),"g"),"$1$2").replace(Do.unit.decimal,".").replace(bo,(function(e,o,r){if(r)r=r.toLowerCase();else if(t){for(var a in Do.unit)if(Do.unit[a]1&&void 0!==arguments[1]&&arguments[1],n=function(t,e){return i("hui-error-card",{type:"error",error:t,config:e})},i=function(t,e){var i=window.document.createElement(t);try{if(!i.setConfig)return;i.setConfig(e)}catch(o){return console.error(t,o),n(o.message,e)}return i};if(!t||"object"!==it(t)||!e&&!t.type)return n("No type defined",t);var o=t.type;if(o&&o.startsWith("custom:"))o=o.substr(7);else if(e)if(Ln.has(o))o="hui-".concat(o,"-row");else{if(!t.entity)return n("Invalid config given.",t);var r=t.entity.split(".",1)[0];o="hui-".concat(Un[r]||"text","-entity-row")}else o="hui-".concat(o,"-card");if(customElements.get(o))return i(o,t);var a=n("Custom element doesn't exist: ".concat(t.type,"."),t);a.style.display="None";var u=setTimeout((function(){a.style.display=""}),2e3);return customElements.whenDefined(t.type).then((function(){clearTimeout(u),He(a,"ll-rebuild",{},a)})),a}(t);return Bo.then((function(){He(e,"ll-rebuild",{})})),e}},{key:"render",value:function(){var t,e,n,i=this;if(!this._config||!this._hass)return Gt(u||(u=B([""])));this._stateObj=this._config.entity?this._hass.states[this._config.entity]:void 0;try{return this._evaluatedVariables={},(null===(t=this._config)||void 0===t?void 0:t.variables)&&(null===(n=Object.keys(null===(e=this._config)||void 0===e?void 0:e.variables))||void 0===n||n.forEach((function(t){var e=i._config.variables[t];if("object"===it(e)&&null!==e&&e.force_eval)i._pVariables[t]}))),this._cardHtml()}catch(Pe){Pe.stack?console.error(Pe.stack):console.error(Pe);var o=document.createElement("hui-error-card");return o.preview=this.preview,o.setConfig({type:"error",error:Pe.name,message:Pe.message}),Gt(s||(s=B([" "," "])),o)}}},{key:"_hasAnEntityChanged",value:function(t){var e=t.get("_hass");if(e){return this._monitoredEntities.some(function(t){return(null==e?void 0:e.states[t])!==this._hass.states[t]}.bind(this))}return!1}},{key:"shouldUpdate",value:function(t){return!!t.has("_config")||(this._updateTimerDuration?!!t.has("_updateTimerMS")||this._updateTimerChanged():!!(t.has("_timeRemaining")||t.has("_updateTimerMS")||t.has("_spinnerActive"))||this._hasAnEntityChanged(t)?(this._expandTriggerGroups(),!0):!!t.has("preview"))}},{key:"willUpdate",value:function(t){var e=this;t.has("preview")&&Object.keys(this._cards).forEach((function(t){e._cards[t].preview=e.preview}))}},{key:"updated",value:function(t){var n=this;if(M(e,"updated",this,3)([t]),this._config&&this._config.entity&&"timer"===Cn(this._config.entity)&&t.has("_hass")){var i=this._hass.states[this._config.entity],o=t.get("_hass");(o?o.states[this._config.entity]:void 0)!==i?this._startInterval(i):i||this._clearInterval()}this.updateComplete.then((function(){var t,e,i,o,r=null===(t=n.shadowRoot)||void 0===t?void 0:t.getElementById("tooltip");r&&(null===(e=r.eventController)||void 0===e||e.abort(),r.eventController=new AbortController,r.anchor=void 0,null===(o=(i=r).handleForChange)||void 0===o||o.call(i))})),this._updateTimer(),this._computeHidden()}},{key:"_clearInterval",value:function(){this._interval&&(window.clearInterval(this._interval),this._interval=void 0)}},{key:"_startInterval",value:function(t){var e=this;this._clearInterval(),this._calculateRemaining(t),"active"===t.state&&(this._interval=window.setInterval((function(){return e._calculateRemaining(t)}),1e3))}},{key:"_calculateRemaining",value:function(t){t.attributes.remaining&&(this._timeRemaining=function(t){if(t.attributes.remaining){var e=$n(t.attributes.remaining);if("active"===t.state){var n=(new Date).getTime(),i=new Date(t.last_changed).getTime();e=Math.max(e-(n-i)/1e3,0)}return e}}(t))}},{key:"_computeTimeDisplay",value:function(t){if(t)return function(t){var e=Math.floor(t/3600),n=Math.floor(t%3600/60),i=Math.floor(t%3600%60);return e>0?"".concat(e,":").concat(xn(n),":").concat(xn(i)):n>0?"".concat(n,":").concat(xn(i)):i>0?""+i:null}(this._timeRemaining||$n(t.attributes.duration))}},{key:"_getMatchingConfigState",value:function(t){var e=this;if(this._config.state){var n=this._config.state.find((function(t){return"template"===t.operator}));if(t||n){var i,o=this._config.state.find((function(n){if(!n.operator)return t&&e._getTemplateOrValue(t,n.value)==t.state;switch(n.operator){case"==":return t&&t.state==e._getTemplateOrValue(t,n.value);case"<=":return t&&t.state<=e._getTemplateOrValue(t,n.value);case"<":return t&&t.state=":return t&&t.state>=e._getTemplateOrValue(t,n.value);case">":return t&&t.state>e._getTemplateOrValue(t,n.value);case"!=":return t&&t.state!=e._getTemplateOrValue(t,n.value);case"regex":return!(!t||!t.state.match(e._getTemplateOrValue(t,n.value)));case"template":return e._getTemplateOrValue(t,n.value);case"default":return i=n,!1;default:return!1}}));return!o&&i?i:o}}}},{key:"_localize",value:function(t,e,n){var i,o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],r=arguments.length>4?arguments[4]:void 0;return Si(this._hass.localize,t,this._hass.locale,this._hass.config,this._hass.entities,{numeric_precision:"card"===n?null===(i=this._config)||void 0===i?void 0:i.numeric_precision:n,show_units:o,units:r},e)}},{key:"_relativeTime",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return t?Gt(c||(c=B(['\n \n '])),this._hass,t,e):""}},{key:"_getTemplateHelpers",value:function(){var t=this;return{localize:this._localize.bind(this),formatDateTime:function(e){return wi(new Date(e),t._hass.locale,t._hass.config)},formatShortDateTimeWithYear:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,Ei(i,o.time_zone).format(n);var n,i,o},formatShortDateTime:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,ki(i,o.time_zone).format(n);var n,i,o},formatDateTimeWithSeconds:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,Fi(i,o.time_zone).format(n);var n,i,o},formatDateTimeNumeric:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,"".concat(ci(n,i,o),", ").concat(_i(n,i,o));var n,i,o},relativeTime:this._relativeTime.bind(this),formatTime:function(e){return _i(new Date(e),t._hass.locale,t._hass.config)},formatTimeWithSeconds:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,bi(i,o.time_zone).format(n);var n,i,o},formatTimeWeekday:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,Di(i,o.time_zone).format(n);var n,i,o},formatTime24h:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,Ai(i,o.time_zone).format(n);var n,i,o},formatDateWeekdayDay:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,ai(i,o.time_zone).format(n);var n,i,o},formatDate:function(e){return ui(new Date(e),t._hass.locale,t._hass.config)},formatDateNumeric:function(e){return ci(new Date(e),t._hass.locale,t._hass.config)},formatDateShort:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,di(i,o.time_zone).format(n);var n,i,o},formatDateMonthYear:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,hi(i,o.time_zone).format(n);var n,i,o},formatDateMonth:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,fi(i,o.time_zone).format(n);var n,i,o},formatDateYear:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,vi(i,o.time_zone).format(n);var n,i,o},formatDateWeekday:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,pi(i,o.time_zone).format(n);var n,i,o},formatDateWeekdayShort:function(e){return n=new Date(e),i=t._hass.locale,o=t._hass.config,mi(i,o.time_zone).format(n);var n,i,o},parseDuration:function(e,n,i){var o;return void 0===n&&(n="ms"),void 0===i&&(i=null===(o=t._hass.locale)||void 0===o?void 0:o.language),So(e,n,i)},toastMessage:function(e){return t._sendToastMessage.bind(t)({message:e})},toast:function(e){return t._sendToastMessage.bind(t)(e)},runAction:function(e){var n=t._evalActions(t._config,e);t._buildActionConfig(n)}}}},{key:"_evalTemplate",value:function(t,e){try{return new Function("states","entity","user","hass","variables","html","helpers","'use strict'; ".concat(e)).call(this,this._pStates,t,this._hass.user,this._pHass,this._pVariables,Gt,this._getTemplateHelpers())}catch(Pe){var n=e.length<=100?e.trim():"".concat(e.trim().substring(0,98),"...");throw Pe.message="".concat(Pe.name,": ").concat(Pe.message," in '").concat(n,"'"),Pe.name="ButtonCardJSTemplateError",Pe}}},{key:"_objectEvalTemplate",value:function(t,e){var n=co(e);return this._getTemplateOrValue(t,n)}},{key:"_getTemplateOrValue",value:function(t,e){var n=this;if(["number","boolean","function"].includes(it(e)))return e;if(!e)return e;if("object"===it(e))return Object.keys(e).forEach((function(i){e[i]=n._getTemplateOrValue(t,e[i])})),e;var i=e.trim(),o=new RegExp("^(\\[{3,})(.*?)(\\]{3,})$","s"),r=i.match(o);return r&&4===r.length?3===r[1].length&&3===r[3].length?this._evalTemplate(t,r[2]):r[1].length===r[3].length?i.slice(1,-1):e:e}},{key:"_getColorForLightEntity",value:function(t,e,n){var i,o,r,a,u,s,c,l=Dn;return yn.includes(l)&&(l=Bn(bn)),t&&(Tn(t)?(t.attributes.rgb_color?l="rgb(".concat(t.attributes.rgb_color.join(","),")"):e&&t.attributes.color_temp&&t.attributes.min_mireds&&t.attributes.max_mireds?(i=t.attributes.color_temp,o=t.attributes.min_mireds,r=t.attributes.max_mireds,a=new dn("rgb(255, 160, 0)"),u=new dn("rgb(166, 209, 255)"),s=new dn("white"),l=(c=(i-o)/(r-o)*100)<50?u.mix(s,2*c).toRgbString():s.mix(a,2*(c-50)).toRgbString()):l=ho(t,t.state,n)||Dn,t.attributes.brightness&&(l=function(t,e,n){var i=new dn(En(t,e));if(i.isValid){var o=i.mix("black",100-n).toString();if(o)return o}return e}(this,l,(t.attributes.brightness+245)/5))):l=ho(t,t.state,n)||Dn),l}},{key:"_buildCssColorAttribute",value:function(t,e){var n,i,o="";return(null==e?void 0:e.color)?o=e.color:this._config.color&&(o=this._config.color),_n.includes(o)&&(!t||t&&"light"!==Cn(t.entity_id))&&(o=""),_n.includes(o)?this._getColorForLightEntity(t,"auto-no-temperature"!==o,null===(n=this._config)||void 0===n?void 0:n.color_type):o||(t&&ho(t,t.state,null===(i=this._config)||void 0===i?void 0:i.color_type)||Dn)}},{key:"_buildIcon",value:function(t,e){if(this._config.show_icon){var n;if(null==e?void 0:e.icon)n=e.icon;else{if(!this._config.icon)return;n=this._config.icon}return this._getTemplateOrValue(t,n)}}},{key:"_buildEntityPicture",value:function(t,e){if(this._config.show_entity_picture&&(t||e||this._config.entity_picture)){var n;(null==e?void 0:e.entity_picture)?n=e.entity_picture:this._config.entity_picture?n=this._config.entity_picture:t&&(n=t.attributes&&t.attributes.entity_picture?t.attributes.entity_picture:void 0);var i=this._getTemplateOrValue(t,n);return i&&Nn(i)?Vn(this._hass,i).then((function(t){return t.url})).catch((function(){return""})):i}}},{key:"_buildStyleGeneric",value:function(t,e,n){var i,o,r=this,a={};if((null===(i=this._config.styles)||void 0===i?void 0:i[n])&&(a=Object.assign.apply(Object,[a].concat(H(this._config.styles[n])))),null===(o=null==e?void 0:e.styles)||void 0===o?void 0:o[n]){var u={};u=Object.assign.apply(Object,[u].concat(H(e.styles[n]))),a=Object.assign(Object.assign({},a),u)}return Object.keys(a).forEach((function(e){a[e]=r._getTemplateOrValue(t,a[e])})),a}},{key:"_buildCustomStyleGeneric",value:function(t,e,n){var i,o,r,a,u=this,s={};if((null===(o=null===(i=this._config.styles)||void 0===i?void 0:i.custom_fields)||void 0===o?void 0:o[n])&&(s=Object.assign.apply(Object,[s].concat(H(this._config.styles.custom_fields[n])))),null===(a=null===(r=null==e?void 0:e.styles)||void 0===r?void 0:r.custom_fields)||void 0===a?void 0:a[n]){var c={};c=Object.assign.apply(Object,[c].concat(H(e.styles.custom_fields[n]))),s=Object.assign(Object.assign({},s),c)}return Object.keys(s).forEach((function(e){s[e]=u._getTemplateOrValue(t,s[e])})),s}},{key:"_buildName",value:function(t,e){var n,i;if(!1!==this._config.show_name)return(null==e?void 0:e.name)?n=e.name:this._config.name?n=this._config.name:t&&(n=t.attributes&&t.attributes.friendly_name?t.attributes.friendly_name:(i=t.entity_id).substr(i.indexOf(".")+1)),this._getTemplateOrValue(t,n)}},{key:"_buildStateString",value:function(t){var e;return this._config.show_state&&t&&t.state&&("timer"===Cn(t.entity_id)?"idle"===t.state||0===this._timeRemaining?e=Si(this._hass.localize,t,this._hass.locale,this._hass.config,this._hass.entities,this._config):(e=this._computeTimeDisplay(t),"paused"===t.state&&(e+=" (".concat(Si(this._hass.localize,t,this._hass.locale,this._hass.config,this._hass.entities,this._config),")"))):e=Si(this._hass.localize,t,this._hass.locale,this._hass.config,this._hass.entities,this._config)),e}},{key:"_buildLastChanged",value:function(t,e){return this._config.show_last_changed&&t?Gt(l||(l=B(['\n \n \n ',"\n \n \n "," ","\n ","\n \n "])),S,$e(A),Pe(F),$e(y),this._tooltipShow,(function(t){return u._handleAction(t,{isIcon:!1})}),ze({hasDoubleClick:this._isActionDoingSomething(this._stateObj,this._config.double_tap_action),hasHold:this._isActionDoingSomething(this._stateObj,this._config.hold_action),repeat:null==T?void 0:T.repeat,repeatLimit:null==T?void 0:T.repeat_limit,isMomentary:this._cardMomentary,disableKbd:null===(a=this._config)||void 0===a?void 0:a.disable_kbd}),this._config,this._buttonContent(this._stateObj,s,_),!this._cardRipple,this._getLock(b),this._getSpinner(D,s),this._getTooltip(k,s))}},{key:"_getTooltip",value:function(t,e){var n,i,o,r,a,u,s;u="string"==typeof this._config.tooltip?{content:this._getTemplateOrValue(this._stateObj,this._config.tooltip)}:null!==(n=this._objectEvalTemplate(this._stateObj,this._config.tooltip))&&void 0!==n?n:{},s="string"==typeof(null==e?void 0:e.tooltip)?{content:this._getTemplateOrValue(this._stateObj,null==e?void 0:e.tooltip)}:null!==(i=this._objectEvalTemplate(this._stateObj,null==e?void 0:e.tooltip))&&void 0!==i?i:{};var c=Object.assign(Object.assign({},u),s);if(c&&c.content){var l=So(String(null!==(o=null==c?void 0:c.delay)&&void 0!==o?o:"150"),"ms","en"),d=So(String(null!==(a=null!==(r=null==c?void 0:c.hide_delay)&&void 0!==r?r:null==c?void 0:c.delay)&&void 0!==a?a:"150"),"ms","en"),h=!(null==c?void 0:c.arrow)||void 0;return Gt(_||(_=B(['\n \n ',"\n \n "])),this._tooltipShow,fe(c.placement||void 0),fe(c.distance||void 0),fe(c.skidding||void 0),l,d,h||Wt,$e(t),this._unsafeHTMLorNot(c.content))}return Gt(y||(y=B([""])))}},{key:"_getSpinner",value:function(t,e){var n=this._getTemplateOrValue(this._stateObj,null==e?void 0:e.spinner)||this._getTemplateOrValue(this._stateObj,this._config.spinner);return this._spinnerActive||n?Gt(b||(b=B(['\n
\n
\n \n
\n '])),$e(t)):Gt(D||(D=B([""])))}},{key:"_getLock",value:function(t){var e;return this._config.lock&&this._getTemplateOrValue(this._stateObj,this._config.lock.enabled)?Gt(A||(A=B(['\n \n =100&&n!==this._updateTimerDuration)return!0}return!1}},{key:"_updateTimer",value:function(){var t,e=this;if(this._updateTimeout&&(window.clearTimeout(this._updateTimeout),this._updateTimeout=void 0),null===(t=this._config)||void 0===t?void 0:t.update_timer){var n=this._getTemplateOrValue(this._stateObj,this._config.update_timer),i=So(n,"ms","en");i&&i>=100&&(this._updateTimerDuration=i,this._updateTimeout=window.setTimeout((function(){e._updateRefresh()}),i))}}},{key:"_updateRefresh",value:function(){this._updateTimerMS=Date.now(),this._updateTimeout=void 0}},{key:"getCardSize",value:function(){var t;return(null===(t=this._config)||void 0===t?void 0:t.card_size)||3}},{key:"getGridOptions",value:function(){var t;if(null===(t=this._config)||void 0===t?void 0:t.section_mode)return{rows:2,columns:6,min_rows:1,min_columns:1}}},{key:"_partialActionEval",value:function(t){var e=this;if(!t)return{action:"none"};if("string"==typeof t)return this._objectEvalTemplate(this._stateObj,t);var n=co(t);return["action","repeat","repeat_limit"].forEach((function(t){n[t]=e._getTemplateOrValue(e._stateObj,n[t])})),n}},{key:"_evalActions",value:function(t,e){var n,i;i="string"==typeof e?this._objectEvalTemplate(this._stateObj,e):co(e);var o=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.action);if("none"===o||!o){var r={};return r[wn]={action:"none"},r}var a=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.repeat),u=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.repeat_limit),s=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.sound),c=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.confirmation);!c&&t.confirmation&&(c=this._objectEvalTemplate(this._stateObj,t.confirmation));var l=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.haptic),d=Object.assign(Object.assign({},this._objectEvalTemplate(this._stateObj,t.protect)),this._objectEvalTemplate(this._stateObj,null==i?void 0:i.protect)),h={};switch(o){case"javascript":h[wn]={action:"fire-dom-event",buttonCardCustomAction:{callback:this._customActionsCallback.bind(this),type:"javascript",data:{javascript:null==i?void 0:i.javascript}}};break;case"multi-actions":h[wn]={action:"fire-dom-event",buttonCardCustomAction:{callback:this._customActionsCallback.bind(this),type:"multi-actions",data:{multiActions:null==i?void 0:i.actions}}};break;case"toast":h[wn]={action:"fire-dom-event",buttonCardCustomAction:{callback:this._customActionsCallback.bind(this),type:"toast",data:{toast:null==i?void 0:i.toast}}};break;case"toggle":h.entity=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.entity)||this._getTemplateOrValue(this._stateObj,t.entity),h[wn]={action:"toggle"};break;case"more-info":h.entity=this._getTemplateOrValue(this._stateObj,null==i?void 0:i.entity)||this._getTemplateOrValue(this._stateObj,t.entity),h[wn]={action:"more-info"};break;case"navigate":h[wn]={action:"navigate",navigation_path:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.navigation_path),navigation_replace:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.navigation_replace)};break;case"url":h[wn]={action:"url",url_path:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.url_path)};break;case"perform-action":case"call-service":h[wn]={action:"perform-action",perform_action:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.perform_action)||this._getTemplateOrValue(this._stateObj,null==i?void 0:i.service),data:this._objectEvalTemplate(this._stateObj,null==i?void 0:i.data)||this._objectEvalTemplate(this._stateObj,null==i?void 0:i.service_data),target:this._objectEvalTemplate(this._stateObj,null==i?void 0:i.target)},"entity"===(null===(n=h[wn].data)||void 0===n?void 0:n.entity_id)&&(h[wn].data.entity_id=this._getTemplateOrValue(this._stateObj,t.entity));break;case"assist":h[wn]={action:"assist",pipeline_id:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.pipeline_id),start_listening:this._getTemplateOrValue(this._stateObj,null==i?void 0:i.start_listening)};break;case"fire-dom-event":h[wn]=Object.assign({action:"fire-dom-event"},this._objectEvalTemplate(this._stateObj,i));break;default:return x({},wn,{action:"none"})}return h[wn]=Object.assign(Object.assign({},h[wn]),{repeat:a,repeat_limit:u,sound:s,haptic:l,confirmation:c,protect:d}),d&&(d.password||d.pin)&&(this._protectedAction=co(h)),h}},{key:"_handleRippleIcon",value:function(t){this._ripple.then((function(e){var n,i;if(e)if("pointerenter"===t.type){var o=null!==(i=null===(n=t.target)||void 0===n?void 0:n.getBoundingClientRect())&&void 0!==i?i:null,r=e.getBoundingClientRect(),a={top:0,left:0,bottom:0,right:0},u=t.target?getComputedStyle(t.target):null,s=u?parseInt(u.getPropertyValue("--button-card-ripple-icon-inset-padding")):12,c="";o&&r&&(a.top=o.top-r.top-s,a.top=a.top<0?0:a.top,a.left=o.left-r.left-s,a.left=a.left<0?0:a.left,a.bottom=r.bottom-o.bottom-s,a.bottom=a.bottom<0?0:a.bottom,a.right=r.right-o.right-s,a.right=a.right<0?0:a.right,c="".concat(a.top,"px ").concat(a.right,"px ").concat(a.bottom,"px ").concat(a.left,"px")),e.setAttribute("icon",""),""!=c&&e.style.setProperty("--dynamic-ripple-icon-inset",c)}else"pointerleave"===t.type&&(e.removeAttribute("icon"),e.style.removeProperty("--dynamic-ripple-icon-inset"))}))}},{key:"_hapticInterceptHandler",value:function(t){this._hapticCapture&&t.stopPropagation&&("failure"!==t.detail&&t.stopPropagation())}},{key:"_handleAction",value:function(t,e){var n;if(e.isIcon&&this._hasIconActions&&t.stopPropagation&&t.stopPropagation(),null===(n=t.detail)||void 0===n?void 0:n.action){var i=this._config;if(!i)return;var o=t.detail.action,r=e.isIcon?"icon_".concat(o,"_action"):"".concat(o,"_action");if(this._isActionDoingSomething(this._stateObj,i[r])){var a=this._evalActions(i,i[r]);this._buildActionConfig(a)}}}},{key:"_buildActionConfig",value:function(t){var e,n,i,o,r=this;t&&((null===(n=null===(e=t[wn])||void 0===e?void 0:e.protect)||void 0===n?void 0:n.pin)?window.cardHelpers.showEnterCodeDialog(this,{submit:function(t){return r._protectedConfirmedCallback.bind(r)(t,"pin")},cancel:this._cancelledCallback.bind(this),codeFormat:"number"}):(null===(o=null===(i=t[wn])||void 0===i?void 0:i.protect)||void 0===o?void 0:o.password)?window.cardHelpers.showPromptDialog(this,{title:"Password",inputLabel:"Password",inputType:"password",confirm:function(t){return r._protectedConfirmedCallback.bind(r)(t,"password")},cancel:this._cancelledCallback.bind(this)}):this._executeAction(t))}},{key:"_executeAction",value:function(t){var e,n,i=null===(e=t[wn])||void 0===e?void 0:e.sound;i&&(Nn(i)?Vn(this._hass,i).then((function(t){new Audio(t.url).play()})).catch((function(){console.error("button-card: Error loading media source: ".concat(i))})):new Audio(i).play());var o=null===(n=t[wn])||void 0===n?void 0:n.haptic;this._hapticCapture=void 0!==o,po(this,this._hass,t,"tap"),this._hapticCapture=!1,o&&"none"!=o&&He(this,"haptic",o)}},{key:"_customActionsCallback",value:(n=z(R().m((function t(e){var n,i,o,r,a,u,s,c,l,d,h,f,v,p,m,g,_,y,b,D,A,w,C,E;return R().w((function(t){for(;;)switch(t.p=t.n){case 0:if(e.detail&&e.detail.buttonCardCustomAction){t.n=1;break}return t.a(2);case 1:d=e.detail.buttonCardCustomAction,C=d.type,t.n="javascript"===C?2:"multi-actions"===C?3:"toast"===C?19:20;break;case 2:return this._getTemplateOrValue(this._stateObj,null===(n=d.data)||void 0===n?void 0:n.javascript),t.a(3,20);case 3:if("string"==typeof(h=null===(i=d.data)||void 0===i?void 0:i.multiActions)&&(h=this._objectEvalTemplate(this._stateObj,h)),f=function(t){return new Promise((function(e){return setTimeout(e,t)}))},!Array.isArray(h)){t.n=18;break}this._spinnerActive=h.some((function(t){return"string"!=typeof t&&((null==t?void 0:t.delay)||(null==t?void 0:t.wait_completion))})),v=K(h),t.p=4,v.s();case 5:if((p=v.n()).done){t.n=14;break}if("string"==typeof(m=p.value)||!(null==m?void 0:m.delay)){t.n=7;break}if(g=this._getTemplateOrValue(this._stateObj,m.delay),!(g=So(g||"","ms","en"))){t.n=6;break}return t.n=6,f(g);case 6:t.n=13;break;case 7:if("string"==typeof m||!(null==m?void 0:m.wait_completion)){t.n=12;break}return _=m,t.n=8,f(500);case 8:y=this._getTemplateOrValue(this._stateObj,_.timeout),b=0,D=So(y||"","ms","en")||0;case 9:if(!(b","license":"MIT","dependencies":{"@babel/polyfill":"^7.4.4","@jaames/iro":"^5.5.2","@lit-labs/scoped-registry-mixin":"^1.0.0","@material/mwc-icon":"^0.25.3","@material/mwc-list":"^0.25.3","@material/mwc-menu":"^0.25.3","@material/mwc-notched-outline":"^0.25.3","@material/mwc-select":"^0.25.3","core-js":"^2.6.5","lit":"^2.1.2","lit-element":"^2.2.1"},"devDependencies":{"@babel/cli":"^7.5.5","@babel/core":"^7.5.5","@babel/preset-env":"^7.5.5","babel-loader":"^8.0.6","eslint":"^6.1.0","webpack":"^4.38.0","webpack-cli":"^3.3.6","webpack-merge":"^4.2.1"},"scripts":{"lint":"eslint --fix ./src","start":"webpack --watch --config webpack/config.dev.js","build":"webpack --config webpack/config.prod.js"}}')},function(t,e,i){"use strict";i.r(e); +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const n=window,r=n.ShadowRoot&&(void 0===n.ShadyCSS||n.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;class a{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(r&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}}const l=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1],t[0]);return new a(i,t,o)},c=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new a("string"==typeof t?t:t+"",void 0,o))(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var h;const u=window,d=u.trustedTypes,f=d?d.emptyScript:"",p=u.reactiveElementPolyfillSupport,g={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},v=(t,e)=>e!==t&&(e==e||t==t),_={attribute:!0,type:String,converter:g,reflect:!1,hasChanged:v};class y extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(t){var e;this.finalize(),(null!==(e=this.h)&&void 0!==e?e:this.h=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach((e,i)=>{const n=this._$Ep(i,e);void 0!==n&&(this._$Ev.set(n,i),t.push(n))}),t}static createProperty(t,e=_){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){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 r=this[t];this[e]=n,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||_}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(c(t))}else void 0!==t&&e.push(c(t));return e}static _$Ep(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._$E_=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach(t=>t(this))}addController(t){var e,i;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$ES)||void 0===e||e.splice(this._$ES.indexOf(t)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach((t,e)=>{this.hasOwnProperty(e)&&(this._$Ei.set(e,this[e]),delete this[e])})}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,e)=>{r?t.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet):e.forEach(e=>{const i=document.createElement("style"),r=n.litNonce;void 0!==r&&i.setAttribute("nonce",r),i.textContent=e.cssText,t.appendChild(i)})})(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$EO(t,e,i=_){var n;const r=this.constructor._$Ep(t,i);if(void 0!==r&&!0===i.reflect){const o=(void 0!==(null===(n=i.converter)||void 0===n?void 0:n.toAttribute)?i.converter:g).toAttribute(e,i.type);this._$El=t,null==o?this.removeAttribute(r):this.setAttribute(r,o),this._$El=null}}_$AK(t,e){var i;const n=this.constructor,r=n._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=n.getPropertyOptions(r),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(i=t.converter)||void 0===i?void 0:i.fromAttribute)?t.converter:g;this._$El=r,this[r]=o.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||v)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach((t,e)=>this[e]=t),this._$Ei=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)}),this.update(i)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$ES)||void 0===e||e.forEach(t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)}),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach((t,e)=>this._$EO(e,this[e],t)),this._$EC=void 0),this._$Ek()}updated(t){}firstUpdated(t){}}function b(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&m(t,e)}function m(t,e){return(m=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function w(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var i,n=k(t);if(e){var r=k(this).constructor;i=Reflect.construct(n,arguments,r)}else i=n.apply(this,arguments);return $(this,i)}}function $(t,e){if(e&&("object"===j(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t)}function k(t){return(k=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function x(t,e){var i="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!i){if(Array.isArray(t)||(i=E(t))||e&&t&&"number"==typeof t.length){i&&(t=i);var n=0,r=function(){};return{s:r,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){i=i.call(t)},n:function(){var t=i.next();return s=t.done,t},e:function(t){a=!0,o=t},f:function(){try{s||null==i.return||i.return()}finally{if(a)throw o}}}}function S(t){return function(t){if(Array.isArray(t))return C(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||E(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function A(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var i=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==i)return;var n,r,o=[],s=!0,a=!1;try{for(i=i.call(t);!(s=(n=i.next()).done)&&(o.push(n.value),!e||o.length!==e);s=!0);}catch(t){a=!0,r=t}finally{try{s||null==i.return||i.return()}finally{if(a)throw r}}return o}(t,e)||E(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function E(t,e){if(t){if("string"==typeof t)return C(t,e);var i=Object.prototype.toString.call(t).slice(8,-1);return"Object"===i&&t.constructor&&(i=t.constructor.name),"Map"===i||"Set"===i?Array.from(t):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?C(t,e):void 0}}function C(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,n=new Array(e);i"),D=document,W=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return D.createComment(t)},B=function(t){return null===t||"object"!=j(t)&&"function"!=typeof t},F=Array.isArray,z=function(t){return F(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])},V=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,q=/-->/g,K=/>/g,G=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),J=/'/g,Z=/"/g,X=/^(?:script|style|textarea|title)$/i,Y=function(t){return function(e){for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r":"",s=V,a=0;a"===h[0]?(s=null!=i?i:V,u=-1):void 0===h[1]?u=-2:(u=s.lastIndex-h[2].length,c=h[1],s=void 0===h[3]?G:'"'===h[3]?Z:J):s===Z||s===J?s=G:s===q||s===K?s=V:(s=G,i=void 0);var f=s===G&&t[a+1].startsWith("/>")?" ":"";o+=s===V?l+L:u>=0?(r.push(c),l.slice(0,u)+"$lit$"+l.slice(u)+N+f):l+N+(-2===u?(r.push(void 0),a):f)}var p=o+(t[n]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==H?H.createHTML(p):p,r]},ot=function(){function t(e,i){var n,r=e.strings,o=e._$litType$;O(this,t),this.parts=[];var s=0,a=0,l=r.length-1,c=this.parts,h=A(rt(r,o),2),u=h[0],d=h[1];if(this.el=t.createElement(u,i),nt.currentNode=this.el.content,2===o){var f=this.el.content,p=f.firstChild;p.remove(),f.append.apply(f,S(p.childNodes))}for(;null!==(n=nt.nextNode())&&c.length0){n.textContent=M?M.emptyScript:"";for(var T=0;T2&&void 0!==arguments[2]?arguments[2]:t,a=arguments.length>3?arguments[3]:void 0;if(e===tt)return e;var l=void 0!==a?null===(i=s._$Co)||void 0===i?void 0:i[a]:s._$Cl,c=B(e)?void 0:e._$litDirective$;return(null==l?void 0:l.constructor)!==c&&(null===(n=null==l?void 0:l._$AO)||void 0===n||n.call(l,!1),void 0===c?l=void 0:(l=new c(t))._$AT(t,s,a),void 0!==a?(null!==(r=(o=s)._$Co)&&void 0!==r?r:o._$Co=[])[a]=l:s._$Cl=l),void 0!==l&&(e=st(t,l._$AS(t,e.values),l,a)),e}var at=function(){function t(e,i){O(this,t),this.u=[],this._$AN=void 0,this._$AD=e,this._$AM=i}return T(t,[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"v",value:function(t){var e,i=this._$AD,n=i.el.content,r=i.parts,o=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:D).importNode(n,!0);nt.currentNode=o;for(var s=nt.nextNode(),a=0,l=0,c=r[0];void 0!==c;){if(a===c.index){var h=void 0;2===c.type?h=new lt(s,s.nextSibling,this,t):1===c.type?h=new c.ctor(s,c.name,c.strings,this,t):6===c.type&&(h=new pt(s,this,t)),this.u.push(h),c=r[++l]}a!==(null==c?void 0:c.index)&&(s=nt.nextNode(),a++)}return o}},{key:"p",value:function(t){var e,i=0,n=x(this.u);try{for(n.s();!(e=n.n()).done;){var r=e.value;void 0!==r&&(void 0!==r.strings?(r._$AI(t,r,i),i+=r.strings.length-2):r._$AI(t[i])),i++}}catch(t){n.e(t)}finally{n.f()}}}]),t}(),lt=function(){function t(e,i,n,r){var o;O(this,t),this.type=2,this._$AH=et,this._$AN=void 0,this._$AA=e,this._$AB=i,this._$AM=n,this.options=r,this._$Cm=null===(o=null==r?void 0:r.isConnected)||void 0===o||o}return T(t,[{key:"_$AU",get:function(){var t,e;return null!==(e=null===(t=this._$AM)||void 0===t?void 0:t._$AU)&&void 0!==e?e:this._$Cm}},{key:"parentNode",get:function(){var t=this._$AA.parentNode,e=this._$AM;return void 0!==e&&11===t.nodeType&&(t=e.parentNode),t}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this;t=st(this,t,e),B(t)?t===et||null==t||""===t?(this._$AH!==et&&this._$AR(),this._$AH=et):t!==this._$AH&&t!==tt&&this.g(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):z(t)?this.k(t):this.g(t)}},{key:"O",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this._$AB;return this._$AA.parentNode.insertBefore(t,e)}},{key:"T",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}},{key:"g",value:function(t){this._$AH!==et&&B(this._$AH)?this._$AA.nextSibling.data=t:this.T(D.createTextNode(t)),this._$AH=t}},{key:"$",value:function(t){var e,i=t.values,n=t._$litType$,r="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=ot.createElement(n.h,this.options)),n);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.p(i);else{var o=new at(r,this),s=o.v(this.options);o.p(i),this.T(s),this._$AH=o}}},{key:"_$AC",value:function(t){var e=it.get(t.strings);return void 0===e&&it.set(t.strings,e=new ot(t)),e}},{key:"k",value:function(e){F(this._$AH)||(this._$AH=[],this._$AR());var i,n,r=this._$AH,o=0,s=x(e);try{for(s.s();!(n=s.n()).done;){var a=n.value;o===r.length?r.push(i=new t(this.O(W()),this.O(W()),this,this.options)):i=r[o],i._$AI(a),o++}}catch(t){s.e(t)}finally{s.f()}o0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,i=arguments.length>1?arguments[1]:void 0;for(null===(t=this._$AP)||void 0===t||t.call(this,!1,!0,i);e&&e!==this._$AB;){var n=e.nextSibling;e.remove(),e=n}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cm=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}]),t}(),ct=function(){function t(e,i,n,r,o){O(this,t),this.type=1,this._$AH=et,this._$AN=void 0,this.element=e,this.name=i,this._$AM=r,this.options=o,n.length>2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=et}return T(t,[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,i=arguments.length>2?arguments[2]:void 0,n=arguments.length>3?arguments[3]:void 0,r=this.strings,o=!1;if(void 0===r)t=st(this,t,e,0),(o=!B(t)||t!==this._$AH&&t!==tt)&&(this._$AH=t);else{var s,a,l=t;for(t=r[0],s=0;s1&&void 0!==arguments[1]?arguments[1]:this;if((t=null!==(e=st(this,t,i,0))&&void 0!==e?e:et)!==tt){var n=this._$AH,r=t===et&&n!==et||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,o=t!==et&&(n===et||r);r&&this.element.removeEventListener(this.name,this,n),o&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,i;"function"==typeof this._$AH?this._$AH.call(null!==(i=null===(e=this.options)||void 0===e?void 0:e.host)&&void 0!==i?i:this.element,t):this._$AH.handleEvent(t)}}]),i}(ct),pt=function(){function t(e,i,n){O(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=i,this.options=n}return T(t,[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){st(this,t)}}]),t}(),gt=I.litHtmlPolyfillSupport;null==gt||gt(ot,lt),(null!==(R=I.litHtmlVersions)&&void 0!==R?R:I.litHtmlVersions=[]).push("2.6.1");var vt,_t;function yt(t){return(yt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function bt(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function mt(t,e){for(var i=0;i3)for(i=[i],n=3;n-1,n=parseFloat(t);return i?e/100*n:n}function $e(t){return parseInt(t,16)}function ke(t){return t.toString(16).padStart(2,"0")}var xe=function(){function t(t,e){this.$={h:0,s:0,v:0,a:1},t&&this.set(t),this.onChange=e,this.initialValue=oe({},this.$)}var e,i,n,r=t.prototype;return r.set=function(e){if("string"==typeof e)/^(?:#?|0x?)[0-9a-fA-F]{3,8}$/.test(e)?this.hexString=e:/^rgba?/.test(e)?this.rgbString=e:/^hsla?/.test(e)&&(this.hslString=e);else{if("object"!=typeof e)throw new Error("Invalid color value");e instanceof t?this.hsva=e.hsva:"r"in e&&"g"in e&&"b"in e?this.rgb=e:"h"in e&&"s"in e&&"v"in e?this.hsv=e:"h"in e&&"s"in e&&"l"in e?this.hsl=e:"kelvin"in e&&(this.kelvin=e.kelvin)}},r.setChannel=function(t,e,i){var n;this[t]=oe({},this[t],((n={})[e]=i,n))},r.reset=function(){this.hsva=this.initialValue},r.clone=function(){return new t(this)},r.unbind=function(){this.onChange=void 0},t.hsvToRgb=function(t){var e=t.h/60,i=t.s/100,n=t.v/100,r=be(e),o=e-r,s=n*(1-i),a=n*(1-o*i),l=n*(1-(1-o)*i),c=r%6,h=[l,n,n,a,s,s][c],u=[s,s,l,n,n,a][c];return{r:me(255*[n,a,s,s,l,n][c],0,255),g:me(255*h,0,255),b:me(255*u,0,255)}},t.rgbToHsv=function(t){var e=t.r/255,i=t.g/255,n=t.b/255,r=Math.max(e,i,n),o=Math.min(e,i,n),s=r-o,a=0,l=r,c=0===r?0:s/r;switch(r){case o:a=0;break;case e:a=(i-n)/s+(i.4;){i=.5*(s+o);var a=t.kelvinToRgb(i);a.b/a.r>=r/n?s=i:o=i}return i},e=t,(i=[{key:"hsv",get:function(){var t=this.$;return{h:t.h,s:t.s,v:t.v}},set:function(t){var e=this.$;if(t=oe({},e,t),this.onChange){var i={h:!1,v:!1,s:!1,a:!1};for(var n in e)i[n]=t[n]!=e[n];this.$=t,(i.h||i.s||i.v||i.a)&&this.onChange(this,i)}else this.$=t}},{key:"hsva",get:function(){return oe({},this.$)},set:function(t){this.hsv=t}},{key:"hue",get:function(){return this.$.h},set:function(t){this.hsv={h:t}}},{key:"saturation",get:function(){return this.$.s},set:function(t){this.hsv={s:t}}},{key:"value",get:function(){return this.$.v},set:function(t){this.hsv={v:t}}},{key:"alpha",get:function(){return this.$.a},set:function(t){this.hsv=oe({},this.hsv,{a:t})}},{key:"kelvin",get:function(){return t.rgbToKelvin(this.rgb)},set:function(e){this.rgb=t.kelvinToRgb(e)}},{key:"red",get:function(){return this.rgb.r},set:function(t){this.rgb=oe({},this.rgb,{r:t})}},{key:"green",get:function(){return this.rgb.g},set:function(t){this.rgb=oe({},this.rgb,{g:t})}},{key:"blue",get:function(){return this.rgb.b},set:function(t){this.rgb=oe({},this.rgb,{b:t})}},{key:"rgb",get:function(){var e=t.hsvToRgb(this.$),i=e.r,n=e.g,r=e.b;return{r:ye(i),g:ye(n),b:ye(r)}},set:function(e){this.hsv=oe({},t.rgbToHsv(e),{a:void 0===e.a?1:e.a})}},{key:"rgba",get:function(){return oe({},this.rgb,{a:this.alpha})},set:function(t){this.rgb=t}},{key:"hsl",get:function(){var e=t.hsvToHsl(this.$),i=e.h,n=e.s,r=e.l;return{h:ye(i),s:ye(n),l:ye(r)}},set:function(e){this.hsv=oe({},t.hslToHsv(e),{a:void 0===e.a?1:e.a})}},{key:"hsla",get:function(){return oe({},this.hsl,{a:this.alpha})},set:function(t){this.hsl=t}},{key:"rgbString",get:function(){var t=this.rgb;return"rgb("+t.r+", "+t.g+", "+t.b+")"},set:function(t){var e,i,n,r,o=1;if((e=ce.exec(t))?(i=we(e[1],255),n=we(e[2],255),r=we(e[3],255)):(e=he.exec(t))&&(i=we(e[1],255),n=we(e[2],255),r=we(e[3],255),o=we(e[4],1)),!e)throw new Error("Invalid rgb string");this.rgb={r:i,g:n,b:r,a:o}}},{key:"rgbaString",get:function(){var t=this.rgba;return"rgba("+t.r+", "+t.g+", "+t.b+", "+t.a+")"},set:function(t){this.rgbString=t}},{key:"hexString",get:function(){var t=this.rgb;return"#"+ke(t.r)+ke(t.g)+ke(t.b)},set:function(t){var e,i,n,r,o=255;if((e=fe.exec(t))?(i=17*$e(e[1]),n=17*$e(e[2]),r=17*$e(e[3])):(e=pe.exec(t))?(i=17*$e(e[1]),n=17*$e(e[2]),r=17*$e(e[3]),o=17*$e(e[4])):(e=ge.exec(t))?(i=$e(e[1]),n=$e(e[2]),r=$e(e[3])):(e=ve.exec(t))&&(i=$e(e[1]),n=$e(e[2]),r=$e(e[3]),o=$e(e[4])),!e)throw new Error("Invalid hex string");this.rgb={r:i,g:n,b:r,a:o/255}}},{key:"hex8String",get:function(){var t=this.rgba;return"#"+ke(t.r)+ke(t.g)+ke(t.b)+ke(be(255*t.a))},set:function(t){this.hexString=t}},{key:"hslString",get:function(){var t=this.hsl;return"hsl("+t.h+", "+t.s+"%, "+t.l+"%)"},set:function(t){var e,i,n,r,o=1;if((e=ue.exec(t))?(i=we(e[1],360),n=we(e[2],100),r=we(e[3],100)):(e=de.exec(t))&&(i=we(e[1],360),n=we(e[2],100),r=we(e[3],100),o=we(e[4],1)),!e)throw new Error("Invalid hsl string");this.hsl={h:i,s:n,l:r,a:o}}},{key:"hslaString",get:function(){var t=this.hsla;return"hsla("+t.h+", "+t.s+"%, "+t.l+"%, "+t.a+")"},set:function(t){this.hslString=t}}])&&re(e.prototype,i),n&&re(e,n),t}();function Se(t){var e,i=t.width,n=t.sliderSize,r=t.borderWidth,o=t.handleRadius,s=t.padding,a=t.sliderShape,l="horizontal"===t.layoutDirection;return n=null!=(e=n)?e:2*s+2*o,"circle"===a?{handleStart:t.padding+t.handleRadius,handleRange:i-2*s-2*o,width:i,height:i,cx:i/2,cy:i/2,radius:i/2-r/2}:{handleStart:n/2,handleRange:i-n,radius:n/2,x:0,y:0,width:l?n:i,height:l?i:n}}function Ae(t,e){var i=Se(t),n=i.width,r=i.height,o=i.handleRange,s=i.handleStart,a="horizontal"===t.layoutDirection,l=a?n/2:r/2,c=s+function(t,e){var i=e.hsva,n=e.rgb;switch(t.sliderType){case"red":return n.r/2.55;case"green":return n.g/2.55;case"blue":return n.b/2.55;case"alpha":return 100*i.a;case"kelvin":var r=t.minTemperature,o=t.maxTemperature-r,s=(e.kelvin-r)/o*100;return Math.max(0,Math.min(s,100));case"hue":return i.h/=3.6;case"saturation":return i.s;case"value":default:return i.v}}(t,e)/100*o;return a&&(c=-1*c+o+2*s),{x:a?l:c,y:a?c:l}}var Ee,Ce=2*Math.PI,Oe=function(t,e){return Math.sqrt(t*t+e*e)};function Pe(t){return t.width/2-t.padding-t.handleRadius-t.borderWidth}function Te(t){var e=t.width/2;return{width:t.width,radius:e-t.borderWidth,cx:e,cy:e}}function je(t,e,i){var n=t.wheelAngle,r=t.wheelDirection;return i&&"clockwise"===r?e=n+e:"clockwise"===r?e=360-n+e:i&&"anticlockwise"===r?e=n+180-e:"anticlockwise"===r&&(e=n-e),function(t,e){return(t%e+e)%e}(e,360)}function Re(t,e,i){var n=Te(t),r=n.cx,o=n.cy,s=Pe(t);e=r-e,i=o-i;var a=je(t,Math.atan2(-i,-e)*(360/Ce)),l=Math.min(Oe(e,i),s);return{h:Math.round(a),s:Math.round(100/s*l)}}function Ie(t){var e=t.width,i=t.boxHeight;return{width:e,height:null!=i?i:e,radius:t.padding+t.handleRadius}}function Me(t,e,i){var n=Ie(t),r=n.width,o=n.height,s=n.radius,a=(e-s)/(r-2*s)*100,l=(i-s)/(o-2*s)*100;return{s:Math.max(0,Math.min(a,100)),v:Math.max(0,Math.min(100-l,100))}}function He(t){Ee||(Ee=document.getElementsByTagName("base"));var e=window.navigator.userAgent,i=/^((?!chrome|android).)*safari/i.test(e),n=/iPhone|iPod|iPad/i.test(e),r=window.location;return(i||n)&&Ee.length>0?r.protocol+"//"+r.host+r.pathname+r.search+t:t}function Ne(t,e,i,n){for(var r=0;r0&&(o[n?"marginLeft":"marginTop"]=r),Dt(Bt,null,t.children(this.uid,i,o))},e.prototype.handleEvent=function(t){var e=this,i=this.props.onInput,n=this.base.getBoundingClientRect();t.preventDefault();var r=t.touches?t.changedTouches[0]:t,o=r.clientX-n.left,s=r.clientY-n.top;switch(t.type){case"mousedown":case"touchstart":!1!==i(o,s,0)&&We.forEach((function(t){document.addEventListener(t,e,{passive:!1})}));break;case"mousemove":case"touchmove":i(o,s,1);break;case"mouseup":case"touchend":i(o,s,2),We.forEach((function(t){document.removeEventListener(t,e,{passive:!1})}))}},e}(Ft);function Fe(t){var e=t.r,i=t.url,n=e,r=e;return Dt("svg",{className:"IroHandle IroHandle--"+t.index+" "+(t.isActive?"IroHandle--isActive":""),style:{"-webkit-tap-highlight-color":"rgba(0, 0, 0, 0);",transform:"translate("+De(t.x)+", "+De(t.y)+")",willChange:"transform",top:De(-e),left:De(-e),width:De(2*e),height:De(2*e),position:"absolute",overflow:"visible"}},i&&Dt("use",Object.assign({xlinkHref:He(i)},t.props)),!i&&Dt("circle",{cx:n,cy:r,r:e,fill:"none","stroke-width":2,stroke:"#000"}),!i&&Dt("circle",{cx:n,cy:r,r:e-2,fill:t.fill,"stroke-width":2,stroke:"#fff"}))}function ze(t){var e=t.activeIndex,i=void 0!==e&&e0?e.colors:[e.color]).forEach((function(t){return i.addColor(t)})),this.setActiveColor(0),this.state=Object.assign({},e,{color:this.color,colors:this.colors,layout:e.layout})}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.addColor=function(t,e){void 0===e&&(e=this.colors.length);var i=new xe(t,this.onColorChange.bind(this));this.colors.splice(e,0,i),this.colors.forEach((function(t,e){return t.index=e})),this.state&&this.setState({colors:this.colors}),this.deferredEmit("color:init",i)},e.prototype.removeColor=function(t){var e=this.colors.splice(t,1)[0];e.unbind(),this.colors.forEach((function(t,e){return t.index=e})),this.state&&this.setState({colors:this.colors}),e.index===this.color.index&&this.setActiveColor(0),this.emit("color:remove",e)},e.prototype.setActiveColor=function(t){this.color=this.colors[t],this.state&&this.setState({color:this.color}),this.emit("color:setActive",this.color)},e.prototype.setColors=function(t,e){var i=this;void 0===e&&(e=0),this.colors.forEach((function(t){return t.unbind()})),this.colors=[],t.forEach((function(t){return i.addColor(t)})),this.setActiveColor(e),this.emit("color:setAll",this.colors)},e.prototype.on=function(t,e){var i=this,n=this.events;(Array.isArray(t)?t:[t]).forEach((function(t){(n[t]||(n[t]=[])).push(e),i.deferredEvents[t]&&(i.deferredEvents[t].forEach((function(t){e.apply(null,t)})),i.deferredEvents[t]=[])}))},e.prototype.off=function(t,e){var i=this;(Array.isArray(t)?t:[t]).forEach((function(t){var n=i.events[t];n&&n.splice(n.indexOf(e),1)}))},e.prototype.emit=function(t){for(var e=this,i=[],n=arguments.length-1;n-- >0;)i[n]=arguments[n+1];var r=this.activeEvents,o=!!r.hasOwnProperty(t)&&r[t];if(!o){r[t]=!0;var s=this.events[t]||[];s.forEach((function(t){return t.apply(e,i)})),r[t]=!1}},e.prototype.deferredEmit=function(t){for(var e,i=[],n=arguments.length-1;n-- >0;)i[n]=arguments[n+1];var r=this.deferredEvents;(e=this).emit.apply(e,[t].concat(i)),(r[t]||(r[t]=[])).push(i)},e.prototype.setOptions=function(t){this.setState(t)},e.prototype.resize=function(t){this.setOptions({width:t})},e.prototype.reset=function(){this.colors.forEach((function(t){return t.reset()})),this.setState({colors:this.colors})},e.prototype.onMount=function(t){this.el=t,this.deferredEmit("mount",this)},e.prototype.onColorChange=function(t,e){this.setState({color:this.color}),this.inputActive&&(this.inputActive=!1,this.emit("input:change",t,e)),this.emit("color:change",t,e)},e.prototype.emitInputEvent=function(t,e){0===t?this.emit("input:start",this.color,e):1===t?this.emit("input:move",this.color,e):2===t&&this.emit("input:end",this.color,e)},e.prototype.render=function(t,e){var i=this,n=e.layout;return Array.isArray(n)||(n=[{component:qe},{component:ze}],e.transparency&&n.push({component:ze,options:{sliderType:"alpha"}})),Dt("div",{class:"IroColorPicker",id:e.id,style:{display:e.display}},n.map((function(t,n){var r=t.component,o=t.options;return Dt(r,Object.assign({},e,o,{ref:void 0,onInput:i.emitInputEvent.bind(i),parent:i,index:n}))})))},e}(Ft);Ke.defaultProps=Object.assign({},{width:300,height:300,color:"#fff",colors:[],padding:6,layoutDirection:"vertical",borderColor:"#fff",borderWidth:0,handleRadius:8,activeHandleRadius:null,handleSvg:null,handleProps:{x:0,y:0},wheelLightness:!0,wheelAngle:0,wheelDirection:"anticlockwise",sliderSize:null,sliderMargin:12,boxHeight:null},{colors:[],display:"block",id:null,layout:"default",margin:null});var Ge,Je,Ze,Xe=((Je=function(t,e){var i,n=document.createElement("div");function r(){var e=t instanceof Element?t:document.querySelector(t);e.appendChild(i.base),i.onMount(e)}return function(t,e,i){var n,r,o;Pt.__p&&Pt.__p(t,e),r=(n=i===It)?null:i&&i.__k||e.__k,t=Dt(Bt,null,[t]),o=[],Qt(e,n?e.__k=t:(i||e).__k=t,r||Mt,Mt,void 0!==e.ownerSVGElement,i&&!n?[i]:r?null:Ht.slice.call(e.childNodes),o,!1,i||Mt,n),te(o,t)}(Dt(Ge,Object.assign({},{ref:function(t){return i=t}},e)),n),"loading"!==document.readyState?r():document.addEventListener("DOMContentLoaded",r),i}).prototype=(Ge=Ke).prototype,Object.assign(Je,Ge),Je.__component=Ge,Je);!function(t){t.version="5.5.2",t.Color=xe,t.ColorPicker=Xe,function(t){t.h=Dt,t.ComponentBase=Be,t.Handle=Fe,t.Slider=ze,t.Wheel=qe,t.Box=Ve}(t.ui||(t.ui={}))}(Ze||(Ze={}));var Ye=Ze; +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const Qe=window,ti=Qe.ShadowRoot&&(void 0===Qe.ShadyCSS||Qe.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype;Symbol(),new WeakMap; +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +function ei(t){return class extends t{createRenderRoot(){const t=this.constructor,{registry:e,elementDefinitions:i,shadowRootOptions:n}=t;i&&!e&&(t.registry=new CustomElementRegistry,Object.entries(i).forEach(([e,i])=>t.registry.define(e,i)));const r=this.renderOptions.creationScope=this.attachShadow({...n,customElements:t.registry});return((t,e)=>{ti?t.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet):e.forEach(e=>{const i=document.createElement("style"),n=Qe.litNonce;void 0!==n&&i.setAttribute("nonce",n),i.textContent=e.cssText,t.appendChild(i)})})(r,this.constructor.elementStyles),r}}}var ii=l` + .IroSlider { + display: none !important; + } + + .light-entity-card { + padding: 16px; + } + + .light-entity-child-card { + box-shadow: none !important; + padding: 0 !important; + } + + .light-entity-card.group { + padding-bottom: 5; + padding-top: 0; + } + + .ha-slider-full-width ha-slider { + width: 100%; + } + + .percent-slider { + color: var(--primary-text-color); + display: flex; + justify-content: center; + align-items: center; + } + + .light-entity-card__header { + display: flex; + justify-content: space-between; + @apply --paper-font-headline; + line-height: 40px; + color: var(--primary-text-color); + } + + .group .light-entity-card__header { + } + + .light-entity-card-sliders > div { + margin-top: 10px; + } + + .group .light-entity-card-sliders > div { + margin-top: 0px; + } + + .light-entity-card__toggle { + display: flex; + cursor: pointer; + } + + .light-entity-card__color-picker { + display: flex; + justify-content: space-around; + margin-top: 10px; + } + + .light-entity-card-color_temp { + background-image: var(--ha-slider-background); + } + + .light-entity-card-effectlist { + padding-top: 10px; + padding-bottom: 10px; + } + + .group .light-entity-card-effectlist { + padding-bottom: 20px; + } + + .light-entity-card-center { + display: flex; + justify-content: center; + cursor: pointer; + } + + .hidden { + display: none; + } + + .icon-container { + display: flex; + justify-content: center; + align-items: center; + } +`,ni={shorten_cards:!1,consolidate_entities:!1,child_card:!1,hide_header:!1,show_header_icon:!1,header:"",color_wheel:!0,persist_features:!1,brightness:!0,color_temp:!0,white_value:!0,color_picker:!0,speed:!0,intensity:!0,force_features:!1,show_slider_percent:!1,full_width_sliders:!1,brightness_icon:"weather-sunny",white_icon:"file-word-box",temperature_icon:"thermometer",speed_icon:"speedometer",intensity_icon:"transit-connection-horizontal"};var ri=l` + .entities { + padding-top: 10px; + padding-bottom: 10px; + display: flex; + } + + .entities ha-formfield { + display: block; + margin-bottom: 10px; + margin-left: 10px; + } + + .checkbox-options { + display: flex; + } + + mwc-select { + width: 100%; + } + + .checkbox-options ha-formfield, + .entities mwc-switch, + .entities ha-form-string { + padding-right: 2%; + width: 48%; + } + + .checkbox-options ha-formfield { + margin-top: 10px; + } + + .overall-config { + margin-bottom: 20px; + } +`;var oi=(t,e)=>t.reduce((t,i)=>(i.defineId?t[i.defineId]=i:i.promise.then(t=>{void 0===e.registry.get(i.name)&&e.registry.define(i.name,t)}),t),{});var si=t=>({name:t,promise:customElements.whenDefined(t).then(()=>customElements.get(t))});const ai=(t,e,i={},n={})=>{const r=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,t.dispatchEvent(r),r};class li extends(ei(Ct)){static get elementDefinitions(){return oi([si("ha-checkbox"),si("ha-formfield"),si("ha-form-string"),si("ha-select"),si("mwc-list-item")],li)}static get styles(){return ri}static get properties(){return{hass:{},_config:{}}}setConfig(t){this._config={...ni,...t}}get entityOptions(){const t=Object.keys(this.hass.states).filter(t=>["switch","light","group"].includes(t.substr(0,t.indexOf("."))));return t.sort(),t}firstUpdated(){this._firstRendered=!0}render(){if(!this.hass)return Q``;let{header:t}=this._config;if(!t&&this._config.entity){let e=this._config.entity.split(".")[1]||"";e&&(e=e.charAt(0).toUpperCase()+e.slice(1),t=e)}const e=this.entityOptions.map(t=>Q`${t}`);return Q` +
+ +
+ +
+ +
+ + ${e} + + +
+ +
+ + +
+ +
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + +
+ +
+ + + +
+
+
+ `}configChanged(t){if(!this._config||!this.hass||!this._firstRendered)return;const{target:{configValue:e,value:i},detail:{value:n}}=t;this._config=null!=n?{...this._config,[e]:n}:{...this._config,[e]:i},ai(this,"config-changed",{config:this._config})}checkboxConfigChanged(t){if(!this._config||!this.hass||!this._firstRendered)return;const{target:{value:e,checked:i}}=t;this._config={...this._config,[e]:i},ai(this,"config-changed",{config:this._config})}}var ci=i(0);customElements.define("light-entity-card-editor",li),console.info("light-entity-card v"+ci.version);class hi extends(ei(Ct)){static get elementDefinitions(){return oi([si("ha-card"),si("more-info-light"),si("ha-switch"),si("ha-icon"),si("state-badge"),si("ha-slider"),si("ha-color-picker"),si("ha-select"),si("mwc-list-item")],hi)}static get properties(){return{hass:{},config:{}}}async firstUpdated(){this.setColorWheels(),this._firstUpdate=!0}async updated(){this.setColorWheels()}setColorWheels(){if(!this._shownStateObjects)return;const t=this.getColorPickerWidth();for(const e of this._shownStateObjects){const i=this.renderRoot.getElementById("picker-"+e.entity_id);if(!i)continue;i.innerHTML="";let n={h:0,s:0,l:50};if(e.attributes.hs_color){n={h:parseInt(e.attributes.hs_color[0]),s:parseInt(e.attributes.hs_color[1]),l:50}}new Ye.ColorPicker(i,{sliderSize:0,color:n,width:t,wheelLightness:!1}).on("input:end",t=>this.setColorPicker(t.hsl,e))}}getColorPickerWidth(){const t=this.shadowRoot.querySelector(".light-entity-card").offsetWidth,e=this.config.shorten_cards,i=t-(e?100:50),n=e?200:300;return n>i?i:n}setConfig(t){if(!t.entity)throw Error("entity required.");this.config={...ni,...t}}static async getConfigElement(){return document.createElement("light-entity-card-editor")}static get featureNames(){return{brightness:1,colorTemp:2,effectList:4,color:16,whiteValue:128}}static get cmdToggle(){return{on:"turn_on",off:"turn_off"}}static get entityLength(){return{light:10,switch:1}}getCardSize(){if(!this.config||!this.__hass||!this.__hass.states[this.config.entity])return 1;let t=0;const e=this.__hass.states[this.config.entity];return Array.isArray(e.attributes.entity_id)?e.attributes.entity_id.forEach(e=>t+=this.getEntityLength(e)):t+=this.getEntityLength(e.attributes.entity_id),this.config.group&&(t*=.8),parseInt(t,1)}getEntityLength(t){return/^light\./.test(t)?hi.entityLength.light:/^switch\./.test(t)?hi.entityLength.switch:0}get styles(){return ii}get language(){return this.__hass.resources[this.__hass.language]}isEntityOn(t){return"on"===t.state}render(){const t=this.hass.states[this.config.entity];if(!t)return Q` + + ${"Invalid entity: "+this.config.entity} + `;this._stateObjects=this.getEntitiesToShow(t),this.config.consolidate_entities?this._shownStateObjects=[t]:this._shownStateObjects=[...this._stateObjects];const e=this._shownStateObjects.reduce((t,e)=>Q`${t}${this.createEntityTemplate(e)}`,""),i=`light-entity-card ${this.config.shorten_cards?" group":""} ${this.config.child_card?" light-entity-child-card":""}`;return setTimeout(()=>{this.setColorWheels()},100),Q` + + + ${e} + + `}getEntitiesToShow(t){return t.attributes.entity_id&&Array.isArray(t.attributes.entity_id)?t.attributes.entity_id.map(t=>this.hass.states[t]).filter(Boolean):[t]}createEntityTemplate(t){const e=this.config.full_width_sliders?"ha-slider-full-width":"";return Q` + ${this.createHeader(t)} +
+ ${this.createBrightnessSlider(t)} ${this.createSpeedSlider(t)} + ${this.createIntensitySlider(t)} ${this.createColorTemperature(t)} + ${this.createWhiteValue(t)} +
+ ${this.createColorPicker(t)} ${this.createEffectList(t)} + `}createHeader(t){if(this.config.hide_header)return Q``;const e=this.config.header||t.attributes.friendly_name||t.entity_id;return Q` +
+ ${this.showHeaderIcon(t)} +
${e}
+
+ this.setToggle(e,t)}> +
+
+ `}showHeaderIcon(t){return this.config.show_header_icon?Q` +
+ +
+ `:Q``}createBrightnessSlider(t){return!1===this.config.brightness||this.dontShowFeature("brightness",t)?Q``:Q` +
+
+ +
+ + ${this.showPercent(t.attributes.brightness,0,254)} +
+ `}createSpeedSlider(t){return!1===this.config.speed||this.dontShowFeature("speed",t)?Q``:Q` +
+
+ +
+ + ${this.showPercent(t.attributes.speed,0,254)} +
+ `}createIntensitySlider(t){return!1===this.config.speed||this.dontShowFeature("intensity",t)?Q``:Q` +
+
+ +
+ + ${this.showPercent(t.attributes.intensity,0,254)} +
+ `}showPercent(t,e,i){if(!this.config.show_slider_percent)return Q``;let n=parseInt(100*(t-e)/(i-e),0);return isNaN(n)&&(n=0),Q`
${n}%
`}createColorTemperature(t){if(!1===this.config.color_temp)return Q``;if(this.dontShowFeature("colorTemp",t))return Q``;const e=this.showPercent(t.attributes.color_temp,t.attributes.min_mireds-1,t.attributes.max_mireds-1);return Q` +
+
+ +
+ + + ${e} +
+ `}createWhiteValue(t){return!1===this.config.white_value||this.dontShowFeature("whiteValue",t)?Q``:Q` +
+
+ +
+ + + ${this.showPercent(t.attributes.white_value,0,254)} +
+ `}createEffectList(t){if(!1===this.config.effects_list)return Q``;if(!this.config.persist_features&&!this.isEntityOn(t))return Q``;let e=t.attributes.effect_list||[];if(this.config.effects_list&&Array.isArray(this.config.effects_list))e=this.config.effects_list;else if(this.config.effects_list&&this.hass.states[this.config.effects_list]){const t=this.hass.states[this.config.effects_list];e=t.attributes&&t.attributes.options||[]}else if(this.dontShowFeature("effectList",t))return Q``;const i=e.map(e=>this.createListItem(t,e)),n=this.language["ui.card.light.effect"];return Q` +
+ this.setEffect(e,t)} + label="${n}" + > + ${i} + +
+ `}createListItem(t,e){return Q`${e}`}createColorPicker(t){return!1===this.config.color_picker||this.dontShowFeature("color",t)?Q``:Q` +
+
+
+ `}dontShowFeature(t,e){if(this.config.force_features)return!1;if("speed"===t&&"speed"in e.attributes)return!0;if("intensity"===t&&"intensity"in e.attributes)return!0;let i=hi.featureNames[t]&e.attributes.supported_features;const n=e.attributes.supported_color_modes||[];if(!i)switch(t){case"brightness":if(i=Object.prototype.hasOwnProperty.call(e.attributes,"brightness"),!i){const t=["hs","rgb","rgbw","rgbww","white","brightness","color_temp","xy"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0}break;case"colorTemp":if(n){const t=["color_temp"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0}break;case"effectList":i=e.attributes.effect_list&&e.attributes.effect_list.length;break;case"color":if(!i){const t=["hs","rgb","rgbw","rgbww","xy"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0}break;case"whiteValue":i=Object.prototype.hasOwnProperty.call(e.attributes,"white_value");break;default:i=!1}return!i||(!this.config.persist_features&&!this.isEntityOn(e)||void 0)}setColorPicker(t,e){this.callEntityService({hs_color:[t.h,t.s]},e)}_setValue(t,e,i){const n=parseInt(t.target.value,0);isNaN(n)||parseInt(e.attributes[i],0)===n||this.callEntityService({[i]:n},e)}setToggle(t,e){const i=this.isEntityOn(e)?hi.cmdToggle.off:hi.cmdToggle.on;this.callEntityService({},e,i)}setEffect(t,e){t.target.value&&this.callEntityService({effect:t.target.value},e)}callEntityService(t,e,i){if(!this._firstUpdate)return;let n=e.entity_id.split(".")[0];"group"===n&&(n="homeassistant"),this.hass.callService(n,i||hi.cmdToggle.on,{entity_id:e.entity_id,...t})}}customElements.define("light-entity-card",hi),window.customCards=window.customCards||[],window.customCards.push({type:"light-entity-card",name:"Light Entity Card",description:"Control lights and switches"})}]); +//# sourceMappingURL=light-entity-card.js.map diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-auto-entities/lovelace-auto-entities.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-auto-entities/lovelace-auto-entities.js new file mode 100644 index 0000000..af00237 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-auto-entities/lovelace-auto-entities.js @@ -0,0 +1 @@ +var t,e,n,r,i,o,a,s,u,c,l,d,f,h,p,v,y,m,g,b,_,w,x,k,$,E,A,O,S,C;function P(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}function j(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function T(t){for(var e=1;e=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return i("end");if(a.tryLoc<=this.prev){var u=r.call(a,"catchLoc"),c=r.call(a,"finallyLoc");if(u&&c){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&r.call(i,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),C(n),y}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var i=r.arg;C(n)}return i}}throw Error("illegal catch attempt")},delegateYield:function(e,n,r){return this.delegate={iterator:j(e),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=t),y}},e}function F(t,e,n,r,i,o,a){try{var s=t[o](a),u=s.value}catch(t){return void n(t)}s.done?e(u):Promise.resolve(u).then(r,i)}function D(t){return function(){var e=this,n=arguments;return new Promise((function(r,i){var o=t.apply(e,n);function a(t){F(o,r,i,a,s,"next",t)}function s(t){F(o,r,i,a,s,"throw",t)}a(void 0)}))}}function H(t,e,n){return e=V(e),function(t,e){if(e&&("object"==et(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return W(t)}(t,B()?Reflect.construct(e,n||[],V(t).constructor):e.apply(t,n))}function W(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function q(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&z(t,e)}function G(t){var e="function"==typeof Map?new Map:void 0;return G=function(t){if(null===t||!function(t){try{return-1!==Function.toString.call(t).indexOf("[native code]")}catch(e){return"function"==typeof t}}(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,n)}function n(){return function(t,e,n){if(B())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,e);var i=new(t.bind.apply(t,r));return n&&z(i,n.prototype),i}(t,arguments,V(this).constructor)}return n.prototype=Object.create(t.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),z(n,t)},G(t)}function B(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(B=function(){return!!t})()}function z(t,e){return z=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},z(t,e)}function V(t){return V=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},V(t)}function J(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=K(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var r=0,i=function(){};return{s:i,n:function(){return r>=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,a=!0,s=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){s=!0,o=t},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw o}}}}function K(t,e){if(t){if("string"==typeof t)return Y(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Y(t,e):void 0}}function Y(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n=0;s--)(i=t[s])&&(a=(o<3?i(a):o>3?i(e,n,a):i(e,n))||a);return o>3&&a&&Object.defineProperty(e,n,a),a}"function"==typeof SuppressedError&&SuppressedError;var rt=globalThis,it=rt.ShadowRoot&&(void 0===rt.ShadyCSS||rt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ot=Symbol(),at=new WeakMap,st=function(){return X((function t(e,n,r){if(Z(this,t),this._$cssResult$=!0,r!==ot)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=n}),[{key:"styleSheet",get:function(){var t=this.o,e=this.t;if(it&&void 0===t){var n=void 0!==e&&1===e.length;n&&(t=at.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),n&&at.set(e,t))}return t}},{key:"toString",value:function(){return this.cssText}}])}(),ut=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r0&&(this._$Ep=e)}},{key:"createRenderRoot",value:function(){var t,e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return function(t,e){if(it)t.adoptedStyleSheets=e.map((function(t){return t instanceof CSSStyleSheet?t:t.styleSheet}));else{var n,r=J(e);try{for(r.s();!(n=r.n()).done;){var i=n.value,o=document.createElement("style"),a=rt.litNonce;void 0!==a&&o.setAttribute("nonce",a),o.textContent=i.cssText,t.appendChild(o)}}catch(t){r.e(t)}finally{r.f()}}}(e,this.constructor.elementStyles),e}},{key:"connectedCallback",value:function(){var t,e;null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$EO)||void 0===e||e.forEach((function(t){var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}},{key:"enableUpdating",value:function(t){}},{key:"disconnectedCallback",value:function(){var t;null===(t=this._$EO)||void 0===t||t.forEach((function(t){var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}},{key:"attributeChangedCallback",value:function(t,e,n){this._$AK(t,n)}},{key:"_$EC",value:function(t,e){var n=this.constructor.elementProperties.get(t),r=this.constructor._$Eu(t,n);if(void 0!==r&&!0===n.reflect){var i,o=(void 0!==(null===(i=n.converter)||void 0===i?void 0:i.toAttribute)?n.converter:wt).toAttribute(e,n.type);this._$Em=t,null==o?this.removeAttribute(r):this.setAttribute(r,o),this._$Em=null}}},{key:"_$AK",value:function(t,e){var n=this.constructor,r=n._$Eh.get(t);if(void 0!==r&&this._$Em!==r){var i,o=n.getPropertyOptions(r),a="function"==typeof o.converter?{fromAttribute:o.converter}:void 0!==(null===(i=o.converter)||void 0===i?void 0:i.fromAttribute)?o.converter:wt;this._$Em=r,this[r]=a.fromAttribute(e,o.type),this._$Em=null}}},{key:"requestUpdate",value:function(t,e,n){if(void 0!==t){var r;if(null!=n||(n=this.constructor.getPropertyOptions(t)),!(null!==(r=n.hasChanged)&&void 0!==r?r:xt)(this[t],e))return;this.P(t,e,n)}!1===this.isUpdatePending&&(this._$ES=this._$ET())}},{key:"P",value:function(t,e,n){var r;this._$AL.has(t)||this._$AL.set(t,e),!0===n.reflect&&this._$Em!==t&&(null!==(r=this._$Ej)&&void 0!==r?r:this._$Ej=new Set).add(t)}},{key:"_$ET",value:(e=D(I().mark((function t(){var e;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return this.isUpdatePending=!0,t.prev=1,t.next=4,this._$ES;case 4:t.next=9;break;case 6:t.prev=6,t.t0=t.catch(1),Promise.reject(t.t0);case 9:if(e=this.scheduleUpdate(),t.t1=null!=e,!t.t1){t.next=14;break}return t.next=14,e;case 14:return t.abrupt("return",!this.isUpdatePending);case 15:case"end":return t.stop()}}),t,this,[[1,6]])}))),function(){return e.apply(this,arguments)})},{key:"scheduleUpdate",value:function(){return this.performUpdate()}},{key:"performUpdate",value:function(){if(this.isUpdatePending){if(!this.hasUpdated){var t;if(null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this._$Ep){var e,n=J(this._$Ep);try{for(n.s();!(e=n.n()).done;){var r=L(e.value,2),i=r[0],o=r[1];this[i]=o}}catch(t){n.e(t)}finally{n.f()}this._$Ep=void 0}var a=this.constructor.elementProperties;if(a.size>0){var s,u=J(a);try{for(u.s();!(s=u.n()).done;){var c=L(s.value,2),l=c[0],d=c[1];!0!==d.wrapped||this._$AL.has(l)||void 0===this[l]||this.P(l,this[l],d)}}catch(t){u.e(t)}finally{u.f()}}}var f=!1,h=this._$AL;try{var p;(f=this.shouldUpdate(h))?(this.willUpdate(h),null!==(p=this._$EO)&&void 0!==p&&p.forEach((function(t){var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(h)):this._$EU()}catch(h){throw f=!1,this._$EU(),h}f&&this._$AE(h)}}},{key:"willUpdate",value:function(t){}},{key:"_$AE",value:function(t){var e;null!==(e=this._$EO)&&void 0!==e&&e.forEach((function(t){var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}},{key:"_$EU",value:function(){this._$AL=new Map,this.isUpdatePending=!1}},{key:"updateComplete",get:function(){return this.getUpdateComplete()}},{key:"getUpdateComplete",value:function(){return this._$ES}},{key:"shouldUpdate",value:function(t){return!0}},{key:"update",value:function(t){var e=this;this._$Ej&&(this._$Ej=this._$Ej.forEach((function(t){return e._$EC(t,e[t])}))),this._$EU()}},{key:"updated",value:function(t){}},{key:"firstUpdated",value:function(t){}}],[{key:"addInitializer",value:function(t){var e;this._$Ei(),(null!==(e=this.l)&&void 0!==e?e:this.l=[]).push(t)}},{key:"observedAttributes",get:function(){return this.finalize(),this._$Eh&&R(this._$Eh.keys())}},{key:"createProperty",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:kt;if(e.state&&(e.attribute=!1),this._$Ei(),this.elementProperties.set(t,e),!e.noAccessor){var n=Symbol(),r=this.getPropertyDescriptor(t,n,e);void 0!==r&&dt(this.prototype,t,r)}}},{key:"getPropertyDescriptor",value:function(t,e,n){var r,i=null!==(r=ft(this.prototype,t))&&void 0!==r?r:{get:function(){return this[e]},set:function(t){this[e]=t}},o=i.get,a=i.set;return{get:function(){return null==o?void 0:o.call(this)},set:function(e){var r=null==o?void 0:o.call(this);a.call(this,e),this.requestUpdate(t,r,n)},configurable:!0,enumerable:!0}}},{key:"getPropertyOptions",value:function(t){var e;return null!==(e=this.elementProperties.get(t))&&void 0!==e?e:kt}},{key:"_$Ei",value:function(){if(!this.hasOwnProperty(_t("elementProperties"))){var t=vt(this);t.finalize(),void 0!==t.l&&(this.l=R(t.l)),this.elementProperties=new Map(t.elementProperties)}}},{key:"finalize",value:function(){if(!this.hasOwnProperty(_t("finalized"))){if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(_t("properties"))){var t,e=this.properties,n=J([].concat(R(ht(e)),R(pt(e))));try{for(n.s();!(t=n.n()).done;){var r=t.value;this.createProperty(r,e[r])}}catch(t){n.e(t)}finally{n.f()}}var i=this[Symbol.metadata];if(null!==i){var o=litPropertyMetadata.get(i);if(void 0!==o){var a,s=J(o);try{for(s.s();!(a=s.n()).done;){var u=L(a.value,2),c=u[0],l=u[1];this.elementProperties.set(c,l)}}catch(t){s.e(t)}finally{s.f()}}}this._$Eh=new Map;var d,f=J(this.elementProperties);try{for(f.s();!(d=f.n()).done;){var h=L(d.value,2),p=h[0],v=h[1],y=this._$Eu(p,v);void 0!==y&&this._$Eh.set(y,p)}}catch(t){f.e(t)}finally{f.f()}this.elementStyles=this.finalizeStyles(this.styles)}}},{key:"finalizeStyles",value:function(t){var e=[];if(Array.isArray(t)){var n,r=J(new Set(t.flat(1/0).reverse()));try{for(r.s();!(n=r.n()).done;){var i=n.value;e.unshift(ct(i))}}catch(t){r.e(t)}finally{r.f()}}else void 0!==t&&e.push(ct(t));return e}},{key:"_$Eu",value:function(t,e){var n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof t?t.toLowerCase():void 0}}]);var e}();$t.elementStyles=[],$t.shadowRootOptions={mode:"open"},$t[_t("elementProperties")]=new Map,$t[_t("finalized")]=new Map,null!=bt&&bt({ReactiveElement:$t}),(null!==(n=yt.reactiveElementVersions)&&void 0!==n?n:yt.reactiveElementVersions=[]).push("2.0.4");var Et=globalThis,At=Et.trustedTypes,Ot=At?At.createPolicy("lit-html",{createHTML:function(t){return t}}):void 0,St="$lit$",Ct="lit$".concat(Math.random().toFixed(9).slice(2),"$"),Pt="?"+Ct,jt="<".concat(Pt,">"),Tt=document,Ut=function(){return Tt.createComment("")},Nt=function(t){return null===t||"object"!=et(t)&&"function"!=typeof t},Mt=Array.isArray,Rt="[ \t\n\f\r]",Lt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,It=/-->/g,Ft=/>/g,Dt=RegExp(">|".concat(Rt,"(?:([^\\s\"'>=/]+)(").concat(Rt,"*=").concat(Rt,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),Ht=/'/g,Wt=/"/g,qt=/^(?:script|style|textarea|title)$/i,Gt=function(t){return function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),i=1;i":3===e?"":"",a=Lt,s=0;s"===l[0]?(a=null!=n?n:Lt,d=-1):void 0===l[1]?d=-2:(d=a.lastIndex-l[2].length,c=l[1],a=void 0===l[3]?Dt:'"'===l[3]?Wt:Ht):a===Wt||a===Ht?a=Dt:a===It||a===Ft?a=Lt:(a=Dt,n=void 0);var h=a===Dt&&t[s+1].startsWith("/>")?" ":"";o+=a===Lt?u+jt:d>=0?(i.push(c),u.slice(0,d)+St+u.slice(d)+Ct+h):u+Ct+(-2===d?s:h)}return[Kt(t,o+(t[r]||"")+(2===e?"":3===e?"":"")),i]},Zt=function(){return X((function t(e,n){var r,i=e.strings,o=e._$litType$;Z(this,t),this.parts=[];var a=0,s=0,u=i.length-1,c=this.parts,l=L(Yt(i,o),2),d=l[0],f=l[1];if(this.el=t.createElement(d,n),Jt.currentNode=this.el.content,2===o||3===o){var h=this.el.content.firstChild;h.replaceWith.apply(h,R(h.childNodes))}for(;null!==(r=Jt.nextNode())&&c.length0){r.textContent=At?At.emptyScript:"";for(var x=0;x2&&void 0!==arguments[2]?arguments[2]:t,u=arguments.length>3?arguments[3]:void 0;if(e===Bt)return e;var c=void 0!==u?null===(n=s._$Co)||void 0===n?void 0:n[u]:s._$Cl,l=Nt(e)?void 0:e._$litDirective$;return(null===(r=c)||void 0===r?void 0:r.constructor)!==l&&(null!==(i=c)&&void 0!==i&&null!==(o=i._$AO)&&void 0!==o&&o.call(i,!1),void 0===l?c=void 0:(c=new l(t))._$AT(t,s,u),void 0!==u?(null!==(a=s._$Co)&&void 0!==a?a:s._$Co=[])[u]=c:s._$Cl=c),void 0!==c&&(e=Qt(t,c._$AS(t,e.values),c,u)),e}var Xt=function(){return X((function t(e,n){Z(this,t),this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=n}),[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(t){var e,n=this._$AD,r=n.el.content,i=n.parts,o=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:Tt).importNode(r,!0);Jt.currentNode=o;for(var a=Jt.nextNode(),s=0,u=0,c=i[0];void 0!==c;){var l;if(s===c.index){var d=void 0;2===c.type?d=new te(a,a.nextSibling,this,t):1===c.type?d=new c.ctor(a,c.name,c.strings,this,t):6===c.type&&(d=new oe(a,this,t)),this._$AV.push(d),c=i[++u]}s!==(null===(l=c)||void 0===l?void 0:l.index)&&(a=Jt.nextNode(),s++)}return Jt.currentNode=Tt,o}},{key:"p",value:function(t){var e,n=0,r=J(this._$AV);try{for(r.s();!(e=r.n()).done;){var i=e.value;void 0!==i&&(void 0!==i.strings?(i._$AI(t,i,n),n+=i.strings.length-2):i._$AI(t[n])),n++}}catch(t){r.e(t)}finally{r.f()}}}])}(),te=function(){function t(e,n,r,i){var o;Z(this,t),this.type=2,this._$AH=zt,this._$AN=void 0,this._$AA=e,this._$AB=n,this._$AM=r,this.options=i,this._$Cv=null===(o=null==i?void 0:i.isConnected)||void 0===o||o}return X(t,[{key:"_$AU",get:function(){var t,e;return null!==(t=null===(e=this._$AM)||void 0===e?void 0:e._$AU)&&void 0!==t?t:this._$Cv}},{key:"parentNode",get:function(){var t,e=this._$AA.parentNode,n=this._$AM;return void 0!==n&&11===(null===(t=e)||void 0===t?void 0:t.nodeType)&&(e=n.parentNode),e}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){t=Qt(this,t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),Nt(t)?t===zt||null==t||""===t?(this._$AH!==zt&&this._$AR(),this._$AH=zt):t!==this._$AH&&t!==Bt&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):function(t){return Mt(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])}(t)?this.k(t):this._(t)}},{key:"O",value:function(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}},{key:"T",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}},{key:"_",value:function(t){this._$AH!==zt&&Nt(this._$AH)?this._$AA.nextSibling.data=t:this.T(Tt.createTextNode(t)),this._$AH=t}},{key:"$",value:function(t){var e,n=t.values,r=t._$litType$,i="number"==typeof r?this._$AC(t):(void 0===r.el&&(r.el=Zt.createElement(Kt(r.h,r.h[0]),this.options)),r);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===i)this._$AH.p(n);else{var o=new Xt(i,this),a=o.u(this.options);o.p(n),this.T(a),this._$AH=o}}},{key:"_$AC",value:function(t){var e=Vt.get(t.strings);return void 0===e&&Vt.set(t.strings,e=new Zt(t)),e}},{key:"k",value:function(e){Mt(this._$AH)||(this._$AH=[],this._$AR());var n,r,i=this._$AH,o=0,a=J(e);try{for(a.s();!(r=a.n()).done;){var s=r.value;o===i.length?i.push(n=new t(this.O(Ut()),this.O(Ut()),this,this.options)):n=i[o],n._$AI(s),o++}}catch(t){a.e(t)}finally{a.f()}o0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,e=arguments.length>1?arguments[1]:void 0;for(null===(n=this._$AP)||void 0===n||n.call(this,!1,!0,e);t&&t!==this._$AB;){var n,r=t.nextSibling;t.remove(),t=r}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cv=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}])}(),ee=function(){return X((function t(e,n,r,i,o){Z(this,t),this.type=1,this._$AH=zt,this._$AN=void 0,this.element=e,this.name=n,this._$AM=i,this.options=o,r.length>2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=zt}),[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,n=arguments.length>2?arguments[2]:void 0,r=arguments.length>3?arguments[3]:void 0,i=this.strings,o=!1;if(void 0===i)t=Qt(this,t,e,0),(o=!Nt(t)||t!==this._$AH&&t!==Bt)&&(this._$AH=t);else{var a,s,u=t;for(t=i[0],a=0;a1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==e?e:zt)!==Bt){var n=this._$AH,r=t===zt&&n!==zt||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,i=t!==zt&&(n===zt||r);r&&this.element.removeEventListener(this.name,this,n),i&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,n;"function"==typeof this._$AH?this._$AH.call(null!==(e=null===(n=this.options)||void 0===n?void 0:n.host)&&void 0!==e?e:this.element,t):this._$AH.handleEvent(t)}}])}(),oe=function(){return X((function t(e,n,r){Z(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=n,this.options=r}),[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){Qt(this,t)}}])}(),ae=Et.litHtmlPolyfillSupport;null!=ae&&ae(Zt,te),(null!==(r=Et.litHtmlVersions)&&void 0!==r?r:Et.litHtmlVersions=[]).push("3.2.1");var se=function(){function t(){var e;return Z(this,t),(e=H(this,t,arguments)).renderOptions={host:W(e)},e._$Do=void 0,e}return q(t,$t),X(t,[{key:"createRenderRoot",value:function(){var e,n,r=N(t,"createRenderRoot",this,3)([]);return null!==(n=(e=this.renderOptions).renderBefore)&&void 0!==n||(e.renderBefore=r.firstChild),r}},{key:"update",value:function(e){var n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),N(t,"update",this,3)([e]),this._$Do=function(t,e,n){var r,i=null!==(r=null==n?void 0:n.renderBefore)&&void 0!==r?r:e,o=i._$litPart$;if(void 0===o){var a,s=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;i._$litPart$=o=new te(e.insertBefore(Ut(),s),s,void 0,null!=n?n:{})}return o._$AI(t),o}(n,this.renderRoot,this.renderOptions)}},{key:"connectedCallback",value:function(){var e;N(t,"connectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!0)}},{key:"disconnectedCallback",value:function(){var e;N(t,"disconnectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!1)}},{key:"render",value:function(){return Bt}}])}();se._$litElement$=!0,se.finalized=!0,null===(i=globalThis.litElementHydrateSupport)||void 0===i||i.call(globalThis,{LitElement:se});var ue=globalThis.litElementPolyfillSupport;null==ue||ue({LitElement:se}),(null!==(o=globalThis.litElementVersions)&&void 0!==o?o:globalThis.litElementVersions=[]).push("4.1.1");var ce={attribute:!0,type:String,converter:wt,reflect:!1,hasChanged:xt},le=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ce,e=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,r=n.kind,i=n.metadata,o=globalThis.litPropertyMetadata.get(i);if(void 0===o&&globalThis.litPropertyMetadata.set(i,o=new Map),o.set(n.name,t),"accessor"===r){var a=n.name;return{set:function(n){var r=e.get.call(this);e.set.call(this,n),this.requestUpdate(a,r,t)},init:function(e){return void 0!==e&&this.P(a,void 0,t),e}}}if("setter"===r){var s=n.name;return function(n){var r=this[s];e.call(this,n),this.requestUpdate(s,r,t)}}throw Error("Unsupported decorator location: "+r)};function de(t){return function(e,n){return"object"==et(n)?le(t,e,n):function(t,e,n){var r=e.hasOwnProperty(n);return e.constructor.createProperty(n,r?T(T({},t),{},{wrapped:!0}):t),r?Object.getOwnPropertyDescriptor(e,n):void 0}(t,e,n)}}function fe(t){return de(T(T({},t),{},{state:!0,attribute:!1}))}function he(){return pe.apply(this,arguments)}function pe(){return pe=D(I().mark((function t(){var e;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.race([customElements.whenDefined("home-assistant"),customElements.whenDefined("hc-main")]);case 2:e=customElements.get("home-assistant")?"home-assistant":"hc-main";case 3:if(document.querySelector(e)){t.next=8;break}return t.next=6,new Promise((function(t){return window.setTimeout(t,100)}));case 6:t.next=3;break;case 8:return t.abrupt("return",document.querySelector(e));case 9:case"end":return t.stop()}}),t)}))),pe.apply(this,arguments)}function ve(){return ye.apply(this,arguments)}function ye(){return ye=D(I().mark((function t(){var e;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,he();case 2:e=t.sent;case 3:if(e.hass){t.next=8;break}return t.next=6,new Promise((function(t){return window.setTimeout(t,100)}));case 6:t.next=3;break;case 8:return t.abrupt("return",e.hass);case 9:case"end":return t.stop()}}),t)}))),ye.apply(this,arguments)}var me="browser_mod-browser-id";window.cardMod_template_cache=window.cardMod_template_cache||{};var ge,be=window.cardMod_template_cache;function _e(t,e){var n=be[t];n&&(n.value=e.result,n.callbacks.forEach((function(t){return t(e.result)})))}function we(t){return!!t&&(String(t).includes("{%")||String(t).includes("{{"))}function xe(t,e,n){return ke.apply(this,arguments)}function ke(){return ke=D(I().mark((function t(e,n,r){var i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,ve();case 2:i=t.sent,o=i.connection,a=JSON.stringify([n,r]),(s=be[a])?(s.callbacks.has(e)||$e(e),e(s.value),s.callbacks.add(e)):($e(e),e(""),r=Object.assign({user:i.user.name,browser:document.querySelector("hc-main")?"CAST":localStorage[me]?localStorage[me]:"",hash:location.hash.substr(1)||""},r),be[a]=s={template:n,variables:r,value:"",callbacks:new Set([e]),unsubscribe:o.subscribeMessage((function(t){return _e(a,t)}),{type:"render_template",template:n,variables:r})});case 7:case"end":return t.stop()}}),t)}))),ke.apply(this,arguments)}function $e(t){return Ee.apply(this,arguments)}function Ee(){return(Ee=D(I().mark((function t(e){var n,r,i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:r=0,i=Object.entries(be);case 1:if(!(r=")&&(u=parseFloat(e.substring(2)),n.push((function(t){return parseFloat(t)>=u}))),e.startsWith("==")&&(c=parseFloat(e.substring(2)),n.push((function(t){return parseFloat(t)==c}))),e.startsWith("!=")&&(l=parseFloat(e.substring(2)),n.push((function(t){return parseFloat(t)!=l}))),e.startsWith("<")&&(d=parseFloat(e.substring(1)),n.push((function(t){return parseFloat(t)")&&(f=parseFloat(e.substring(1)),n.push((function(t){return parseFloat(t)>f}))),e.startsWith("!")&&(h=parseFloat(e.substring(1)),n.push((function(t){return parseFloat(t)!=h}))),e.startsWith("=")&&(p=parseFloat(e.substring(1)),n.push((function(t){return parseFloat(t)==p}))),n.push((function(t){return t===e}))):n.push((function(t){return t===e})),t.abrupt("return",(function(t){var e=r.reduce((function(t,e){return e(t)}),t);return void 0!==e&&n.some((function(t){return t(e)}))}));case 4:case"end":return t.stop()}}),t)}))),Re.apply(this,arguments)}var Le,Ie,Fe,De,He,We,qe,Ge,Be,ze,Ve,Je,Ke,Ye,Ze,Qe,Xe,tn,en,nn,rn,on,an,sn,un,cn=/([mhd])\s+ago\s*$/i,ln="m ago",dn={type:(un=D(I().mark((function t(e,n){return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",void 0);case 1:case"end":return t.stop()}}),t)}))),function(t,e){return un.apply(this,arguments)}),options:(sn=D(I().mark((function t(e,n){return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",void 0);case 1:case"end":return t.stop()}}),t)}))),function(t,e){return sn.apply(this,arguments)}),sort:(an=D(I().mark((function t(e,n){return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",void 0);case 1:case"end":return t.stop()}}),t)}))),function(t,e){return an.apply(this,arguments)}),domain:(on=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Me(n);case 2:return r=t.sent,t.abrupt("return",(function(t){return r(t.entity_id.split(".")[0])}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return on.apply(this,arguments)}),entity_id:(rn=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Me(n);case 2:return r=t.sent,t.abrupt("return",(function(t){return r(t.entity_id)}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return rn.apply(this,arguments)}),state:(nn=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Me(n);case 2:return r=t.sent,t.abrupt("return",(function(t){return r(t.state)||r(e.formatEntityState(e.states[t.entity_id]))}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return nn.apply(this,arguments)}),name:(en=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Me(n);case 2:return r=t.sent,t.abrupt("return",(function(t){var e;return r(null===(e=t.attributes)||void 0===e?void 0:e.friendly_name)}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return en.apply(this,arguments)}),group:(tn=D(I().mark((function t(e,n){return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",(function(t){var r,i,o;return null===(o=null===(i=null===(r=e.states[n])||void 0===r?void 0:r.attributes)||void 0===i?void 0:i.entity_id)||void 0===o?void 0:o.includes(t.entity_id)}));case 1:case"end":return t.stop()}}),t)}))),function(t,e){return tn.apply(this,arguments)}),attributes:(Xe=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all(Object.entries(n).map(function(){var t=D(I().mark((function t(e){var n,r,i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=L(e,2),r=n[0],i=n[1],o=r.split(" ")[0],a=function(t){return o.split(":").reduce((function(t,e){return null==t?void 0:t[e]}),t)},t.next=5,Me(i);case 5:return s=t.sent,t.abrupt("return",{prepare:a,match:s});case 7:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()));case 2:return r=t.sent,t.abrupt("return",(function(t){return r.every((function(e){var n=e.prepare;return(0,e.match)(n(t.attributes))}))}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return Xe.apply(this,arguments)}),not:(Qe=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,fn(e,n);case 2:return r=t.sent,t.abrupt("return",(function(t){return!r(t.entity_id)}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return Qe.apply(this,arguments)}),and:(Ze=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all(n.map((function(t){return fn(e,t)})));case 2:return r=t.sent,t.abrupt("return",(function(t){return r.every((function(e){return e(t.entity_id)}))}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return Ze.apply(this,arguments)}),or:(Ye=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all(n.map((function(t){return fn(e,t)})));case 2:return r=t.sent,t.abrupt("return",(function(t){return r.some((function(e){return e(t.entity_id)}))}));case 4:case"end":return t.stop()}}),t)}))),function(t,e){return Ye.apply(this,arguments)}),device:(Ke=D(I().mark((function t(e,n){var r,i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e)]);case 2:return r=t.sent,i=L(r,3),o=i[0],a=i[1],s=i[2],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=s.find((function(t){return t.id===e.device_id}));return!!n&&(o(n.id)||o(n.name_by_user)||o(n.name))}));case 8:case"end":return t.stop()}}),t)}))),function(t,e){return Ke.apply(this,arguments)}),device_manufacturer:(Je=D(I().mark((function t(e,n){var r,i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e)]);case 2:return r=t.sent,i=L(r,3),o=i[0],a=i[1],s=i[2],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=s.find((function(t){return t.id===e.device_id}));return!!n&&o(n.manufacturer)}));case 8:case"end":return t.stop()}}),t)}))),function(t,e){return Je.apply(this,arguments)}),device_model:(Ve=D(I().mark((function t(e,n){var r,i,o,a,s;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e)]);case 2:return r=t.sent,i=L(r,3),o=i[0],a=i[1],s=i[2],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=s.find((function(t){return t.id===e.device_id}));return!!n&&o(n.model)}));case 8:case"end":return t.stop()}}),t)}))),function(t,e){return Ve.apply(this,arguments)}),area:(ze=D(I().mark((function t(e,n){var r,i,o,a,s,u;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e),Ce(e)]);case 2:return r=t.sent,i=L(r,4),o=i[0],a=i[1],s=i[2],u=i[3],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=u.find((function(t){return t.area_id===e.area_id}));if(n)return o(n.name)||o(n.area_id);var r=s.find((function(t){return t.id===e.device_id}));return!!r&&(n=u.find((function(t){return t.area_id===r.area_id})),!!n&&(o(n.name)||o(n.area_id)))}));case 9:case"end":return t.stop()}}),t)}))),function(t,e){return ze.apply(this,arguments)}),floor:(Be=D(I().mark((function t(e,n){var r,i,o,a,s,u,c;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e),Ce(e),Pe(e)]);case 2:return r=t.sent,i=L(r,5),o=i[0],a=i[1],s=i[2],u=i[3],c=i[4],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=u.find((function(t){return t.area_id===e.area_id}));if(!n){var r=s.find((function(t){return t.id===e.device_id}));if(!r)return!1;n=u.find((function(t){return t.area_id===r.area_id}))}if(!n)return!1;var i=c.find((function(t){return t.floor_id===n.floor_id}));return!!i&&(o(i.name)||o(i.floor_id))}));case 10:case"end":return t.stop()}}),t)}))),function(t,e){return Be.apply(this,arguments)}),level:(Ge=D(I().mark((function t(e,n){var r,i,o,a,s,u,c;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e),Ce(e),Pe(e)]);case 2:return r=t.sent,i=L(r,5),o=i[0],a=i[1],s=i[2],u=i[3],c=i[4],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;var n=u.find((function(t){return t.area_id===e.area_id}));if(!n){var r=s.find((function(t){return t.id===e.device_id}));if(!r)return!1;n=u.find((function(t){return t.area_id===r.area_id}))}if(!n)return!1;var i=c.find((function(t){return t.floor_id===n.floor_id}));return!!i&&o(i.level)}));case 10:case"end":return t.stop()}}),t)}))),function(t,e){return Ge.apply(this,arguments)}),entity_category:(qe=D(I().mark((function t(e,n){var r,i,o,a;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e)]);case 2:return r=t.sent,i=L(r,2),o=i[0],a=i[1],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));return!!e&&o(e.entity_category)}));case 7:case"end":return t.stop()}}),t)}))),function(t,e){return qe.apply(this,arguments)}),last_changed:(We=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return cn.test(n)||(n+=ln),t.next=3,Me(n);case 3:return r=t.sent,t.abrupt("return",(function(t){return r(t.last_changed)}));case 5:case"end":return t.stop()}}),t)}))),function(t,e){return We.apply(this,arguments)}),last_updated:(He=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return cn.test(n)||(n+=ln),t.next=3,Me(n);case 3:return r=t.sent,t.abrupt("return",(function(t){return r(t.last_updated)}));case 5:case"end":return t.stop()}}),t)}))),function(t,e){return He.apply(this,arguments)}),last_triggered:(De=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return cn.test(n)||(n+=ln),t.next=3,Me(n);case 3:return r=t.sent,t.abrupt("return",(function(t){return r(t.attributes.last_triggered)}));case 5:case"end":return t.stop()}}),t)}))),function(t,e){return De.apply(this,arguments)}),integration:(Fe=D(I().mark((function t(e,n){var r,i,o,a;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e)]);case 2:return r=t.sent,i=L(r,2),o=i[0],a=i[1],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));return!!e&&(o(e.platform)||o(e.config_entry_id))}));case 7:case"end":return t.stop()}}),t)}))),function(t,e){return Fe.apply(this,arguments)}),hidden_by:(Ie=D(I().mark((function t(e,n){var r,i,o,a;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e)]);case 2:return r=t.sent,i=L(r,2),o=i[0],a=i[1],t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));return!!e&&o(e.hidden_by)}));case 7:case"end":return t.stop()}}),t)}))),function(t,e){return Ie.apply(this,arguments)}),label:(Le=D(I().mark((function t(e,n){var r,i,o,a,s,u,c;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all([Me(n),Te(e),je(e),Ue(e)]);case 2:return r=t.sent,i=L(r,4),o=i[0],a=i[1],s=i[2],u=i[3],c=function(t){if(o(t))return!0;var e=u.find((function(e){return e.label_id===t}));return o(null==e?void 0:e.name)},t.abrupt("return",(function(t){var e=a.find((function(e){return e.entity_id===t.entity_id}));if(!e)return!1;if(!e.labels)return!1;if(e.labels.some(c))return!0;var n=s.find((function(t){return t.id===e.device_id}));return!!n&&n.labels.some(c)}));case 10:case"end":return t.stop()}}),t)}))),function(t,e){return Le.apply(this,arguments)})};function fn(t,e){return hn.apply(this,arguments)}function hn(){return hn=D(I().mark((function t(e,n){var r;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Promise.all(Object.entries(n).map((function(t){var n,r,i=L(t,2),o=i[0],a=i[1];return o=o.trim().split(" ")[0].trim(),null!==(r=null===(n=dn[o])||void 0===n?void 0:n.call(dn,e,a))&&void 0!==r?r:function(){return!1}})));case 2:return r=t.sent.filter((function(t){return void 0!==t})).filter(Boolean),t.abrupt("return",(function(t){var n;if(!r.length)return!1;if("string"!=typeof t&&(t=t.entity),!t)return!1;var i=null===(n=null==e?void 0:e.states)||void 0===n?void 0:n[t];return!!i&&r.every((function(t){return t(i)}))}));case 4:case"end":return t.stop()}}),t)}))),hn.apply(this,arguments)}function pn(t,e,n){var r,i,o,a,s=L(n.reverse?[1,-1]:[-1,1],2),u=s[0],c=s[1];return n.ignore_case&&(t=null!==(i=null===(r=null==t?void 0:t.toLowerCase)||void 0===r?void 0:r.call(t))&&void 0!==i?i:t,e=null!==(a=null===(o=null==e?void 0:e.toLowerCase)||void 0===o?void 0:o.call(e))&&void 0!==a?a:e),n.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?c:void 0===e?u:n.numeric?t===e?0:t1&&void 0!==o[1]&&o[1],!(null===(r=e.localName)||void 0===r?void 0:r.includes("-"))){t.next=4;break}return t.next=4,customElements.whenDefined(e.localName);case 4:if(!e.updateComplete){t.next=7;break}return t.next=7,e.updateComplete;case 7:if(!n){t.next=18;break}if(!e.pageRendered){t.next=11;break}return t.next=11,e.pageRendered;case 11:if(!e._panelState){t.next=18;break}i=0;case 13:if(!("loaded"!==e._panelState&&i++<5)){t.next=18;break}return t.next=16,new Promise((function(t){return setTimeout(t,100)}));case 16:t.next=13;break;case 18:case"end":return t.stop()}}),t)}))),kn.apply(this,arguments)}function $n(t,e){return En.apply(this,arguments)}function En(){return En=D(I().mark((function t(e,n){var r,i,o,a,s,u,c,l=arguments;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:for(r=l.length>2&&void 0!==l[2]&&l[2],i=[e],"string"==typeof n&&(n=n.split(/(\$| )/));""===n[n.length-1];)n.pop();o=J(n.entries()),t.prev=5,o.s();case 7:if((a=o.n()).done){t.next=24;break}if(s=L(a.value,2),s[0],"$"!==(u=s[1])){t.next=14;break}return t.next=12,Promise.all(R(i).map((function(t){return xn(t)})));case 12:return i=R(i).map((function(t){return t.shadowRoot})),t.abrupt("continue",22);case 14:if(c=i[0]){t.next=17;break}return t.abrupt("return",null);case 17:if(u.trim().length){t.next=19;break}return t.abrupt("continue",22);case 19:return t.next=21,xn(c);case 21:i=c.querySelectorAll(u);case 22:t.next=7;break;case 24:t.next=29;break;case 26:t.prev=26,t.t0=t.catch(5),o.e(t.t0);case 29:return t.prev=29,o.f(),t.finish(29);case 32:return t.abrupt("return",r?i:i[0]);case 33:case"end":return t.stop()}}),t,null,[[5,26,29,32]])}))),En.apply(this,arguments)}function An(t,e){return On.apply(this,arguments)}function On(){return On=D(I().mark((function t(e,n){var r,i,o=arguments;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=o.length>2&&void 0!==o[2]&&o[2],i=o.length>3&&void 0!==o[3]?o[3]:1e4,t.abrupt("return",Promise.race([$n(e,n,r),new Promise((function(t,e){return setTimeout((function(){return e(new Error(wn))}),i)}))]).catch((function(t){if(!t.message||t.message!==wn)throw t;return null})));case 3:case"end":return t.stop()}}),t)}))),On.apply(this,arguments)}var Sn={type:"select",options:[["area","Area"],["attributes","Attribute"],["device","Device"],["domain","Domain"],["entity_category","Entity Category"],["entity_id","Entity ID"],["floor","Floor"],["group","Group"],["hidden_by","Hidden by"],["integration","Integration"],["label","Label"],["last_changed","Last Changed"],["last_triggered","Last Triggered"],["last_updated","Last Updated"],["level","Level"],["device_manufacturer","Manufacturer"],["device_model","Model"],["name","Name"],["state","State"]]},Cn={attributes:{object:{}},area:{area:{}},device:{device:{}},entity_id:{entity:{}},floor:{floor:{}},group:{entity:{filter:{domain:"group"}}},integration:{config_entry:{}},label:{label:{}}},Pn=function(t,e){var n,r=L(t,2),i=r[0];r[1];if(!["sort","optios"].includes(i))return Sn.options.some((function(t){var e=L(t,2),n=e[0];e[1];return n===i}))?{type:"grid",name:"",schema:[Object.assign(Object.assign({},Sn),{name:"key_".concat(e),label:"Rule"}),{name:"value_".concat(e),selector:null!==(n=Cn[i])&&void 0!==n?n:{text:{}},label:""}]}:{type:"Constant",name:"Some rules are not shown",value:'The rule "'.concat(i,'" is not supported by the GUI editor.\n Please switch to the CODE EDITOR to access all options.')}},jn=function(){var t=D(I().mark((function t(e){var n,r,i,o,a,s,u,c,l,d;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,xn(e);case 2:return t.t0=J,t.next=5,An(e,"$ ha-form-grid",!0);case 5:t.t1=t.sent,s=(0,t.t0)(t.t1),t.prev=7,s.s();case 9:if((u=s.n()).done){t.next=85;break}return c=u.value,t.next=13,xn(c);case 13:return t.next=15,An(c,"$ ha-form:nth-child(2) $ ha-selector");case 15:if(l=t.sent){t.next=18;break}return t.abrupt("continue",83);case 18:return t.next=20,xn(l);case 20:return t.next=22,An(l,"$ ha-selector-area $ ha-area-picker $ ha-combo-box");case 22:if(t.t7=n=t.sent,t.t6=null!==t.t7,!t.t6){t.next=26;break}t.t6=void 0!==n;case 26:if(!t.t6){t.next=30;break}t.t8=n,t.next=33;break;case 30:return t.next=32,An(l,"$ ha-selector-device $ ha-device-picker $ ha-combo-box");case 32:t.t8=t.sent;case 33:if(t.t9=r=t.t8,t.t5=null!==t.t9,!t.t5){t.next=37;break}t.t5=void 0!==r;case 37:if(!t.t5){t.next=41;break}t.t10=r,t.next=44;break;case 41:return t.next=43,An(l,"$ ha-selector-entity $ ha-entity-picker $ ha-combo-box");case 43:t.t10=t.sent;case 44:if(t.t11=i=t.t10,t.t4=null!==t.t11,!t.t4){t.next=48;break}t.t4=void 0!==i;case 48:if(!t.t4){t.next=52;break}t.t12=i,t.next=55;break;case 52:return t.next=54,An(l,"$ ha-selector-label $ ha-label-picker $ ha-combo-box");case 54:t.t12=t.sent;case 55:if(t.t13=o=t.t12,t.t3=null!==t.t13,!t.t3){t.next=59;break}t.t3=void 0!==o;case 59:if(!t.t3){t.next=63;break}t.t14=o,t.next=66;break;case 63:return t.next=65,An(l,"$ ha-selector-config_entry $ ha-config-entry-picker $ ha-combo-box");case 65:t.t14=t.sent;case 66:if(t.t15=a=t.t14,t.t2=null!==t.t15,!t.t2){t.next=70;break}t.t2=void 0!==a;case 70:if(!t.t2){t.next=74;break}t.t16=a,t.next=77;break;case 74:return t.next=76,An(l,"$ ha-selector-floor $ ha-floor-picker $ ha-combo-box");case 76:t.t16=t.sent;case 77:if(!(d=t.t16)){t.next=83;break}return t.next=81,xn(d);case 81:return d.allowCustomValue=!0,t.abrupt("continue",83);case 83:t.next=9;break;case 85:t.next=90;break;case 87:t.prev=87,t.t17=t.catch(7),s.e(t.t17);case 90:return t.prev=90,s.f(),t.finish(90);case 93:case"end":return t.stop()}}),t,null,[[7,87,90,93]])})));return function(e){return t.apply(this,arguments)}}(),Tn=[{name:"data",label:" ",selector:{object:{}}}],Un=[{name:"entities",label:"Entities:",selector:{object:{}}}],Nn=[{name:"template",label:"Template:",selector:{template:{}}}],Mn=function(t){var e=[{name:"method",label:"Sort method",type:"select",options:[["domain","Entity Domain"],["entity_id","Entity ID"],["friendly_name","Friendly Name"],["state","Entity State"],["last_changed","Last Change"],["last_updated","Last Update"],["last_triggered","Last Trigger"],["attribute","Attribute"]]},{type:"constant",name:"Sorting options:",value:""},{type:"grid",name:"",schema:[{name:"reverse",type:"boolean",label:"Reverse"},{name:"ignore_case",type:"boolean",label:"Ignore case"},{name:"numeric",type:"boolean",label:"Numeric sort"},{name:"ip",type:"boolean",label:"IP address sort"}]}];return void 0===t||e[0].options.some((function(e){var n=L(e,2),r=n[0];n[1];return r===t}))?("attribute"==t&&e.push(),e.push({name:"attribute",label:"Attribute:",selector:{object:{}}}),e):[{type:"Constant",name:"GUI editor not available",value:"Sorting by ".concat(t," is not supported by the GUI editor.\n Please switch to the CODE EDITOR to access all options.")}]},Rn=[{type:"grid",name:"",schema:[{name:"show_empty",type:"boolean",label:"Show if empty"},{name:"card_param",type:"string",label:"Parameter to populate"}]}],Ln=function(){function t(){return Z(this,t),H(this,t,arguments)}return q(t,se),X(t,[{key:"_describe_filter",value:function(t){return"type"in t?"".concat(t.type," ").concat(t.label?'"'.concat(t.label,'"'):""):"".concat(Object.keys(t).length," rules")}},{key:"_getFilters",value:function(t){var e,n,r;return null!==(r=null===(n=null===(e=this._config.filter)||void 0===e?void 0:e[t])||void 0===n?void 0:n.concat())&&void 0!==r?r:[]}},{key:"_setFilters",value:function(t,e){var n=Object.assign(Object.assign({},this._config.filter),U({},t,e));this._config=Object.assign(Object.assign({},this._config),{filter:n}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_closeAll",value:function(t){this.shadowRoot.querySelectorAll("ha-expansion-panel .".concat(t)).forEach((function(t){t.expanded=!1}))}},{key:"_filterAdd",value:function(t,e){var n=this,r=arguments.length>2&&void 0!==arguments[2]&&arguments[2];t.stopPropagation(),this._closeAll(e);var i=this._getFilters(e);r?i.push({type:"section"}):i.push({}),this._setFilters(e,i),this.requestUpdate(),this.updateComplete.then((function(){n.shadowRoot.querySelector("ha-expansion-panel .".concat(e,":last-child")).expanded=!0}))}},{key:"_filterMove",value:function(t,e){t.stopPropagation(),this._closeAll(e);var n=t.detail,r=n.oldIndex,i=n.newIndex,o=this._getFilters(e);o.splice(i,0,o.splice(r,1)[0]),this._setFilters(e,o)}},{key:"_filterDelete",value:function(t,e,n){t.stopPropagation(),this._closeAll(n);var r=this._getFilters(n);r.splice(e,1),this._setFilters(n,r)}},{key:"_rulesChanged",value:function(t,e,n){t.stopPropagation();var r=function(t,e){var n,r={};r.options=e.options;for(var i=0;i<=t.filter.include.length+1;i++)void 0!==e["key_".concat(i)]&&(r[e["key_".concat(i)]]=null!==(n=e["value_".concat(i)])&&void 0!==n?n:"");return void 0!==e.key_new&&(r[e.key_new]=""),r}(this._config,t.detail.value);if("string"!=typeof r.options&&void 0!==r.options){var i=this._getFilters(n);i[e]=Object.assign({},r),this._setFilters(n,i)}}},{key:"_sortChanged",value:function(t,e,n){t.stopPropagation();var r=t.detail.value,i=this._getFilters(n);i[e]=Object.assign(Object.assign({},i[e]),{sort:r}),this._setFilters(n,i)}},{key:"_customChanged",value:function(t,e,n){if(t.stopPropagation(),void 0!==t.detail.value.data){var r=this._getFilters(n);r[e]=Object.assign({},t.detail.value.data),this._setFilters(n,r)}}},{key:"_templateChanged",value:function(t){t.stopPropagation();var e=t.detail.value.template;console.log(e),this._setFilters("template",e)}},{key:"_entitiesChanged",value:function(t){t.stopPropagation();var e=t.detail.value.entities;this._config=Object.assign(Object.assign({},this._config),{entities:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"firstUpdated",value:function(t){var e=this;this.updateComplete.then((function(){e.shadowRoot.querySelector("ha-expansion-panel:first-child").expanded=!0}))}},{key:"updated",value:function(t){var e=this;this.updateComplete.then(D(I().mark((function t(){var n;return I().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=Array.from(e.shadowRoot.querySelectorAll(".filter-rule-form")).map(jn),t.next=3,Promise.all(n);case 3:e.shadowRoot.querySelectorAll("ha-form").forEach((function(t){var n,r,i,o=t;void 0!==o.filter_type&&void 0!==o.filter_idx&&(o.data=(n=e._config.filter[o.filter_type][o.filter_idx],r=Object.assign({},n),i=Object.assign({},n.options),delete r.options,Object.assign.apply(Object,[{}].concat(R(Object.entries(r).map((function(t,e){var n=L(t,2),r=n[0],i=n[1];return U(U({},"key_".concat(e),r),"value_".concat(e),i)}))),[{options:i}]))))}));case 4:case"end":return t.stop()}}),t)}))))}},{key:"render",value:function(){var t=this,e=function(e){var n;return Gt(a||(a=P(['\n \n
\n \n
\n

\n [',"] - ",'\n

\n
\n \n \n \n

Sorting

\n
\n \n If entering a custom Value (e.g. "*light" or\n "/^[Bb]ed/") in a box with options, you need to\n finish with the Enter key.\n

\n ']))):"",t.hass,(o=n,delete(a=Object.assign({},o)).options,[].concat(R(Object.entries(a).map(Pn).filter(Boolean)),[Object.assign(Object.assign({},Sn),{name:"key_new",label:"New Rule ..."}),{name:"options",label:"Options:",selector:{object:{}}}])),(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),(function(n){return t._rulesChanged(n,r,e)}),e,r,t.hass,Mn(null===(i=n.sort)||void 0===i?void 0:i.method),n.sort,(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),(function(n){return t._sortChanged(n,r,e)})):Gt(l||(l=P(["\n \n \n "])),t.hass,Tn,{data:n},(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),(function(n){return t._customChanged(n,r,e)})))})),(function(n){return t._filterAdd(n,e)}),"mdi:plus",(function(n){return t._filterAdd(n,e,!0)}),"mdi:plus")};return Gt(d||(d=P(["\n
\n \n \n

Include

\n\n
',"
\n
\n \n \n

Exclude

\n
',"
\n
\n ","\n ","\n
\n "])),"mdi:plus",e("include"),"mdi:minus",e("exclude"),this._config.entities?Gt(f||(f=P(["\n \n \n "])),this.hass,Un,this._config,(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),(function(e){return t._entitiesChanged(e)})):"",this._config.filter.template?Gt(h||(h=P(["\n \n \n "])),this.hass,Nn,this._config.filter,(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),(function(e){return t._templateChanged(e)})):"")}}],[{key:"styles",get:function(){return[ut(p||(p=P(['\n ha-expansion-panel {\n margin-bottom: 24px;\n display: block;\n --expansion-panel-content-padding: 0;\n border-radius: 6px;\n --ha-card-border-radius: 6px;\n }\n ha-sortable ha-expansion-panel {\n margin-bottom: 8px;\n }\n ha-expansion-panel .content {\n padding: 12px;\n }\n ha-expansion-panel > *[slot="header"] {\n margin: 0;\n font-size: inherit;\n font-weight: inherit;\n }\n ha-expansion-panel ha-svg-icon {\n color: var(--secondary-text-color);\n }\n ha-expansion-panel .sort {\n margin-top: 8px;\n }\n\n .handle > ha-icon {\n pointer-events: none;\n }\n\n mwc-button.warning {\n --mdc-theme-primary: var(--error-color);\n }\n\n p.info {\n font-size: 0.875rem;\n color: var(--secondary-text-color);\n }\n '])))]}}])}();nt([fe()],Ln.prototype,"_config",void 0),nt([de()],Ln.prototype,"hass",void 0),customElements.define("auto-entities-filter-editor",Ln);var In=function(){function t(){return Z(this,t),H(this,t,arguments)}return q(t,se),X(t,[{key:"_changeSortOptions",value:function(t){if(this._config){var e=t.detail.value;this._config=Object.assign(Object.assign({},this._config),{sort:e}),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}}},{key:"render",value:function(){var t,e=null!==(t=this._config.sort)&&void 0!==t?t:{};return Gt(v||(v=P(["\n
\n \n
\n "])),this.hass,e,Mn(e.method),(function(t){var e;return null!==(e=t.label)&&void 0!==e?e:t.name}),this._changeSortOptions)}}])}();nt([fe()],In.prototype,"_config",void 0),nt([de()],In.prototype,"hass",void 0),customElements.define("auto-entities-sorting-editor",In);var Fn=function(){function t(){var e;return Z(this,t),(e=H(this,t,arguments))._cardGUIMode=!0,e._cardGUIModeAvailable=!0,e}return q(t,se),X(t,[{key:"_changeCardOptions",value:function(t){if(this._config){var e=t.detail.value;this._config=Object.assign(Object.assign({},this._config),e),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}}},{key:"_toggleCardMode",value:function(t){var e;null===(e=this._cardEditorEl)||void 0===e||e.toggleMode()}},{key:"_deleteCard",value:function(t){this._config&&(this._config=Object.assign({},this._config),delete this._config.card,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}},{key:"_getCardConfig",value:function(){var t=Object.assign({},this._config.card);return t[this._config.card_param||"entities"]=[],t}},{key:"_handleCardConfigChanged",value:function(t){if(t.stopPropagation(),this._config){var 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}}))}}},{key:"_cardGUIModeChanged",value:function(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}},{key:"render",value:function(){var t,e=Object.assign({},this._config);return e.show_empty=null===(t=e.show_empty)||void 0===t||t,Gt(y||(y=P(['\n
\n \n

\n See\n \n auto-entities on github\n \n for usage instructions.\n

\n

Not all options are available in the GUI editor.

\n
\n '])),_n)}}],[{key:"styles",get:function(){return[ut(_||(_=P(["\n a {\n color: var(--primary-color);\n }\n "])))]}}])}();customElements.define("auto-entities-help",Dn),customElements.whenDefined("ha-yaml-editor").then((function(){var t=customElements.get("ha-yaml-editor").prototype,e=t.setValue;t.setValue=function(t){!this.autoUpdate&&Oe(t,this.value)||e.bind(this)(t)}}));var Hn=function(){function t(){var e;return Z(this,t),(e=H(this,t,arguments))._selectedTab="Filters",e}return q(t,se),X(t,[{key:"setConfig",value:function(t){this._config=t}},{key:"connectedCallback",value:function(){N(t,"connectedCallback",this,3)([]),Ae()}},{key:"_config_changed",value:function(t){t.stopPropagation(),this._config&&(this._config=t.detail.config,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}})))}},{key:"_handleSwitchTab",value:function(t){this._selectedTab=t.detail.name}},{key:"render",value:function(){var t=this;if(!this.hass||!this._config)return Gt(w||(w=P([""])));var e={Filters:function(){return Gt(x||(x=P([""])),t.hass,t._config,t._config_changed)},Sorting:function(){return Gt(k||(k=P([""])),t.hass,t._config,t._config_changed)},Card:function(){return Gt($||($=P([""])),t.hass,t.lovelace,t._config,t._config_changed)},"?":function(){return Gt(E||(E=P([""])))}};return Gt(A||(A=P(["\n
\n \n ","\n \n\n
","
\n
\n "])),this._handleSwitchTab,Object.keys(e).map((function(e){return Gt(O||(O=P(['\n =0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=r.call(a,"catchLoc"),c=r.call(a,"finallyLoc");if(u&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),O(n),m}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;O(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:j(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),m}},t}function y(e,t,n,r,o,i,a){try{var s=e[i](a),u=s.value}catch(e){return void n(e)}s.done?t(u):Promise.resolve(u).then(r,o)}function m(e){return function(){var t=this,n=arguments;return new Promise((function(r,o){var i=e.apply(t,n);function a(e){y(i,r,o,a,s,"next",e)}function s(e){y(i,r,o,a,s,"throw",e)}a(void 0)}))}}function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&E(e,t)}function g(e){var t=x();return function(){var n,r=A(e);if(t){var o=A(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return function(e,t){if(t&&("object"===M(t)||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return w(e)}(this,n)}}function w(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function _(e){var t="function"==typeof Map?new Map:void 0;return _=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf("[native code]")}catch(t){return"function"==typeof e}}(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return k(e,arguments,A(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),E(n,e)},_(e)}function k(e,t,n){return k=x()?Reflect.construct.bind():function(e,t,n){var r=[null];r.push.apply(r,t);var o=new(Function.bind.apply(e,r));return n&&E(o,n.prototype),o},k.apply(null,arguments)}function x(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function E(e,t){return E=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},E(e,t)}function A(e){return A=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},A(e)}function $(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=S(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,o=function(){};return{s:o,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return a=e.done,e},e:function(e){s=!0,i=e},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw i}}}}function S(e,t){if(e){if("string"==typeof e)return C(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?C(e,t):void 0}}function C(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0;s--)(o=e[s])&&(a=(i<3?o(a):i>3?o(t,n,a):o(t,n))||a);return i>3&&a&&Object.defineProperty(t,n,a),a}"function"==typeof SuppressedError&&SuppressedError;var R=globalThis,L=R.ShadowRoot&&(void 0===R.ShadyCSS||R.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,U=Symbol(),D=new WeakMap,H=function(){function e(t,n,r){if(P(this,e),this._$cssResult$=!0,r!==U)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=n}return T(e,[{key:"styleSheet",get:function(){var e=this.o,t=this.t;if(L&&void 0===e){var n=void 0!==t&&1===t.length;n&&(e=D.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),n&&D.set(t,e))}return e}},{key:"toString",value:function(){return this.cssText}}]),e}(),I=L?function(e){return e}:function(e){return e instanceof CSSStyleSheet?function(e){var t,n="",r=$(e.cssRules);try{for(r.s();!(t=r.n()).done;){n+=t.value.cssText}}catch(e){r.e(e)}finally{r.f()}return function(e){return new H("string"==typeof e?e:e+"",void 0,U)}(n)}(e):e},q=Object.is,V=Object.defineProperty,z=Object.getOwnPropertyDescriptor,B=Object.getOwnPropertyNames,W=Object.getOwnPropertySymbols,J=Object.getPrototypeOf,F=globalThis,G=F.trustedTypes,Y=G?G.emptyScript:"",K=F.reactiveElementPolyfillSupport,Z=function(e,t){return e},X={toAttribute:function(e,t){switch(t){case Boolean:e=e?Y:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute:function(e,t){var n=e;switch(t){case Boolean:n=null!==e;break;case Number:n=null===e?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch(e){n=null}}return n}},Q=function(e,t){return!q(e,t)},ee={attribute:!0,type:String,converter:X,reflect:!1,hasChanged:Q};null!==(e=Symbol.metadata)&&void 0!==e||(Symbol.metadata=Symbol("metadata")),null!==(t=F.litPropertyMetadata)&&void 0!==t||(F.litPropertyMetadata=new WeakMap);var te=function(e){b(r,_(HTMLElement));var t,n=g(r);function r(){var e;return P(this,r),(e=n.call(this))._$Ep=void 0,e.isUpdatePending=!1,e.hasUpdated=!1,e._$Em=null,e._$Ev(),e}return T(r,[{key:"_$Ev",value:function(){var e,t=this;this._$Eg=new Promise((function(e){return t.enableUpdating=e})),this._$AL=new Map,this._$ES(),this.requestUpdate(),null===(e=this.constructor.l)||void 0===e||e.forEach((function(e){return e(t)}))}},{key:"addController",value:function(e){var t,n;(null!==(t=this._$E_)&&void 0!==t?t:this._$E_=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&(null===(n=e.hostConnected)||void 0===n||n.call(e))}},{key:"removeController",value:function(e){var t;null===(t=this._$E_)||void 0===t||t.delete(e)}},{key:"_$ES",value:function(){var e,t=new Map,n=$(this.constructor.elementProperties.keys());try{for(n.s();!(e=n.n()).done;){var r=e.value;this.hasOwnProperty(r)&&(t.set(r,this[r]),delete this[r])}}catch(e){n.e(e)}finally{n.f()}t.size>0&&(this._$Ep=t)}},{key:"createRenderRoot",value:function(){var e,t=null!==(e=this.shadowRoot)&&void 0!==e?e:this.attachShadow(this.constructor.shadowRootOptions);return function(e,t){if(L)e.adoptedStyleSheets=t.map((function(e){return e instanceof CSSStyleSheet?e:e.styleSheet}));else{var n,r=$(t);try{for(r.s();!(n=r.n()).done;){var o=n.value,i=document.createElement("style"),a=R.litNonce;void 0!==a&&i.setAttribute("nonce",a),i.textContent=o.cssText,e.appendChild(i)}}catch(e){r.e(e)}finally{r.f()}}}(t,this.constructor.elementStyles),t}},{key:"connectedCallback",value:function(){var e,t;null!==(e=this.renderRoot)&&void 0!==e||(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$E_)||void 0===t||t.forEach((function(e){var t;return null===(t=e.hostConnected)||void 0===t?void 0:t.call(e)}))}},{key:"enableUpdating",value:function(e){}},{key:"disconnectedCallback",value:function(){var e;null===(e=this._$E_)||void 0===e||e.forEach((function(e){var t;return null===(t=e.hostDisconnected)||void 0===t?void 0:t.call(e)}))}},{key:"attributeChangedCallback",value:function(e,t,n){this._$AK(e,n)}},{key:"_$EO",value:function(e,t){var n=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,n);if(void 0!==r&&!0===n.reflect){var o,i=(void 0!==(null===(o=n.converter)||void 0===o?void 0:o.toAttribute)?n.converter:X).toAttribute(t,n.type);this._$Em=e,null==i?this.removeAttribute(r):this.setAttribute(r,i),this._$Em=null}}},{key:"_$AK",value:function(e,t){var n=this.constructor,r=n._$Eh.get(e);if(void 0!==r&&this._$Em!==r){var o,i=n.getPropertyOptions(r),a="function"==typeof i.converter?{fromAttribute:i.converter}:void 0!==(null===(o=i.converter)||void 0===o?void 0:o.fromAttribute)?i.converter:X;this._$Em=r,this[r]=a.fromAttribute(t,i.type),this._$Em=null}}},{key:"requestUpdate",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],o=arguments.length>4?arguments[4]:void 0;if(void 0!==e){var i,a;if(null!==(i=n)&&void 0!==i||(n=this.constructor.getPropertyOptions(e)),!(null!==(a=n.hasChanged)&&void 0!==a?a:Q)(r?o:this[e],t))return;this.C(e,t,n)}!1===this.isUpdatePending&&(this._$Eg=this._$EP())}},{key:"C",value:function(e,t,n){var r;this._$AL.has(e)||this._$AL.set(e,t),!0===n.reflect&&this._$Em!==e&&(null!==(r=this._$Ej)&&void 0!==r?r:this._$Ej=new Set).add(e)}},{key:"_$EP",value:(t=m(v().mark((function e(){var t;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return this.isUpdatePending=!0,e.prev=1,e.next=4,this._$Eg;case 4:e.next=9;break;case 6:e.prev=6,e.t0=e.catch(1),Promise.reject(e.t0);case 9:if(t=this.scheduleUpdate(),e.t1=null!=t,!e.t1){e.next=14;break}return e.next=14,t;case 14:return e.abrupt("return",!this.isUpdatePending);case 15:case"end":return e.stop()}}),e,this,[[1,6]])}))),function(){return t.apply(this,arguments)})},{key:"scheduleUpdate",value:function(){return this.performUpdate()}},{key:"performUpdate",value:function(){if(this.isUpdatePending){if(!this.hasUpdated){var e;if(null!==(e=this.renderRoot)&&void 0!==e||(this.renderRoot=this.createRenderRoot()),this._$Ep){var t,n=$(this._$Ep);try{for(n.s();!(t=n.n()).done;){var r=p(t.value,2),o=r[0],i=r[1];this[o]=i}}catch(e){n.e(e)}finally{n.f()}this._$Ep=void 0}var a=this.constructor.elementProperties;if(a.size>0){var s,u=$(a);try{for(u.s();!(s=u.n()).done;){var c=p(s.value,2),l=c[0],d=c[1];!0!==d.wrapped||this._$AL.has(l)||void 0===this[l]||this.C(l,this[l],d)}}catch(e){u.e(e)}finally{u.f()}}}var f=!1,h=this._$AL;try{var v;(f=this.shouldUpdate(h))?(this.willUpdate(h),null!==(v=this._$E_)&&void 0!==v&&v.forEach((function(e){var t;return null===(t=e.hostUpdate)||void 0===t?void 0:t.call(e)})),this.update(h)):this._$ET()}catch(h){throw f=!1,this._$ET(),h}f&&this._$AE(h)}}},{key:"willUpdate",value:function(e){}},{key:"_$AE",value:function(e){var t;null!==(t=this._$E_)&&void 0!==t&&t.forEach((function(e){var t;return null===(t=e.hostUpdated)||void 0===t?void 0:t.call(e)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}},{key:"_$ET",value:function(){this._$AL=new Map,this.isUpdatePending=!1}},{key:"updateComplete",get:function(){return this.getUpdateComplete()}},{key:"getUpdateComplete",value:function(){return this._$Eg}},{key:"shouldUpdate",value:function(e){return!0}},{key:"update",value:function(e){var t=this;this._$Ej&&(this._$Ej=this._$Ej.forEach((function(e){return t._$EO(e,t[e])}))),this._$ET()}},{key:"updated",value:function(e){}},{key:"firstUpdated",value:function(e){}}],[{key:"addInitializer",value:function(e){var t;this._$Ei(),(null!==(t=this.l)&&void 0!==t?t:this.l=[]).push(e)}},{key:"observedAttributes",get:function(){return this.finalize(),this._$Eh&&h(this._$Eh.keys())}},{key:"createProperty",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:ee;if(t.state&&(t.attribute=!1),this._$Ei(),this.elementProperties.set(e,t),!t.noAccessor){var n=Symbol(),r=this.getPropertyDescriptor(e,n,t);void 0!==r&&V(this.prototype,e,r)}}},{key:"getPropertyDescriptor",value:function(e,t,n){var r,o=null!==(r=z(this.prototype,e))&&void 0!==r?r:{get:function(){return this[t]},set:function(e){this[t]=e}},i=o.get,a=o.set;return{get:function(){return null==i?void 0:i.call(this)},set:function(t){var r=null==i?void 0:i.call(this);a.call(this,t),this.requestUpdate(e,r,n)},configurable:!0,enumerable:!0}}},{key:"getPropertyOptions",value:function(e){var t;return null!==(t=this.elementProperties.get(e))&&void 0!==t?t:ee}},{key:"_$Ei",value:function(){if(!this.hasOwnProperty(Z("elementProperties"))){var e=J(this);e.finalize(),void 0!==e.l&&(this.l=h(e.l)),this.elementProperties=new Map(e.elementProperties)}}},{key:"finalize",value:function(){if(!this.hasOwnProperty(Z("finalized"))){if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(Z("properties"))){var e,t=this.properties,n=$([].concat(h(B(t)),h(W(t))));try{for(n.s();!(e=n.n()).done;){var r=e.value;this.createProperty(r,t[r])}}catch(e){n.e(e)}finally{n.f()}}var o=this[Symbol.metadata];if(null!==o){var i=litPropertyMetadata.get(o);if(void 0!==i){var a,s=$(i);try{for(s.s();!(a=s.n()).done;){var u=p(a.value,2),c=u[0],l=u[1];this.elementProperties.set(c,l)}}catch(e){s.e(e)}finally{s.f()}}}this._$Eh=new Map;var d,f=$(this.elementProperties);try{for(f.s();!(d=f.n()).done;){var v=p(d.value,2),y=v[0],m=v[1],b=this._$Eu(y,m);void 0!==b&&this._$Eh.set(b,y)}}catch(e){f.e(e)}finally{f.f()}this.elementStyles=this.finalizeStyles(this.styles)}}},{key:"finalizeStyles",value:function(e){var t=[];if(Array.isArray(e)){var n,r=$(new Set(e.flat(1/0).reverse()));try{for(r.s();!(n=r.n()).done;){var o=n.value;t.unshift(I(o))}}catch(e){r.e(e)}finally{r.f()}}else void 0!==e&&t.push(I(e));return t}},{key:"_$Eu",value:function(e,t){var n=t.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof e?e.toLowerCase():void 0}}]),r}();te.elementStyles=[],te.shadowRootOptions={mode:"open"},te[Z("elementProperties")]=new Map,te[Z("finalized")]=new Map,null!=K&&K({ReactiveElement:te}),(null!==(n=F.reactiveElementVersions)&&void 0!==n?n:F.reactiveElementVersions=[]).push("2.0.2");var ne=globalThis,re=ne.trustedTypes,oe=re?re.createPolicy("lit-html",{createHTML:function(e){return e}}):void 0,ie="$lit$",ae="lit$".concat((Math.random()+"").slice(9),"$"),se="?"+ae,ue="<".concat(se,">"),ce=document,le=function(){return ce.createComment("")},de=function(e){return null===e||"object"!=M(e)&&"function"!=typeof e},fe=Array.isArray,he="[ \t\n\f\r]",pe=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ve=/-->/g,ye=/>/g,me=RegExp(">|".concat(he,"(?:([^\\s\"'>=/]+)(").concat(he,"*=").concat(he,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),be=/'/g,ge=/"/g,we=/^(?:script|style|textarea|title)$/i,_e=function(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o":"",a=pe,s=0;s"===l[0]?(a=null!==(h=n)&&void 0!==h?h:pe,d=-1):void 0===l[1]?d=-2:(d=a.lastIndex-l[2].length,c=l[1],a=void 0===l[3]?me:'"'===l[3]?ge:be):a===ge||a===be?a=me:a===ve||a===ye?a=pe:(a=me,n=void 0)}var p=a===me&&e[s+1].startsWith("/>")?" ":"";i+=a===pe?u+ue:d>=0?(o.push(c),u.slice(0,d)+ie+u.slice(d)+ae+p):u+ae+(-2===d?s:p)}return[$e(e,i+(e[r]||"")+(2===t?"":"")),o]},Ce=function(){function e(t,n){var r,o=t.strings,i=t._$litType$;P(this,e),this.parts=[];var a=0,s=0,u=o.length-1,c=this.parts,l=p(Se(o,i),2),d=l[0],f=l[1];if(this.el=e.createElement(d,n),Ae.currentNode=this.el.content,2===i){var v=this.el.content.firstChild;v.replaceWith.apply(v,h(v.childNodes))}for(;null!==(r=Ae.nextNode())&&c.length0){r.textContent=re?re.emptyScript:"";for(var E=0;E2&&void 0!==arguments[2]?arguments[2]:e,u=arguments.length>3?arguments[3]:void 0;if(t===ke)return t;var c=void 0!==u?null===(n=s._$Co)||void 0===n?void 0:n[u]:s._$Cl,l=de(t)?void 0:t._$litDirective$;return(null===(r=c)||void 0===r?void 0:r.constructor)!==l&&(null!==(o=c)&&void 0!==o&&null!==(i=o._$AO)&&void 0!==i&&i.call(o,!1),void 0===l?c=void 0:(c=new l(e))._$AT(e,s,u),void 0!==u?(null!==(a=s._$Co)&&void 0!==a?a:s._$Co=[])[u]=c:s._$Cl=c),void 0!==c&&(t=Pe(e,c._$AS(e,t.values),c,u)),t}var Oe=function(){function e(t,n){P(this,e),this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=n}return T(e,[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(e){var t,n=this._$AD,r=n.el.content,o=n.parts,i=(null!==(t=null==e?void 0:e.creationScope)&&void 0!==t?t:ce).importNode(r,!0);Ae.currentNode=i;for(var a=Ae.nextNode(),s=0,u=0,c=o[0];void 0!==c;){var l;if(s===c.index){var d=void 0;2===c.type?d=new Te(a,a.nextSibling,this,e):1===c.type?d=new c.ctor(a,c.name,c.strings,this,e):6===c.type&&(d=new Le(a,this,e)),this._$AV.push(d),c=o[++u]}s!==(null===(l=c)||void 0===l?void 0:l.index)&&(a=Ae.nextNode(),s++)}return Ae.currentNode=ce,i}},{key:"p",value:function(e){var t,n=0,r=$(this._$AV);try{for(r.s();!(t=r.n()).done;){var o=t.value;void 0!==o&&(void 0!==o.strings?(o._$AI(e,o,n),n+=o.strings.length-2):o._$AI(e[n])),n++}}catch(e){r.e(e)}finally{r.f()}}}]),e}(),Te=function(){function e(t,n,r,o){var i;P(this,e),this.type=2,this._$AH=xe,this._$AN=void 0,this._$AA=t,this._$AB=n,this._$AM=r,this.options=o,this._$Cv=null===(i=null==o?void 0:o.isConnected)||void 0===i||i}return T(e,[{key:"_$AU",get:function(){var e,t;return null!==(e=null===(t=this._$AM)||void 0===t?void 0:t._$AU)&&void 0!==e?e:this._$Cv}},{key:"parentNode",get:function(){var e,t=this._$AA.parentNode,n=this._$AM;return void 0!==n&&11===(null===(e=t)||void 0===e?void 0:e.nodeType)&&(t=n.parentNode),t}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(e){e=Pe(this,e,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),de(e)?e===xe||null==e||""===e?(this._$AH!==xe&&this._$AR(),this._$AH=xe):e!==this._$AH&&e!==ke&&this._(e):void 0!==e._$litType$?this.g(e):void 0!==e.nodeType?this.$(e):function(e){return fe(e)||"function"==typeof(null==e?void 0:e[Symbol.iterator])}(e)?this.T(e):this._(e)}},{key:"k",value:function(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}},{key:"$",value:function(e){this._$AH!==e&&(this._$AR(),this._$AH=this.k(e))}},{key:"_",value:function(e){this._$AH!==xe&&de(this._$AH)?this._$AA.nextSibling.data=e:this.$(ce.createTextNode(e)),this._$AH=e}},{key:"g",value:function(e){var t,n=e.values,r=e._$litType$,o="number"==typeof r?this._$AC(e):(void 0===r.el&&(r.el=Ce.createElement($e(r.h,r.h[0]),this.options)),r);if((null===(t=this._$AH)||void 0===t?void 0:t._$AD)===o)this._$AH.p(n);else{var i=new Oe(o,this),a=i.u(this.options);i.p(n),this.$(a),this._$AH=i}}},{key:"_$AC",value:function(e){var t=Ee.get(e.strings);return void 0===t&&Ee.set(e.strings,t=new Ce(e)),t}},{key:"T",value:function(t){fe(this._$AH)||(this._$AH=[],this._$AR());var n,r,o=this._$AH,i=0,a=$(t);try{for(a.s();!(r=a.n()).done;){var s=r.value;i===o.length?o.push(n=new e(this.k(le()),this.k(le()),this,this.options)):n=o[i],n._$AI(s),i++}}catch(e){a.e(e)}finally{a.f()}i0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,t=arguments.length>1?arguments[1]:void 0;for(null===(n=this._$AP)||void 0===n||n.call(this,!1,!0,t);e&&e!==this._$AB;){var n,r=e.nextSibling;e.remove(),e=r}}},{key:"setConnected",value:function(e){var t;void 0===this._$AM&&(this._$Cv=e,null===(t=this._$AP)||void 0===t||t.call(this,e))}}]),e}(),je=function(){function e(t,n,r,o,i){P(this,e),this.type=1,this._$AH=xe,this._$AN=void 0,this.element=t,this.name=n,this._$AM=o,this.options=i,r.length>2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=xe}return T(e,[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,n=arguments.length>2?arguments[2]:void 0,r=arguments.length>3?arguments[3]:void 0,o=this.strings,i=!1;if(void 0===o)e=Pe(this,e,t,0),(i=!de(e)||e!==this._$AH&&e!==ke)&&(this._$AH=e);else{var a,s,u=e;for(e=o[0],a=0;a1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==t?t:xe)!==ke){var n=this._$AH,r=e===xe&&n!==xe||e.capture!==n.capture||e.once!==n.once||e.passive!==n.passive,o=e!==xe&&(n===xe||r);r&&this.element.removeEventListener(this.name,this,n),o&&this.element.addEventListener(this.name,this,e),this._$AH=e}}},{key:"handleEvent",value:function(e){var t,n;"function"==typeof this._$AH?this._$AH.call(null!==(t=null===(n=this.options)||void 0===n?void 0:n.host)&&void 0!==t?t:this.element,e):this._$AH.handleEvent(e)}}]),n}(),Le=function(){function e(t,n,r){P(this,e),this.element=t,this.type=6,this._$AN=void 0,this._$AM=n,this.options=r}return T(e,[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(e){Pe(this,e)}}]),e}(),Ue=ne.litHtmlPolyfillSupport;null!=Ue&&Ue(Ce,Te),(null!==(r=ne.litHtmlVersions)&&void 0!==r?r:ne.litHtmlVersions=[]).push("3.1.0");var De=function(e){b(n,te);var t=g(n);function n(){var e;return P(this,n),(e=t.apply(this,arguments)).renderOptions={host:w(e)},e._$Do=void 0,e}return T(n,[{key:"createRenderRoot",value:function(){var e,t,r=f(A(n.prototype),"createRenderRoot",this).call(this);return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=r.firstChild),r}},{key:"update",value:function(e){var t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),f(A(n.prototype),"update",this).call(this,e),this._$Do=function(e,t,n){var r,o=null!==(r=null==n?void 0:n.renderBefore)&&void 0!==r?r:t,i=o._$litPart$;if(void 0===i){var a,s=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;o._$litPart$=i=new Te(t.insertBefore(le(),s),s,void 0,null!=n?n:{})}return i._$AI(e),i}(t,this.renderRoot,this.renderOptions)}},{key:"connectedCallback",value:function(){var e;f(A(n.prototype),"connectedCallback",this).call(this),null===(e=this._$Do)||void 0===e||e.setConnected(!0)}},{key:"disconnectedCallback",value:function(){var e;f(A(n.prototype),"disconnectedCallback",this).call(this),null===(e=this._$Do)||void 0===e||e.setConnected(!1)}},{key:"render",value:function(){return ke}}]),n}();De._$litElement$=!0,De.finalized=!0,null===(o=globalThis.litElementHydrateSupport)||void 0===o||o.call(globalThis,{LitElement:De});var He=globalThis.litElementPolyfillSupport;null==He||He({LitElement:De}),(null!==(i=globalThis.litElementVersions)&&void 0!==i?i:globalThis.litElementVersions=[]).push("4.0.2");var Ie,qe={attribute:!0,type:String,converter:X,reflect:!1,hasChanged:Q};function Ve(e){return function(t,n){return"object"==M(n)?function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:qe,t=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,r=n.kind,o=n.metadata,i=globalThis.litPropertyMetadata.get(o);if(void 0===i&&globalThis.litPropertyMetadata.set(o,i=new Map),i.set(n.name,e),"accessor"===r){var a=n.name;return{set:function(n){var r=t.get.call(this);t.set.call(this,n),this.requestUpdate(a,r,e)},init:function(t){return void 0!==t&&this.C(a,void 0,e),t}}}if("setter"===r){var s=n.name;return function(n){var r=this[s];t.call(this,n),this.requestUpdate(s,r,e)}}throw Error("Unsupported decorator location: "+r)}(e,t,n):function(e,t,n){var r=t.hasOwnProperty(n);return t.constructor.createProperty(n,r?l(l({},e),{},{wrapped:!0}):e),r?Object.getOwnPropertyDescriptor(t,n):void 0}(e,t,n)}}var ze=new WeakMap,Be=function(){},We=function(){function e(t){var n=this;P(this,e),this.subscribers=[],this.settlement=null,this[Ie]="Unpromise",this.promise="function"==typeof t?new Promise(t):t;var r=this.promise.then((function(e){var t=n.subscribers;n.subscribers=null,n.settlement={status:"fulfilled",value:e},null==t||t.forEach((function(t){(0,t.resolve)(e)}))}));"catch"in r&&r.catch((function(e){var t=n.subscribers;n.subscribers=null,n.settlement={status:"rejected",reason:e},null==t||t.forEach((function(t){(0,t.reject)(e)}))}))}var t,n,r;return T(e,[{key:"subscribe",value:function(){var e,t,n=this,r=this.settlement;if(null===r){if(null===this.subscribers)throw new Error("Unpromise settled but still has subscribers");var o=function(){var e,t;return{promise:new Promise((function(n,r){e=n,t=r})),resolve:e,reject:t}}();this.subscribers=function(e,t){return[].concat(h(e),[t])}(this.subscribers,o),e=o.promise,t=function(){null!==n.subscribers&&(n.subscribers=function(e,t){var n=e.indexOf(t);return-1!==n?function(e,t){return[].concat(h(e.slice(0,t)),h(e.slice(t+1)))}(e,n):e}(n.subscribers,o))}}else{e="fulfilled"===r.status?Promise.resolve(r.value):Promise.reject(r.reason),t=Be}return Object.assign(e,{unsubscribe:t})}},{key:"then",value:function(e,t){var n=this.subscribe(),r=n.unsubscribe;return Object.assign(n.then(e,t),{unsubscribe:r})}},{key:"catch",value:function(e){var t=this.subscribe(),n=t.unsubscribe;return Object.assign(t.catch(e),{unsubscribe:n})}},{key:"finally",value:function(e){var t=this.subscribe(),n=t.unsubscribe;return Object.assign(t.finally(e),{unsubscribe:n})}}],[{key:"proxy",value:function(t){var n=e.getSubscribablePromise(t);return M(n)<"u"?n:e.createSubscribablePromise(t)}},{key:"createSubscribablePromise",value:function(t){var n=new e(t);return ze.set(t,n),ze.set(n,n),n}},{key:"getSubscribablePromise",value:function(e){return ze.get(e)}},{key:"resolve",value:function(t){var n="object"==M(t)&&null!==t&&"then"in t&&"function"==typeof t.then?t:Promise.resolve(t);return e.proxy(n).subscribe()}},{key:"any",value:(r=m(v().mark((function t(n){var r;return v().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=(Array.isArray(n)?n:h(n)).map(e.resolve),t.prev=1,t.next=4,Promise.any(r);case 4:return t.abrupt("return",t.sent);case 5:return t.prev=5,r.forEach((function(e){(0,e.unsubscribe)()})),t.finish(5);case 8:case"end":return t.stop()}}),t,null,[[1,,5,8]])}))),function(e){return r.apply(this,arguments)})},{key:"race",value:(n=m(v().mark((function t(n){var r;return v().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=(Array.isArray(n)?n:h(n)).map(e.resolve),t.prev=1,t.next=4,Promise.race(r);case 4:return t.abrupt("return",t.sent);case 5:return t.prev=5,r.forEach((function(e){(0,e.unsubscribe)()})),t.finish(5);case 8:case"end":return t.stop()}}),t,null,[[1,,5,8]])}))),function(e){return n.apply(this,arguments)})},{key:"raceReferences",value:(t=m(v().mark((function e(t){var n,r,o;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.map(Fe),e.prev=1,e.next=4,Promise.race(n);case 4:return e.abrupt("return",e.sent);case 5:e.prev=5,r=$(n);try{for(r.s();!(o=r.n()).done;)o.value.unsubscribe()}catch(e){r.e(e)}finally{r.f()}return e.finish(5);case 9:case"end":return e.stop()}}),e,null,[[1,,5,9]])}))),function(e){return t.apply(this,arguments)})}]),e}();Ie=Symbol.toStringTag;var Je=We;function Fe(e){return Je.proxy(e).then((function(){return[e]}))}function Ge(){return Ye.apply(this,arguments)}function Ye(){return Ye=m(v().mark((function e(){var t;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Je.race([customElements.whenDefined("home-assistant"),customElements.whenDefined("hc-main")]);case 2:t=customElements.get("home-assistant")?"home-assistant":"hc-main";case 3:if(document.querySelector(t)){e.next=8;break}return e.next=6,new Promise((function(e){return window.setTimeout(e,100)}));case 6:e.next=3;break;case 8:return e.abrupt("return",document.querySelector(t));case 9:case"end":return e.stop()}}),e)}))),Ye.apply(this,arguments)}function Ke(){return Ze.apply(this,arguments)}function Ze(){return Ze=m(v().mark((function e(){var t;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Ge();case 2:t=e.sent;case 3:if(t.hass){e.next=8;break}return e.next=6,new Promise((function(e){return window.setTimeout(e,100)}));case 6:e.next=3;break;case 8:return e.abrupt("return",t.hass);case 9:case"end":return e.stop()}}),e)}))),Ze.apply(this,arguments)}var Xe="browser_mod-browser-id";var Qe="SELECTTREE-TIMEOUT";function et(e){return tt.apply(this,arguments)}function tt(){return tt=m(v().mark((function e(t){var n,r,o,i=arguments;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(n=i.length>1&&void 0!==i[1]&&i[1],!(null===(r=t.localName)||void 0===r?void 0:r.includes("-"))){e.next=4;break}return e.next=4,customElements.whenDefined(t.localName);case 4:if(!t.updateComplete){e.next=7;break}return e.next=7,t.updateComplete;case 7:if(!n){e.next=18;break}if(!t.pageRendered){e.next=11;break}return e.next=11,t.pageRendered;case 11:if(!t._panelState){e.next=18;break}o=0;case 13:if(!("loaded"!==t._panelState&&o++<5)){e.next=18;break}return e.next=16,new Promise((function(e){return setTimeout(e,100)}));case 16:e.next=13;break;case 18:case"end":return e.stop()}}),e)}))),tt.apply(this,arguments)}function nt(e,t){return rt.apply(this,arguments)}function rt(){return rt=m(v().mark((function e(t,n){var r,o,i,a,s,u,c,l=arguments;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:for(r=l.length>2&&void 0!==l[2]&&l[2],o=[t],"string"==typeof n&&(n=n.split(/(\$| )/));""===n[n.length-1];)n.pop();i=$(n.entries()),e.prev=5,i.s();case 7:if((a=i.n()).done){e.next=24;break}if(s=p(a.value,2),s[0],"$"!==(u=s[1])){e.next=14;break}return e.next=12,Promise.all(h(o).map((function(e){return et(e)})));case 12:return o=h(o).map((function(e){return e.shadowRoot})),e.abrupt("continue",22);case 14:if(c=o[0]){e.next=17;break}return e.abrupt("return",null);case 17:if(u.trim().length){e.next=19;break}return e.abrupt("continue",22);case 19:return e.next=21,et(c);case 21:o=c.querySelectorAll(u);case 22:e.next=7;break;case 24:e.next=29;break;case 26:e.prev=26,e.t0=e.catch(5),i.e(e.t0);case 29:return e.prev=29,i.f(),e.finish(29);case 32:return e.abrupt("return",r?o:o[0]);case 33:case"end":return e.stop()}}),e,null,[[5,26,29,32]])}))),rt.apply(this,arguments)}function ot(e,t){return it.apply(this,arguments)}function it(){return it=m(v().mark((function e(t,n){var r,o,i=arguments;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=i.length>2&&void 0!==i[2]&&i[2],o=i.length>3&&void 0!==i[3]?i[3]:1e4,e.abrupt("return",Je.race([nt(t,n,r),new Promise((function(e,t){return setTimeout((function(){return t(new Error(Qe))}),o)}))]).catch((function(e){if(!e.message||e.message!==Qe)throw e;return null})));case 3:case"end":return e.stop()}}),e)}))),it.apply(this,arguments)}var at=null;function st(e){return ut.apply(this,arguments)}function ut(){return ut=m(v().mark((function e(t){var n,r,o;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return o=function(){return o=m(v().mark((function e(t){var n;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,ot(t,"home-assistant $ home-assistant-main $ partial-panel-resolver>*");case 2:if(n=e.sent){e.next=7;break}return e.next=6,ot(t,"hc-main $ hc-lovelace");case 6:n=e.sent;case 7:if(n){e.next=11;break}return e.next=10,ot(t,"hc-main $ hc-lovelace");case 10:n=e.sent;case 11:return e.abrupt("return",n);case 12:case"end":return e.stop()}}),e)}))),o.apply(this,arguments)},r=function(e){return o.apply(this,arguments)},e.next=4,r(t);case 4:n=e.sent;case 5:if(null!==n){e.next=13;break}return e.next=8,new Promise((function(e){return setTimeout(e,100)}));case 8:return e.next=10,r(t);case 10:n=e.sent,e.next=5;break;case 13:return e.abrupt("return",n);case 14:case"end":return e.stop()}}),e)}))),ut.apply(this,arguments)}function ct(e){var t,n,r;if(null===(t=null==e?void 0:e.hass)||void 0===t?void 0:t.localize){var o=function(e){return"lovelace"===(null==e?void 0:e.url_path)?"panel.states":"profile"===(null==e?void 0:e.url_path)?"panel.profile":"panel.".concat(null==e?void 0:e.title)}(e.panel);return e.hass.localize(o)||(null===(n=e.panel)||void 0===n?void 0:n.title)||""}return(null===(r=null==e?void 0:e.panel)||void 0===r?void 0:r.title)||""}function lt(e){var t,n,r,o,i;return{panelTitle:ct(e),panelUrlPath:(null===(n=null===(t=null==e?void 0:e.route)||void 0===t?void 0:t.prefix)||void 0===n?void 0:n.replace(/^\/|\/$/g,""))||"",panelComponentName:(null===(r=null==e?void 0:e.panel)||void 0===r?void 0:r.component_name)||"",panelIcon:(null===(o=null==e?void 0:e.panel)||void 0===o?void 0:o.icon)||"",panelNarrow:(null==e?void 0:e.narrow)||!1,panelRequireAdmin:(null===(i=null==e?void 0:e.panel)||void 0===i?void 0:i.require_admin)||!1}}function dt(e){return ft.apply(this,arguments)}function ft(){return ft=m(v().mark((function e(t){var n,r,o,i,a,s,u,c,l,d,f,h,p;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if("lovelace"===(null===(n=null==t?void 0:t.panel)||void 0===n?void 0:n.component_name)){e.next=2;break}return e.abrupt("return",{viewTitle:"",viewUrlPath:(null===(o=null===(r=null==t?void 0:t.route)||void 0===r?void 0:r.path)||void 0===o?void 0:o.replace(/^\/|\/$/g,""))||"",viewNarrow:(null==t?void 0:t.narrow)||!1});case 2:f=0;case 3:if((null===(i=t.shadowRoot)||void 0===i?void 0:i.querySelector("hui-root"))||!(f<100)){e.next=9;break}return e.next=6,new Promise((function(e){return setTimeout(e,10)}));case 6:f++,e.next=3;break;case 9:if(h=t.shadowRoot.querySelector("hui-root")){e.next=12;break}return e.abrupt("return",{});case 12:return p=h._curView||0,e.abrupt("return",{viewTitle:(null===(u=null===(s=null===(a=h.config)||void 0===a?void 0:a.views)||void 0===s?void 0:s[p])||void 0===u?void 0:u.title)||"",viewUrlPath:(null===(d=null===(l=null===(c=h.config)||void 0===c?void 0:c.views)||void 0===l?void 0:l[p])||void 0===d?void 0:d.path)||"".concat(p),viewNarrow:h.narrow||!1});case 14:case"end":return e.stop()}}),e)}))),ft.apply(this,arguments)}function ht(){return pt.apply(this,arguments)}function pt(){return(pt=m(v().mark((function e(){var t,n,r,o,i;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,st(document);case 2:return t=e.sent,n=lt(t),e.next=6,dt(t);case 6:return r=e.sent,o=[],n.panelTitle&&o.push(n.panelTitle),r.viewTitle&&o.push(r.viewTitle),i=[],n.panelUrlPath&&i.push(n.panelUrlPath),r.viewUrlPath&&i.push(r.viewUrlPath),e.abrupt("return",{hash:location.hash.substr(1)||"",panel:Object.assign(Object.assign({title:o.join(" - "),fullUrlPath:i.join("/")},n),r)});case 14:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function vt(){var e=function(){var e=m(v().mark((function e(){var t,n,r,o;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,ht();case 2:t=e.sent,n=window.location.pathname.slice(1).toLowerCase(),r=t.panel.fullUrlPath.toLowerCase(),o=0;case 6:if(!(n!==r&&o++<200)){e.next=16;break}return e.next=9,new Promise((function(e){return setTimeout(e,10)}));case 9:return e.next=11,ht();case 11:t=e.sent,n=window.location.pathname.slice(1).toLowerCase(),r=t.panel.fullUrlPath.toLowerCase(),e.next=6;break;case 16:return n!==r&&(console.groupCollapsed("Card-mod: cannot resolve Panel information after 2s."),console.log("Browser path:",n),console.log("Panel path:",r),console.log("Final panel state:",t),console.groupEnd()),e.abrupt("return",t);case 18:case"end":return e.stop()}}),e)})));return function(){return e.apply(this,arguments)}}();at=new Promise((function(t){return t(e())}))}window.addEventListener("card-mod-bootstrap",function(){var e=m(v().mark((function e(t){return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:t.stopPropagation(),["popstate","location-changed"].forEach((function(e){window.addEventListener(e,m(v().mark((function e(){return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:at=null,vt(),at.then((function(){document.dispatchEvent(new CustomEvent("cm_update",{detail:{variablesChanged:!0}}))}));case 3:case"end":return e.stop()}}),e)}))))}));case 2:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}()),window.cardMod_template_cache=window.cardMod_template_cache||{};var yt=window.cardMod_template_cache;function mt(e,t){var n=yt[e];n&&(n.value=t.result,n.debug&&(console.groupCollapsed("CardMod: Template updated"),console.log("Template:",n.template),console.log("Variables:",n.variables),console.log("Value:",n.value),console.groupEnd()),n.callbacks.forEach((function(e){return e(t.result)})))}function bt(e,t,n){return gt.apply(this,arguments)}function gt(){return(gt=m(v().mark((function e(t,n,r){var o,i,a,s,u,c;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Ke();case 2:return o=e.sent,e.next=5,at||vt(),at;case 5:i=e.sent,a=o.connection,r=Object.assign(Object.assign({user:o.user.name,browser:document.querySelector("hc-main")?"CAST":localStorage[Xe]?localStorage[Xe]:""},i),r),s=JSON.stringify([n,r]),(u=yt[s])?(u.debug&&(console.groupCollapsed("CardMod: Reusing template"),console.log("Template:",u.template),console.log("Variables:",u.variables),console.log("Value:",u.value),console.groupEnd()),u.callbacks.has(t)||wt(t),t(u.value),u.callbacks.add(t),u.cooldownTimeoutID&&clearTimeout(u.cooldownTimeoutID),u.cooldownTimeoutID=void 0):(c=!1,wt(t),t(""),n.includes("card_mod.debug")&&(c=!0,console.groupCollapsed("CardMod: Binding template"),console.log("Template:",n),console.log("Variables:",r),console.groupEnd()),yt[s]=u={template:n,variables:r,value:"",callbacks:new Set([t]),debug:c,unsubscribe:a.subscribeMessage((function(e){return mt(s,e)}),{type:"render_template",template:n,variables:r})});case 11:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function wt(e){return _t.apply(this,arguments)}function _t(){return(_t=m(v().mark((function e(t){var n,r,o,i,a;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:n=0,r=Object.entries(yt);case 1:if(!(n2?n-2:0),o=2;o1?n-1:0),o=1;o3&&void 0!==l[3]?l[3]:{},i=!(l.length>4&&void 0!==l[4])||l[4],c=!1,void 0!==(a=l.length>5&&void 0!==l[5]?l[5]:void 0)&&"string"!=typeof a&&(c=!0,i=a,a=void 0),"boolean"!=typeof i&&(i=!0,c=!0),"string"==typeof(r=l.length>2&&void 0!==l[2]?l[2]:void 0)&&(r={style:r},c=!0),r&&0!==Object.keys(r).length&&void 0===(null!==(u=null!==(s=null==r?void 0:r.style)&&void 0!==s?s:null==r?void 0:r.class)&&void 0!==u?u:null==r?void 0:r.debug)&&(r={style:r},c=!0),c&&!window.cm_compatibility_warning&&(window.cm_compatibility_warning=!0,console.groupCollapsed("Card-mod warning"),console.info("You are using a custom card which relies on card-mod, and uses an outdated signature for applyToElement."),console.info("The outdated signature will be removed at some point in the future. Hopefully the developer of your card will have updated their card by then."),console.info("The card used card-mod to apply styles here:",t),console.groupEnd()),e.abrupt("return",Rt(t,n,r,o,i,a));case 11:case"end":return e.stop()}}),e)}))),Nt.apply(this,arguments)}function Rt(e,t){return Lt.apply(this,arguments)}function Lt(){return Lt=m(v().mark((function e(t,n){var r,o,i,a,s,u,c,l,d,f,p,y,b,g,w,_=arguments;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(i=_.length>3&&void 0!==_[3]?_[3]:{},a=!(_.length>4&&void 0!==_[4])||_[4],s=_.length>5&&void 0!==_[5]?_[5]:void 0,b=(null==(o=_.length>2&&void 0!==_[2]?_[2]:void 0)?void 0:o.debug)?function(){for(var e,t=arguments.length,n=new Array(t),r=0;r2&&void 0!==u[2]?u[2]:0,o=this.parentElement||this.parentNode,e.next=4,ot(o,t,!0);case 4:if((i=e.sent)&&i.length){e.next=12;break}if(!(r>5)){e.next=8;break}throw new Error("NoElements");case 8:return a=new Promise((function(e,t){setTimeout(e,100*r),s._cancel_style_child.push(t)})),e.next=11,a.catch((function(e){throw new Error("Cancelled")}));case 11:return e.abrupt("return",this._style_child(t,n,r+1));case 12:return e.abrupt("return",h(i).map(function(){var e=m(v().mark((function e(t){var r;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Rt(t,"".concat(s.type,"-child"),{style:n,debug:s.debug},s.variables,!1);case 2:return(r=e.sent)&&(r.card_mod_parent=s),e.abrupt("return",r);case 5:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}()));case 13:case"end":return e.stop()}}),e,this)}))),function(e,t){return r.apply(this,arguments)})},{key:"_connect",value:(n=m(v().mark((function e(){var t,n,r,o,i,a,s,u,c,l,d,f,h,y=this;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:i=null!==(t=this._fixed_styles)&&void 0!==t?t:{},a={},s="",u=!1,this._debug("(Re)connecting",this),this.cancelStyleChild(),c=v().mark((function e(){var t,n,r;return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:t=p(d[l],2),n=t[0],r=t[1],"."===n?"string"==typeof r?s=r:y._debug("Style of '.' must be a string: ",r):(u=!0,a[n]=y._style_child(n,r).catch((function(e){if("NoElements"!=e.message){if("Cancelled"!=e.message)throw e;y.debug&&(console.groupCollapsed("card-mod style_child cancelled while looking for elements"),console.info("Looked for ".concat(n)),console.info(y),console.groupEnd())}else y.debug&&(console.groupCollapsed("card-mod found no elements"),console.info("Looked for ".concat(n)),console.info(y),console.groupEnd())})));case 2:case"end":return e.stop()}}),e)})),l=0,d=Object.entries(i);case 8:if(!(l\n ","\n \n "])),this._rendered_styles)}}],[{key:"applyToElement",get:function(){return Mt}}]),s}();N([Ve({attribute:"card-mod-type",reflect:!0})],Ht.prototype,"type",void 0),N([Ve()],Ht.prototype,"_rendered_styles",void 0),customElements.get("card-mod")||(customElements.define("card-mod",Ht),console.info("%cCARD-MOD ".concat(Et," IS INSTALLED"),"color: green; font-weight: bold"),window.dispatchEvent(new Event("card-mod-bootstrap"))),m(v().mark((function e(){return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(void 0!==customElements.get("home-assistant")){e.next=5;break}return e.next=3,new Promise((function(e){return window.setTimeout(e,100)}));case 3:e.next=0;break;case 5:customElements.get("card-mod")||customElements.define("card-mod",Ht);case 6:case"end":return e.stop()}}),e)})))(),window.cardMod_patch_state=window.cardMod_patch_state||{};var It=window.cardMod_patch_state,qt=function(e,t,n){if("constructor"!==t){var r=e[t];e[t]=function(){for(var e=arguments.length,t=new Array(e),o=0;o1?t-1:0),r=1;r1?s-1:0),c=1;c1?t-1:0),r=1;r1?t-1:0),r=1;r1?t-1:0),r=1;r1?t-1:0),r=1;r1?t-1:0),r=1;r2?o-2:0),a=2;a2?a-2:0),u=2;u1?o-1:0),a=1;a1?n-1:0),o=1;o1?n-1:0),o=1;o1?t-1:0),r=1;r1?t-1:0),r=1;r1?r-1:0),i=1;i1&&void 0!==s[1]?s[1]:0,r=new Set,10!=n){e.next=4;break}return e.abrupt("return",r);case 4:if(t){e.next=6;break}return e.abrupt("return",r);case 6:if(!t.updateComplete){e.next=9;break}return e.next=9,t.updateComplete;case 9:if(t._cardMod){o=$(t._cardMod);try{for(o.s();!(i=o.n()).done;)(a=i.value).styles&&r.add(a)}catch(e){o.e(e)}finally{o.f()}}if(!t.parentElement){e.next=19;break}return e.t0=dn,e.t1=r,e.next=15,fn(t.parentElement,n+1);case 15:e.t2=e.sent,(0,e.t0)(e.t1,e.t2),e.next=26;break;case 19:if(!t.parentNode){e.next=26;break}return e.t3=dn,e.t4=r,e.next=24,fn(t.parentNode,n+1);case 24:e.t5=e.sent,(0,e.t3)(e.t4,e.t5);case 26:if(!t.host){e.next=33;break}return e.t6=dn,e.t7=r,e.next=31,fn(t.host,n+1);case 31:e.t8=e.sent,(0,e.t6)(e.t7,e.t8);case 33:return e.abrupt("return",r);case 34:case"end":return e.stop()}}),e)}))),hn.apply(this,arguments)}ln=N([Bt("ha-svg-icon")],ln);var pn=function(e){b(n,jt);var t=g(n);function n(){return P(this,n),t.apply(this,arguments)}return T(n,[{key:"updated",value:function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:new WeakSet;if(null==e)return e;var n=M(e);if("function"!==n&&!("undefined"!=typeof HTMLElement&&e instanceof HTMLElement||"undefined"!=typeof Element&&e instanceof Element)){if("object"!==n)return e;if(!t.has(e)){if(t.add(e),Array.isArray(e)){var r=e.map((function(e){return yn(e,t)})).filter((function(e){return void 0!==e}));return r}for(var o={},i=0,a=Object.entries(e);i2?r-2:0),i=2;i2?r-2:0),i=2;i1?t-1:0),r=1;r2?h-2:0),y=2;y1?n-1:0),o=1;o2?o-2:0),a=2;a1?a-1:0),u=1;u1?t-1:0),r=1;r1?t-1:0),r=1;r1?t-1:0),r=1;r1?t-1:0),r=1;r "," "])),this.card)}},{key:"getCardSize",value:function(){if(this._config.report_size)return this._config.report_size;var e=this.shadowRoot;return e&&(e=e.querySelector("ha-card card-maker")),e&&(e=e.getCardSize),e&&(e=e()),e||1}}]),r}();N([Ve()],Tn.prototype,"card",void 0),customElements.get("mod-card")||customElements.define("mod-card",Tn),m(v().mark((function e(){return v().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(void 0!==customElements.get("home-assistant")){e.next=5;break}return e.next=3,new Promise((function(e){return window.setTimeout(e,100)}));case 3:e.next=0;break;case 5:customElements.get("mod-card")||customElements.define("mod-card",Tn);case 6:case"end":return e.stop()}}),e)})))();var jn,Mn=[],Nn=$(document.querySelectorAll("script"));try{for(Nn.s();!(jn=Nn.n()).done;){var Rn=jn.value;if(null===(Cn=null===(Sn=null==Rn?void 0:Rn.innerText)||void 0===Sn?void 0:Sn.trim())||void 0===Cn?void 0:Cn.startsWith("import(")){var Ln,Un=null===(Pn=Rn.innerText.split("\n"))||void 0===Pn?void 0:Pn.map((function(e){return e.trim()})),Dn=$(Un);try{for(Dn.s();!(Ln=Dn.n()).done;){var Hn=Ln.value;Mn.push(Hn.replace(/^import\(\"/,"").replace(/\"\);/,""))}}catch(e){Dn.e(e)}finally{Dn.f()}}}}catch(e){Nn.e(e)}finally{Nn.f()}Mn.some((function(e){return e.includes("/card-mod.js")}))||console.info("You may not be getting optimal performance out of card-mod.\nSee https://github.com/thomasloven/lovelace-card-mod#performance-improvements"); diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-layout-card/lovelace-layout-card.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-layout-card/lovelace-layout-card.js new file mode 100644 index 0000000..eeae97a --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/lovelace-layout-card/lovelace-layout-card.js @@ -0,0 +1 @@ +var t,e,n,i,r,o,a,s,u,l,c,d,h,v,f,p,y,g,m,_,b,w,k,E,C;function x(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}function $(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function A(t){for(var e=1;e=0;--r){var o=this.tryEntries[r],a=o[4],s=this.prev,u=o[1],l=o[2];if(-1===o[0])return i("end"),!1;if(!u&&!l)throw Error("try statement without catch or finally");if(null!=o[0]&&o[0]<=s){if(s=0;--n){var i=this.tryEntries[n];if(i[0]>-1&&i[0]<=this.prev&&this.prev=0;--e){var n=this.tryEntries[e];if(n[2]===t)return this.complete(n[4],n[3]),E(n),d}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n[0]===t){var i=n[4];if("throw"===i.type){var r=i.arg;E(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(e,n,i){return this.delegate={i:x(e),r:n,n:i},"next"===this.method&&(this.arg=t),d}},e}function U(t,e,n,i,r,o,a){try{var s=t[o](a),u=s.value}catch(t){return void n(t)}s.done?e(u):Promise.resolve(u).then(i,r)}function L(t){return function(){var e=this,n=arguments;return new Promise((function(i,r){var o=t.apply(e,n);function a(t){U(o,i,r,a,s,"next",t)}function s(t){U(o,i,r,a,s,"throw",t)}a(void 0)}))}}function R(t,e,n){return e=G(e),function(t,e){if(e&&("object"==K(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return H(t)}(t,I()?Reflect.construct(e,n||[],G(t).constructor):e.apply(t,n))}function H(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function N(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&D(t,e)}function z(t){var e="function"==typeof Map?new Map:void 0;return z=function(t){if(null===t||!function(t){try{return-1!==Function.toString.call(t).indexOf("[native code]")}catch(e){return"function"==typeof t}}(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,n)}function n(){return function(t,e,n){if(I())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,e);var r=new(t.bind.apply(t,i));return n&&D(r,n.prototype),r}(t,arguments,G(this).constructor)}return n.prototype=Object.create(t.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),D(n,t)},z(t)}function I(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(I=function(){return!!t})()}function D(t,e){return D=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},D(t,e)}function G(t){return G=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},G(t)}function q(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=B(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0,r=function(){};return{s:r,n:function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,a=!0,s=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){s=!0,o=t},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw o}}}}function B(t,e){if(t){if("string"==typeof t)return W(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?W(t,e):void 0}}function W(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=0;s--)(r=t[s])&&(a=(o<3?r(a):o>3?r(e,n,a):r(e,n))||a);return o>3&&a&&Object.defineProperty(e,n,a),a}"function"==typeof SuppressedError&&SuppressedError;var Z=globalThis,X=Z.ShadowRoot&&(void 0===Z.ShadyCSS||Z.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,tt=Symbol(),et=new WeakMap,nt=function(){return F((function t(e,n,i){if(Q(this,t),this._$cssResult$=!0,i!==tt)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=n}),[{key:"styleSheet",get:function(){var t=this.o,e=this.t;if(X&&void 0===t){var n=void 0!==e&&1===e.length;n&&(t=et.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),n&&et.set(e,t))}return t}},{key:"toString",value:function(){return this.cssText}}])}(),it=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),i=1;i0&&(this._$Ep=e)}},{key:"createRenderRoot",value:function(){var t,e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return function(t,e){if(X)t.adoptedStyleSheets=e.map((function(t){return t instanceof CSSStyleSheet?t:t.styleSheet}));else{var n,i=q(e);try{for(i.s();!(n=i.n()).done;){var r=n.value,o=document.createElement("style"),a=Z.litNonce;void 0!==a&&o.setAttribute("nonce",a),o.textContent=r.cssText,t.appendChild(o)}}catch(t){i.e(t)}finally{i.f()}}}(e,this.constructor.elementStyles),e}},{key:"connectedCallback",value:function(){var t,e;null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$EO)||void 0===e||e.forEach((function(t){var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}},{key:"enableUpdating",value:function(t){}},{key:"disconnectedCallback",value:function(){var t;null===(t=this._$EO)||void 0===t||t.forEach((function(t){var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}},{key:"attributeChangedCallback",value:function(t,e,n){this._$AK(t,n)}},{key:"_$ET",value:function(t,e){var n=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,n);if(void 0!==i&&!0===n.reflect){var r,o=(void 0!==(null===(r=n.converter)||void 0===r?void 0:r.toAttribute)?n.converter:yt).toAttribute(e,n.type);this._$Em=t,null==o?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}},{key:"_$AK",value:function(t,e){var n=this.constructor,i=n._$Eh.get(t);if(void 0!==i&&this._$Em!==i){var r,o,a,s,u=n.getPropertyOptions(i),l="function"==typeof u.converter?{fromAttribute:u.converter}:void 0!==(null===(r=u.converter)||void 0===r?void 0:r.fromAttribute)?u.converter:yt;this._$Em=i,this[i]=null!==(o=null!==(a=l.fromAttribute(e,u.type))&&void 0!==a?a:null===(s=this._$Ej)||void 0===s?void 0:s.get(i))&&void 0!==o?o:null,this._$Em=null}}},{key:"requestUpdate",value:function(t,e,n){if(void 0!==t){var i,r,o=this.constructor,a=this[t];if(null!=n||(n=o.getPropertyOptions(t)),!((null!==(i=n.hasChanged)&&void 0!==i?i:gt)(a,e)||n.useDefault&&n.reflect&&a===(null===(r=this._$Ej)||void 0===r?void 0:r.get(t))&&!this.hasAttribute(o._$Eu(t,n))))return;this.C(t,e,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}},{key:"C",value:function(t,e,n,i){var r,o,a,s=n.useDefault,u=n.reflect,l=n.wrapped;s&&!(null!==(r=this._$Ej)&&void 0!==r?r:this._$Ej=new Map).has(t)&&(this._$Ej.set(t,null!==(o=null!=i?i:e)&&void 0!==o?o:this[t]),!0!==l||void 0!==i)||(this._$AL.has(t)||(this.hasUpdated||s||(e=void 0),this._$AL.set(t,e)),!0===u&&this._$Em!==t&&(null!==(a=this._$Eq)&&void 0!==a?a:this._$Eq=new Set).add(t))}},{key:"_$EP",value:(e=L(T().mark((function t(){var e;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return this.isUpdatePending=!0,t.prev=1,t.next=4,this._$ES;case 4:t.next=9;break;case 6:t.prev=6,t.t0=t.catch(1),Promise.reject(t.t0);case 9:if(e=this.scheduleUpdate(),t.t1=null!=e,!t.t1){t.next=14;break}return t.next=14,e;case 14:return t.abrupt("return",!this.isUpdatePending);case 15:case"end":return t.stop()}}),t,this,[[1,6]])}))),function(){return e.apply(this,arguments)})},{key:"scheduleUpdate",value:function(){return this.performUpdate()}},{key:"performUpdate",value:function(){if(this.isUpdatePending){if(!this.hasUpdated){var t;if(null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this._$Ep){var e,n=q(this._$Ep);try{for(n.s();!(e=n.n()).done;){var i=P(e.value,2),r=i[0],o=i[1];this[r]=o}}catch(t){n.e(t)}finally{n.f()}this._$Ep=void 0}var a=this.constructor.elementProperties;if(a.size>0){var s,u=q(a);try{for(u.s();!(s=u.n()).done;){var l=P(s.value,2),c=l[0],d=l[1],h=d.wrapped,v=this[c];!0!==h||this._$AL.has(c)||void 0===v||this.C(c,void 0,d,v)}}catch(t){u.e(t)}finally{u.f()}}}var f=!1,p=this._$AL;try{var y;(f=this.shouldUpdate(p))?(this.willUpdate(p),null!==(y=this._$EO)&&void 0!==y&&y.forEach((function(t){var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(p)):this._$EM()}catch(p){throw f=!1,this._$EM(),p}f&&this._$AE(p)}}},{key:"willUpdate",value:function(t){}},{key:"_$AE",value:function(t){var e;null!==(e=this._$EO)&&void 0!==e&&e.forEach((function(t){var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}},{key:"_$EM",value:function(){this._$AL=new Map,this.isUpdatePending=!1}},{key:"updateComplete",get:function(){return this.getUpdateComplete()}},{key:"getUpdateComplete",value:function(){return this._$ES}},{key:"shouldUpdate",value:function(t){return!0}},{key:"update",value:function(t){var e=this;this._$Eq&&(this._$Eq=this._$Eq.forEach((function(t){return e._$ET(t,e[t])}))),this._$EM()}},{key:"updated",value:function(t){}},{key:"firstUpdated",value:function(t){}}],[{key:"addInitializer",value:function(t){var e;this._$Ei(),(null!==(e=this.l)&&void 0!==e?e:this.l=[]).push(t)}},{key:"observedAttributes",get:function(){return this.finalize(),this._$Eh&&j(this._$Eh.keys())}},{key:"createProperty",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:mt;if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){var n=Symbol(),i=this.getPropertyDescriptor(t,n,e);void 0!==i&&at(this.prototype,t,i)}}},{key:"getPropertyDescriptor",value:function(t,e,n){var i,r=null!==(i=st(this.prototype,t))&&void 0!==i?i:{get:function(){return this[e]},set:function(t){this[e]=t}},o=r.get,a=r.set;return{get:o,set:function(e){var i=null==o?void 0:o.call(this);null!=a&&a.call(this,e),this.requestUpdate(t,i,n)},configurable:!0,enumerable:!0}}},{key:"getPropertyOptions",value:function(t){var e;return null!==(e=this.elementProperties.get(t))&&void 0!==e?e:mt}},{key:"_$Ei",value:function(){if(!this.hasOwnProperty(pt("elementProperties"))){var t=ct(this);t.finalize(),void 0!==t.l&&(this.l=j(t.l)),this.elementProperties=new Map(t.elementProperties)}}},{key:"finalize",value:function(){if(!this.hasOwnProperty(pt("finalized"))){if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(pt("properties"))){var t,e=this.properties,n=q([].concat(j(ut(e)),j(lt(e))));try{for(n.s();!(t=n.n()).done;){var i=t.value;this.createProperty(i,e[i])}}catch(t){n.e(t)}finally{n.f()}}var r=this[Symbol.metadata];if(null!==r){var o=litPropertyMetadata.get(r);if(void 0!==o){var a,s=q(o);try{for(s.s();!(a=s.n()).done;){var u=P(a.value,2),l=u[0],c=u[1];this.elementProperties.set(l,c)}}catch(t){s.e(t)}finally{s.f()}}}this._$Eh=new Map;var d,h=q(this.elementProperties);try{for(h.s();!(d=h.n()).done;){var v=P(d.value,2),f=v[0],p=v[1],y=this._$Eu(f,p);void 0!==y&&this._$Eh.set(y,f)}}catch(t){h.e(t)}finally{h.f()}this.elementStyles=this.finalizeStyles(this.styles)}}},{key:"finalizeStyles",value:function(t){var e=[];if(Array.isArray(t)){var n,i=q(new Set(t.flat(1/0).reverse()));try{for(i.s();!(n=i.n()).done;){var r=n.value;e.unshift(rt(r))}}catch(t){i.e(t)}finally{i.f()}}else void 0!==t&&e.push(rt(t));return e}},{key:"_$Eu",value:function(t,e){var n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof t?t.toLowerCase():void 0}}]);var e}();_t.elementStyles=[],_t.shadowRootOptions={mode:"open"},_t[pt("elementProperties")]=new Map,_t[pt("finalized")]=new Map,null!=ft&&ft({ReactiveElement:_t}),(null!==(n=dt.reactiveElementVersions)&&void 0!==n?n:dt.reactiveElementVersions=[]).push("2.1.0");var bt=globalThis,wt=bt.trustedTypes,kt=wt?wt.createPolicy("lit-html",{createHTML:function(t){return t}}):void 0,Et="$lit$",Ct="lit$".concat(Math.random().toFixed(9).slice(2),"$"),xt="?"+Ct,$t="<".concat(xt,">"),At=document,St=function(){return At.createComment("")},Ot=function(t){return null===t||"object"!=K(t)&&"function"!=typeof t},Mt=Array.isArray,jt="[ \t\n\f\r]",Pt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Tt=/-->/g,Ut=/>/g,Lt=RegExp(">|".concat(jt,"(?:([^\\s\"'>=/]+)(").concat(jt,"*=").concat(jt,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),Rt=/'/g,Ht=/"/g,Nt=/^(?:script|style|textarea|title)$/i,zt=function(t){return function(e){for(var n=arguments.length,i=new Array(n>1?n-1:0),r=1;r":3===e?"":"",a=Pt,s=0;s"===c[0]?(a=null!=n?n:Pt,d=-1):void 0===c[1]?d=-2:(d=a.lastIndex-c[2].length,l=c[1],a=void 0===c[3]?Lt:'"'===c[3]?Ht:Rt):a===Ht||a===Rt?a=Lt:a===Tt||a===Ut?a=Pt:(a=Lt,n=void 0);var v=a===Lt&&t[s+1].startsWith("/>")?" ":"";o+=a===Pt?u+$t:d>=0?(r.push(l),u.slice(0,d)+Et+u.slice(d)+Ct+v):u+Ct+(-2===d?s:v)}return[Bt(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),r]},Qt=function(){return F((function t(e,n){var i,r=e.strings,o=e._$litType$;Q(this,t),this.parts=[];var a=0,s=0,u=r.length-1,l=this.parts,c=P(Wt(r,o),2),d=c[0],h=c[1];if(this.el=t.createElement(d,n),qt.currentNode=this.el.content,2===o||3===o){var v=this.el.content.firstChild;v.replaceWith.apply(v,j(v.childNodes))}for(;null!==(i=qt.nextNode())&&l.length0){i.textContent=wt?wt.emptyScript:"";for(var k=0;k2&&void 0!==arguments[2]?arguments[2]:t,u=arguments.length>3?arguments[3]:void 0;if(e===It)return e;var l=void 0!==u?null===(n=s._$Co)||void 0===n?void 0:n[u]:s._$Cl,c=Ot(e)?void 0:e._$litDirective$;return(null===(i=l)||void 0===i?void 0:i.constructor)!==c&&(null!==(r=l)&&void 0!==r&&null!==(o=r._$AO)&&void 0!==o&&o.call(r,!1),void 0===c?l=void 0:(l=new c(t))._$AT(t,s,u),void 0!==u?(null!==(a=s._$Co)&&void 0!==a?a:s._$Co=[])[u]=l:s._$Cl=l),void 0!==l&&(e=Vt(t,l._$AS(t,e.values),l,u)),e}var Ft=function(){return F((function t(e,n){Q(this,t),this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=n}),[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(t){var e,n=this._$AD,i=n.el.content,r=n.parts,o=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:At).importNode(i,!0);qt.currentNode=o;for(var a=qt.nextNode(),s=0,u=0,l=r[0];void 0!==l;){var c;if(s===l.index){var d=void 0;2===l.type?d=new Jt(a,a.nextSibling,this,t):1===l.type?d=new l.ctor(a,l.name,l.strings,this,t):6===l.type&&(d=new te(a,this,t)),this._$AV.push(d),l=r[++u]}s!==(null===(c=l)||void 0===c?void 0:c.index)&&(a=qt.nextNode(),s++)}return qt.currentNode=At,o}},{key:"p",value:function(t){var e,n=0,i=q(this._$AV);try{for(i.s();!(e=i.n()).done;){var r=e.value;void 0!==r&&(void 0!==r.strings?(r._$AI(t,r,n),n+=r.strings.length-2):r._$AI(t[n])),n++}}catch(t){i.e(t)}finally{i.f()}}}])}(),Jt=function(){function t(e,n,i,r){var o;Q(this,t),this.type=2,this._$AH=Dt,this._$AN=void 0,this._$AA=e,this._$AB=n,this._$AM=i,this.options=r,this._$Cv=null===(o=null==r?void 0:r.isConnected)||void 0===o||o}return F(t,[{key:"_$AU",get:function(){var t,e;return null!==(t=null===(e=this._$AM)||void 0===e?void 0:e._$AU)&&void 0!==t?t:this._$Cv}},{key:"parentNode",get:function(){var t,e=this._$AA.parentNode,n=this._$AM;return void 0!==n&&11===(null===(t=e)||void 0===t?void 0:t.nodeType)&&(e=n.parentNode),e}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){t=Vt(this,t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),Ot(t)?t===Dt||null==t||""===t?(this._$AH!==Dt&&this._$AR(),this._$AH=Dt):t!==this._$AH&&t!==It&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):function(t){return Mt(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])}(t)?this.k(t):this._(t)}},{key:"O",value:function(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}},{key:"T",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}},{key:"_",value:function(t){this._$AH!==Dt&&Ot(this._$AH)?this._$AA.nextSibling.data=t:this.T(At.createTextNode(t)),this._$AH=t}},{key:"$",value:function(t){var e,n=t.values,i=t._$litType$,r="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=Qt.createElement(Bt(i.h,i.h[0]),this.options)),i);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.p(n);else{var o=new Ft(r,this),a=o.u(this.options);o.p(n),this.T(a),this._$AH=o}}},{key:"_$AC",value:function(t){var e=Gt.get(t.strings);return void 0===e&&Gt.set(t.strings,e=new Qt(t)),e}},{key:"k",value:function(e){Mt(this._$AH)||(this._$AH=[],this._$AR());var n,i,r=this._$AH,o=0,a=q(e);try{for(a.s();!(i=a.n()).done;){var s=i.value;o===r.length?r.push(n=new t(this.O(St()),this.O(St()),this,this.options)):n=r[o],n._$AI(s),o++}}catch(t){a.e(t)}finally{a.f()}o0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,e=arguments.length>1?arguments[1]:void 0;for(null===(n=this._$AP)||void 0===n||n.call(this,!1,!0,e);t&&t!==this._$AB;){var n,i=t.nextSibling;t.remove(),t=i}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cv=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}])}(),Kt=function(){return F((function t(e,n,i,r,o){Q(this,t),this.type=1,this._$AH=Dt,this._$AN=void 0,this.element=e,this.name=n,this._$AM=r,this.options=o,i.length>2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Dt}),[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,n=arguments.length>2?arguments[2]:void 0,i=arguments.length>3?arguments[3]:void 0,r=this.strings,o=!1;if(void 0===r)t=Vt(this,t,e,0),(o=!Ot(t)||t!==this._$AH&&t!==It)&&(this._$AH=t);else{var a,s,u=t;for(t=r[0],a=0;a1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==e?e:Dt)!==It){var n=this._$AH,i=t===Dt&&n!==Dt||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,r=t!==Dt&&(n===Dt||i);i&&this.element.removeEventListener(this.name,this,n),r&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,n;"function"==typeof this._$AH?this._$AH.call(null!==(e=null===(n=this.options)||void 0===n?void 0:n.host)&&void 0!==e?e:this.element,t):this._$AH.handleEvent(t)}}])}(),te=function(){return F((function t(e,n,i){Q(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=n,this.options=i}),[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){Vt(this,t)}}])}(),ee=bt.litHtmlPolyfillSupport;null!=ee&&ee(Qt,Jt),(null!==(i=bt.litHtmlVersions)&&void 0!==i?i:bt.litHtmlVersions=[]).push("3.3.0");var ne=globalThis,ie=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).renderOptions={host:H(e)},e._$Do=void 0,e}return N(t,_t),F(t,[{key:"createRenderRoot",value:function(){var e,n,i=O(t,"createRenderRoot",this,3)([]);return null!==(n=(e=this.renderOptions).renderBefore)&&void 0!==n||(e.renderBefore=i.firstChild),i}},{key:"update",value:function(e){var n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),O(t,"update",this,3)([e]),this._$Do=function(t,e,n){var i,r=null!==(i=null==n?void 0:n.renderBefore)&&void 0!==i?i:e,o=r._$litPart$;if(void 0===o){var a,s=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;r._$litPart$=o=new Jt(e.insertBefore(St(),s),s,void 0,null!=n?n:{})}return o._$AI(t),o}(n,this.renderRoot,this.renderOptions)}},{key:"connectedCallback",value:function(){var e;O(t,"connectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!0)}},{key:"disconnectedCallback",value:function(){var e;O(t,"disconnectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!1)}},{key:"render",value:function(){return It}}])}();ie._$litElement$=!0,ie.finalized=!0,null===(r=ne.litElementHydrateSupport)||void 0===r||r.call(ne,{LitElement:ie});var re=ne.litElementPolyfillSupport;null==re||re({LitElement:ie}),(null!==(o=ne.litElementVersions)&&void 0!==o?o:ne.litElementVersions=[]).push("4.2.0");var oe={attribute:!0,type:String,converter:yt,reflect:!1,hasChanged:gt},ae=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe,e=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,i=n.kind,r=n.metadata,o=globalThis.litPropertyMetadata.get(r);if(void 0===o&&globalThis.litPropertyMetadata.set(r,o=new Map),"setter"===i&&((t=Object.create(t)).wrapped=!0),o.set(n.name,t),"accessor"===i){var a=n.name;return{set:function(n){var i=e.get.call(this);e.set.call(this,n),this.requestUpdate(a,i,t)},init:function(e){return void 0!==e&&this.C(a,void 0,t,e),e}}}if("setter"===i){var s=n.name;return function(n){var i=this[s];e.call(this,n),this.requestUpdate(s,i,t)}}throw Error("Unsupported decorator location: "+i)};function se(t){return function(e,n){return"object"==K(n)?ae(t,e,n):function(t,e,n){var i=e.hasOwnProperty(n);return e.constructor.createProperty(n,t),i?Object.getOwnPropertyDescriptor(e,n):void 0}(t,e,n)}}function ue(t){return se(A(A({},t),{},{state:!0,attribute:!1}))}var le=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).cards=[],e._editMode=!1,e._editorLoaded=!1,e}return N(t,ie),F(t,[{key:"setConfig",value:(n=L(T().mark((function t(e){return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:this._config=Object.assign({},e),this._config.view_layout&&void 0===this._config.layout&&(this._config.layout=this._config.view_layout);case 2:case"end":return t.stop()}}),t,this)}))),function(t){return n.apply(this,arguments)})},{key:"updated",value:(e=L(T().mark((function t(e){var n,i,r,o,a,s,u=this;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:e.has("lovelace")&&(null===(n=this.lovelace)||void 0===n?void 0:n.editMode)!=(null===(i=e.get("lovelace"))||void 0===i?void 0:i.editMode)&&((null===(r=this.lovelace)||void 0===r?void 0:r.editMode)&&!this._editorLoaded&&(this._editorLoaded=!0,(s=document.createElement("hui-masonry-view")).lovelace={editMode:!0},s.willUpdate(new Map)),this.cards.forEach((function(t){var e;return t.editMode=null===(e=u.lovelace)||void 0===e?void 0:e.editMode})),this._editMode=null!==(a=null===(o=this.lovelace)||void 0===o?void 0:o.editMode)&&void 0!==a&&a);case 1:case"end":return t.stop()}}),t,this)}))),function(t){return e.apply(this,arguments)})},{key:"_shouldShow",value:function(t,e,n){var i,r,o,a,s,u,l,c;return"always"===(null===(i=e.view_layout)||void 0===i?void 0:i.show)||"never"!==(null===(r=e.view_layout)||void 0===r?void 0:r.show)&&(("shown"!==(null===(a=null===(o=e.view_layout)||void 0===o?void 0:o.show)||void 0===a?void 0:a.sidebar)||"auto"!==(null===(s=this.hass)||void 0===s?void 0:s.dockedSidebar)&&!this.narrow)&&!("hidden"===(null===(l=null===(u=e.view_layout)||void 0===u?void 0:u.show)||void 0===l?void 0:l.sidebar)&&"docked"===(null===(c=this.hass)||void 0===c?void 0:c.dockedSidebar)&&!this.narrow))}},{key:"getCardElement",value:function(t){var e;if(!(null===(e=this.lovelace)||void 0===e?void 0:e.editMode))return t.card;var n=document.createElement("hui-card-options");return n.hass=this.hass,n.lovelace=this.lovelace,n.path=[this.index,t.index],t.card.editMode=!0,n.appendChild(t.card),!1===t.show&&(n.style.border="1px solid red"),n}},{key:"_addCard",value:function(){this.dispatchEvent(new CustomEvent("ll-create-card"))}},{key:"_render_fab",value:function(){var t;return!0==!(null===(t=this.lovelace)||void 0===t?void 0:t.editMode)?zt(a||(a=x([""]))):zt(s||(s=x(["\n
\n ',"\n "])),this._render_fab())}}],[{key:"styles",get:function(){return[this._fab_styles,it(c||(c=x(["\n :host {\n display: block;\n height: 100%;\n box-sizing: border-box;\n overflow-y: var(--layout-overflow);\n }\n\n #columns {\n display: grid;\n grid-auto-columns: minmax(\n var(--column-width),\n var(--column-max-width)\n );\n grid-template-columns: var(--column-widths);\n justify-content: center;\n justify-items: center;\n margin: var(--layout-margin);\n padding: var(--layout-padding);\n height: var(--layout-height);\n overflow-y: var(--layout-overflow);\n }\n .column {\n grid-row: 1/2;\n max-width: var(--column-max-width);\n width: 100%;\n }\n .column > * {\n display: block;\n margin: var(--card-margin);\n }\n "])))]}}]);var e,n,i,r,o,a,s}();Y([se()],ce.prototype,"_columns",void 0),Y([se()],ce.prototype,"_config",void 0);var de=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,ce),F(t,[{key:"_placeColumnCards",value:(e=L(T().mark((function t(e,n){var i,r,o,a,s,u,l;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:o=function(){for(var t=0,n=0;n
\n ',""])),this._render_fab())}}],[{key:"styles",get:function(){return[this._fab_styles,it(h||(h=x(["\n :host {\n height: 100%;\n box-sizing: border-box;\n }\n #root {\n display: grid;\n justify-content: stretch;\n margin: var(--layout-margin);\n padding: var(--layout-padding);\n height: var(--layout-height);\n overflow-y: var(--layout-overflow);\n }\n #root > * {\n margin: var(--masonry-view-card-margin, 4px 4px 8px);\n }\n "])))]}}]);var e,n,i}();customElements.define("grid-layout",ye);var ge=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).editMode=!1,e.isPanel=!1,e._cards=[],e}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=Object.assign({},t),this._config.entities&&(this._config.cards=this._config.entities.map((function(t){return t.type?t:Object.assign(Object.assign({},t),{type:"entity"})})));var e=t.layout_type;e?((null==e?void 0:e.endsWith("-layout"))||(e+="-layout"),e.startsWith("custom:")&&(e=e.substring(7))):e="hui-masonry-view",this._layoutType=e}},{key:"updated",value:(i=L(T().mark((function e(n){var i,r,o,a=this;return T().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(O(t,"updated",this,3)([n]),!n.has("_layoutType")&&!n.has("_config")){e.next=12;break}return r={type:this._layoutType,layout:this._config.layout||this._config.layout_options,cards:this._config.cards},o=document.createElement(this._layoutType),null===(i=o.setConfig)||void 0===i||i.call(o,r),this._layoutElement=o,e.next=8,this._createCards();case 8:this._layoutElement.hass=this.hass,this._layoutElement.narrow=!1,this._layoutElement.lovelace=Object.assign(Object.assign({},this._getLovelace()),{editMode:!1}),this._layoutElement.index=1;case 12:n.has("hass")&&(this._cards.forEach((function(t){t.hass=a.hass})),this._layoutElement&&(this._layoutElement.hass=this.hass)),n.has("_cards")&&this._layoutElement&&(this._layoutElement.cards=this._cards),n.has("editMode")&&this._layoutElement&&(this._layoutElement.lovelace=Object.assign(Object.assign({},this._getLovelace()),{editMode:!1}));case 15:case"end":return e.stop()}}),e,this)}))),function(t){return i.apply(this,arguments)})},{key:"_getLovelace",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this;return t.lovelace?t.lovelace:"home-assistant"!==t.localName?t.parentElement&&t.parentElement.host?this._getLovelace(t.parentElement.host):t.parentNode&&t.parentNode.host?this._getLovelace(t.parentNode.host):t.parentElement?this._getLovelace(t.parentElement):t.parentNode?this._getLovelace(t.parentNode):void 0:void 0}},{key:"_createCard",value:function(t,e){var n=this,i=e.createCardElement(t);return i.addEventListener("ll-rebuild",(function(e){e.stopPropagation(),n._rebuildCard(i,t)})),i.hass=this.hass,i}},{key:"_createCards",value:(n=L(T().mark((function t(){var e,n=this;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,window.loadCardHelpers();case 2:e=t.sent,this._cards=this._config.cards.map((function(t){return n._createCard(t,e)}));case 4:case"end":return t.stop()}}),t,this)}))),function(){return n.apply(this,arguments)})},{key:"_rebuildCard",value:(e=L(T().mark((function t(e,n){var i,r;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,window.loadCardHelpers();case 2:i=t.sent,r=this._createCard(n,i),e.parentElement&&e.parentElement.replaceChild(r,e),this._cards=this._cards.map((function(t){return t===e?r:t}));case 6:case"end":return t.stop()}}),t,this)}))),function(t,n){return e.apply(this,arguments)})},{key:"render",value:function(){return zt(v||(v=x(["",""])),this._layoutElement)}}],[{key:"styles",get:function(){return it(f||(f=x(["\n :host(:not(:first-child)) {\n margin-top: 0 !important;\n }\n :host(:not(:last-child)) {\n margin-bottom: 0 !important;\n }\n "])))}},{key:"getConfigElement",value:function(){return document.createElement("layout-card-editor")}},{key:"getStubConfig",value:function(){return{layout_type:"masonry",layout:{},cards:[]}}}]);var e,n,i}();Y([se()],ge.prototype,"hass",void 0),Y([se()],ge.prototype,"editMode",void 0),Y([se()],ge.prototype,"isPanel",void 0),Y([se()],ge.prototype,"_config",void 0),Y([se()],ge.prototype,"_cards",void 0),Y([se()],ge.prototype,"_layoutElement",void 0),Y([se()],ge.prototype,"_layoutType",void 0),customElements.define("layout-card",ge),window.customCards=window.customCards||[],window.customCards.push({type:"layout-card",name:"Layout Card",preview:!1,description:"Like a stack card, but with way more control."});var me=function(){var t=L(T().mark((function t(){var e,n,i,r;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!customElements.get("ha-form")){t.next=2;break}return t.abrupt("return");case 2:return t.next=4,null===(n=(e=window).loadCardHelpers)||void 0===n?void 0:n.call(e);case 4:if(i=t.sent){t.next=7;break}return t.abrupt("return");case 7:return t.next=9,i.createCardElement({type:"entity"});case 9:if(r=t.sent){t.next=12;break}return t.abrupt("return");case 12:return t.next=14,r.getConfigElement();case 14:case"end":return t.stop()}}),t)})));return function(){return t.apply(this,arguments)}}(),_e=[{value:"custom:masonry-layout",label:"Masonry (layout-card)"},{value:"custom:horizontal-layout",label:"Horizontal (layout-card)"},{value:"custom:vertical-layout",label:"Vertical (layout-card)"},{value:"custom:grid-layout",label:"Grid (layout-card)"}],be=["masonry","sidebar","panel"],we=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments))._selectedTab=0,e._selectedCard=0,e._cardGUIMode=!0,e._cardGUIModeAvailable=!0,e._schema=function(t){return[{name:"layout_type",selector:{select:{options:[].concat(j(be.map((function(e){return{value:e,label:t("ui.panel.lovelace.editor.edit_view.types.".concat(e))}}))),_e)}}},{name:"layout",selector:{object:{}}}]},e}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=t}},{key:"firstUpdated",value:(n=L(T().mark((function t(){return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return me(),t.next=3,this._preloadCardEditors();case 3:case"end":return t.stop()}}),t,this)}))),function(){return n.apply(this,arguments)})},{key:"_preloadCardEditors",value:(e=L(T().mark((function t(){var e,n,i,r,o;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,null===(n=(e=window).loadCardHelpers)||void 0===n?void 0:n.call(e);case 2:if(r=t.sent){t.next=5;break}return t.abrupt("return");case 5:return t.next=7,r.createCardElement({type:"vertical-stack",cards:[]});case 7:return o=t.sent,t.next=10,customElements.whenDefined("hui-vertical-stack-card");case 10:if(!(null===(i=null==o?void 0:o.constructor)||void 0===i?void 0:i.getConfigElement)){t.next=13;break}return t.next=13,o.constructor.getConfigElement();case 13:case"end":return t.stop()}}),t)}))),function(){return e.apply(this,arguments)})},{key:"_handleSwitchTab",value:function(t){t.stopPropagation(),this._selectedTab=parseInt(t.detail.name,10)}},{key:"_editCard",value:function(t){t.stopPropagation(),"add-card"!==t.detail.name?(this._cardGUIMode=!0,this._cardEditorEl&&(this._cardEditorEl.GUImode=!0),this._cardGUIModeAvailable=!0,this._selectedCard=parseInt(t.detail.name,10)):this._selectedCard=this._config.cards.length}},{key:"_addCard",value:function(t){t.stopPropagation();var e=j(this._config.cards);e.push(t.detail.config),this._config=Object.assign(Object.assign({},this._config),{cards:e}),this._selectedCard=this._config.cards.length-1,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_updateCard",value:function(t){t.stopPropagation();var e=j(this._config.cards);e[this._selectedCard]=t.detail.config,this._config=Object.assign(Object.assign({},this._config),{cards:e}),this._cardGUIModeAvailable=t.detail.guiModeAvailable,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_GUIModeChange",value:function(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}},{key:"_toggleMode",value:function(t){this._cardEditorEl.toggleMode()}},{key:"_moveCard",value:function(t){var e=this._selectedCard,n=e+t.currentTarget.move,i=j(this._config.cards),r=i.splice(e,1)[0];i.splice(n,0,r),this._config=Object.assign(Object.assign({},this._config),{cards:i}),this._selectedCard=n,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_deleteCard",value:function(){var t=j(this._config.cards);t.splice(this._selectedCard,1),this._config=Object.assign(Object.assign({},this._config),{cards:t}),this._selectedCard=Math.max(0,this._selectedCard-1),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_valueChanged",value:function(t){t.stopPropagation();var e=t.detail.value;this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"_computeLabel",value:function(t){return"layout_type"===t.name?this.hass.localize("ui.panel.lovelace.editor.edit_view.type"):"layout"===t.name?"Layout options (layout-card)":void 0}},{key:"render",value:function(){return this.hass&&this._config?zt(y||(y=x(['\n
\n \n \n Layout\n \n \n Cards\n \n \n
\n ',"\n
\n
\n "])),this._handleSwitchTab,0==this._selectedTab,0,1==this._selectedTab,1,[this._renderLayoutEditor,this._renderCardsEditor][this._selectedTab].bind(this)()):zt(p||(p=x([""])))}},{key:"_renderLayoutEditor",value:function(){var t=this._schema(this.hass.localize),e=Object.assign({},this._config);return zt(g||(g=x(['\n

\n See\n layout-card on GitHub for usage instructions.\n

\n \n \n \n \n \n
\n ',"\n
\n \n "])),this._editCard,this._config.cards.map((function(e,n){return zt(b||(b=x(['\n \n \n ',"\n \n \n \n \n \n \n \n \n \n \n \n \n "])),this._toggleMode,!this._cardGUIModeAvailable,this.hass.localize(this._cardEditorEl||this._cardGUIMode?"ui.panel.lovelace.editor.edit_card.show_code_editor":"ui.panel.lovelace.editor.edit_card.show_visual_editor"),0===t,this._moveCard,-1,"mdi:arrow-left",t===e-1,this._moveCard,1,"mdi:arrow-right",this._deleteCard,"mdi:delete",this.hass,this._config.cards[t],this.lovelace,this._updateCard,this._GUIModeChange):zt(k||(k=x(["\n \n "])),this.hass,this.lovelace,this._addCard))}}],[{key:"styles",get:function(){return[it(E||(E=x(['\n mwc-tab-bar {\n border-bottom: 1px solid var(--divider-color);\n }\n\n .layout,\n .cards #editor {\n margin-top: 8px;\n border: 1px solid var(--divider-color);\n padding: 12px;\n }\n\n #add-card {\n max-width: 32px;\n padding: 0;\n }\n ha-tab-group {\n margin-top: -16px;\n margin-bottom: 16px;\n }\n ha-tab-group-tab {\n flex: 1;\n }\n ha-tab-group-tab::part(base) {\n width: 100%;\n justify-content: center;\n }\n ha-tab-group-tab[panel="?"] {\n flex: 0;\n }\n\n .cards .card-options {\n display: flex;\n justify-content: flex-end;\n width: 100%;\n }\n #editor {\n border: 1px solid var(--divider-color);\n padding: 12px;\n }\n .gui-mode-button {\n margin-right: auto;\n }\n\n a {\n color: var(--primary-color);\n }\n '])))]}}]);var e,n}();Y([se()],we.prototype,"_config",void 0),Y([se()],we.prototype,"lovelace",void 0),Y([se()],we.prototype,"hass",void 0),Y([ue()],we.prototype,"_selectedTab",void 0),Y([ue()],we.prototype,"_selectedCard",void 0),Y([ue()],we.prototype,"_cardGUIMode",void 0),Y([ue()],we.prototype,"_cardGUIModeAvailable",void 0),Y([function(t){return function(e,n,i){return function(t,e,n){return n.configurable=!0,n.enumerable=!0,Reflect.decorate&&"object"!=K(e)&&Object.defineProperty(t,e,n),n}(e,n,{get:function(){return function(e){var n,i;return null!==(n=null===(i=e.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null}(this)}})}}("hui-card-element-editor")],we.prototype,"_cardEditorEl",void 0),customElements.define("layout-card-editor",we),customElements.whenDefined("hui-card-element-editor").then((function(){var t=customElements.get("hui-card-element-editor"),e=t.prototype.getConfigElement;t.prototype.getConfigElement=L(T().mark((function t(){var n,i;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,e.bind(this)();case 2:if(n=t.sent){i=n.setConfig;try{n.setConfig=function(t){var e=JSON.parse(JSON.stringify(t));this._layoutData=e.view_layout,delete e.view_layout,i.bind(this)(e)}}catch(t){console.warn(t)}}return t.abrupt("return",n);case 5:case"end":return t.stop()}}),t,this)})));var n=t.prototype._handleUIConfigChanged;t.prototype._handleUIConfigChanged=function(t){this._configElement&&this._configElement._layoutData&&(t.detail.config.view_layout=this._configElement._layoutData),n.bind(this)(t)}})),customElements.whenDefined("hui-view-editor").then((function(){var t=customElements.get("hui-view-editor"),e=t.prototype.firstUpdated;t.prototype.firstUpdated=function(){var t=this;null==e||e.bind(this)(),this._oldSchema=this._schema,this._schema=function(){var e,n=t._oldSchema.apply(t,arguments),i=n.find((function(t){return"type"==t.name}));if("layout"===i.name)return n;i.selector.select.options.find((function(t){return t.value===_e[0].value}))||(e=i.selector.select.options).push.apply(e,_e);return void 0===n.find((function(t){return"layout"===t.name}))&&n.push({name:"layout",selector:{object:{}}}),n};var n=document.createElement("p");n.innerHTML='\n You have layout-card installed which adds some options to this dialog.
\n Please see\n layout-card on GitHubfor usage instructions.\n \n ',this.shadowRoot.appendChild(n),this.requestUpdate()}}));var ke=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,z(HTMLElement)),F(t,[{key:"setConfig",value:function(t){var e,n;this.height=null!==(e=t.height)&&void 0!==e?e:50,this.size=null!==(n=t.size)&&void 0!==n?n:Math.ceil(this.height/50),this.style.cssText="\n display: block;\n height: ".concat(this.height,"px;\n ")}},{key:"getCardSize",value:function(){return this.size}}],[{key:"getConfigElement",value:function(){return document.createElement("gap-card-editor")}},{key:"getStubConfig",value:function(){return{}}}])}();customElements.define("gap-card",ke),window.customCards=window.customCards||[],window.customCards.push({type:"gap-card",name:"Gap Card",preview:!1,description:"Add a customizable gap in the layout."});var Ee=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=t}},{key:"heightChanged",value:function(t){var e=Object.assign({},this._config);delete e.height,t.detail.value&&(e.height=parseInt(t.detail.value)),this._config=e,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"sizeChanged",value:function(t){var e=Object.assign({},this._config);delete e.size,t.detail.value&&(e.size=parseInt(t.detail.value)),this._config=e,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"render",value:function(){return zt(C||(C=x(["\n t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new o("string"==typeof t?t:t+"",i))(e)})(t):t;var a;const l=window.trustedTypes,h=l?l.emptyScript:"",d=window.reactiveElementPolyfillSupport,c={toAttribute(t,e){switch(e){case Boolean:t=t?h:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},u=(t,e)=>e!==t&&(e==e||t==t),p={attribute:!0,type:String,converter:c,reflect:!1,hasChanged:u};class v extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o()}static addInitializer(t){var e;null!==(e=this.l)&&void 0!==e||(this.l=[]),this.l.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this._$Eh(i,e);void 0!==s&&(this._$Eu.set(s,i),t.push(s))})),t}static createProperty(t,e=p){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const o=this[t];this[e]=s,this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||p}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this._$Eu=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(r(t))}else void 0!==t&&e.push(r(t));return e}static _$Eh(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}o(){var t;this._$Ep=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Em(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$Eg)&&void 0!==e?e:this._$Eg=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$Eg)||void 0===e||e.splice(this._$Eg.indexOf(t)>>>0,1)}_$Em(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Et.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const i=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,i)=>{e?t.adoptedStyleSheets=i.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):i.forEach((e=>{const i=document.createElement("style"),s=window.litNonce;void 0!==s&&i.setAttribute("nonce",s),i.textContent=e.cssText,t.appendChild(i)}))})(i,this.constructor.elementStyles),i}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ES(t,e,i=p){var s,o;const n=this.constructor._$Eh(t,i);if(void 0!==n&&!0===i.reflect){const r=(null!==(o=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==o?o:c.toAttribute)(e,i.type);this._$Ei=t,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$Ei=null}}_$AK(t,e){var i,s,o;const n=this.constructor,r=n._$Eu.get(t);if(void 0!==r&&this._$Ei!==r){const t=n.getPropertyOptions(r),a=t.converter,l=null!==(o=null!==(s=null===(i=a)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof a?a:null)&&void 0!==o?o:c.fromAttribute;this._$Ei=r,this[r]=l(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||u)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this._$Ep=this._$E_())}async _$E_(){this.isUpdatePending=!0;try{await this._$Ep}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,e)=>this[e]=t)),this._$Et=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$Eg)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$ES(e,this[e],t))),this._$EC=void 0),this._$EU()}updated(t){}firstUpdated(t){}}var f;v.finalized=!0,v.elementProperties=new Map,v.elementStyles=[],v.shadowRootOptions={mode:"open"},null==d||d({ReactiveElement:v}),(null!==(a=globalThis.reactiveElementVersions)&&void 0!==a?a:globalThis.reactiveElementVersions=[]).push("1.3.2");const _=globalThis.trustedTypes,m=_?_.createPolicy("lit-html",{createHTML:t=>t}):void 0,$=`lit$${(Math.random()+"").slice(9)}$`,g="?"+$,y=`<${g}>`,b=document,A=(t="")=>b.createComment(t),w=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,S=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,C=/-->/g,x=/>/g,P=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,k=/'/g,U=/"/g,T=/^(?:script|style|textarea|title)$/i,M=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),H=Symbol.for("lit-noChange"),O=Symbol.for("lit-nothing"),R=new WeakMap,N=b.createTreeWalker(b,129,null,!1),L=(t,e)=>{const i=t.length-1,s=[];let o,n=2===e?"":"",r=S;for(let e=0;e"===l[0]?(r=null!=o?o:S,h=-1):void 0===l[1]?h=-2:(h=r.lastIndex-l[2].length,a=l[1],r=void 0===l[3]?P:'"'===l[3]?U:k):r===U||r===k?r=P:r===C||r===x?r=S:(r=P,o=void 0);const c=r===P&&t[e+1].startsWith("/>")?" ":"";n+=r===S?i+y:h>=0?(s.push(a),i.slice(0,h)+"$lit$"+i.slice(h)+$+c):i+$+(-2===h?(s.push(void 0),e):c)}const a=n+(t[i]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==m?m.createHTML(a):a,s]};class z{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let o=0,n=0;const r=t.length-1,a=this.parts,[l,h]=L(t,e);if(this.el=z.createElement(l,i),N.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=N.nextNode())&&a.length0){s.textContent=_?_.emptyScript:"";for(let i=0;i{var e;return E(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])})(t)?this.S(t):this.$(t)}M(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}k(t){this._$AH!==t&&(this._$AR(),this._$AH=this.M(t))}$(t){this._$AH!==O&&w(this._$AH)?this._$AA.nextSibling.data=t:this.k(b.createTextNode(t)),this._$AH=t}T(t){var e;const{values:i,_$litType$:s}=t,o="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=z.createElement(s.h,this.options)),s);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===o)this._$AH.m(i);else{const t=new D(o,this),e=t.p(this.options);t.m(i),this.k(e),this._$AH=t}}_$AC(t){let e=R.get(t.strings);return void 0===e&&R.set(t.strings,e=new z(t)),e}S(t){E(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const o of t)s===e.length?e.push(i=new j(this.M(A()),this.M(A()),this,this.options)):i=e[s],i._$AI(o),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=O}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,s){const o=this.strings;let n=!1;if(void 0===o)t=I(this,t,e,0),n=!w(t)||t!==this._$AH&&t!==H,n&&(this._$AH=t);else{const s=t;let r,a;for(t=o[0],r=0;r{var s,o;const n=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let r=n._$litPart$;if(void 0===r){const t=null!==(o=null==i?void 0:i.renderBefore)&&void 0!==o?o:null;n._$litPart$=r=new j(e.insertBefore(A(),t),t,void 0,null!=i?i:{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!1)}render(){return H}}Q.finalized=!0,Q._$litElement$=!0,null===(F=globalThis.litElementHydrateSupport)||void 0===F||F.call(globalThis,{LitElement:Q});const X=globalThis.litElementPolyfillSupport;null==X||X({LitElement:Q}),(null!==(G=globalThis.litElementVersions)&&void 0!==G?G:globalThis.litElementVersions=[]).push("3.2.0");const Y=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function tt(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):Y(t,e)}var et;null===(et=window.HTMLSlotElement)||void 0===et||et.prototype.assignedElements;let it=function(){const t="lovelace-player-device-id";if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage[t]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage[t]=`${e()}${e()}-${e()}${e()}`}return localStorage[t]}();function st(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}function ot(t){return!!String(t).includes("{%")||(!!String(t).includes("{{")||void 0)}window.cardMod_template_cache=window.cardMod_template_cache||{};const nt=window.cardMod_template_cache;async function rt(t,e,i){const s=st().connection,o=JSON.stringify([e,i]);let n=nt[o];n?(n.callbacks.has(t)||at(t),t(n.value),n.callbacks.add(t)):(at(t),t(""),i=Object.assign({user:st().user.name,browser:it,hash:location.hash.substr(1)||""},i),nt[o]=n={template:e,variables:i,value:"",callbacks:new Set([t]),unsubscribe:s.subscribeMessage((t=>function(t,e){const i=nt[t];i&&(i.value=e.result,i.callbacks.forEach((t=>t(e.result))))}(o,t)),{type:"render_template",template:e,variables:i})})}async function at(t){let e;for(const[i,s]of Object.entries(nt))if(s.callbacks.has(t)){s.callbacks.delete(t),0==s.callbacks.size&&(e=s.unsubscribe,delete nt[i]);break}e&&await(await e)()}var lt="1.9.6";class ht extends Q{constructor(){super(...arguments),this.connectedWhileHidden=!0,this.cardsInitialized=!1,this.templateRenderer=t=>{this._tmpl=t}}async setConfig(t){if(window.deviceID=it,this._config=t,this.state=void 0,this.classList.add("no-match"),this.cards={},this.buildCards(),"hash"===t.entity&&(window.addEventListener("location-changed",(()=>this.updated(new Map))),window.addEventListener("hashchange",(()=>this.updated(new Map)))),"mediaquery"===t.entity)for(const e in t.states){window.matchMedia(e).addEventListener("change",(()=>this.update_state()))}if("template"===t.entity||ot(t.entity)){const e=ot(t.entity)?t.entity:t.template;rt(this.templateRenderer,e,{config:t})}this.style.setProperty("display","none")}connectedCallback(){if(super.connectedCallback(),this._config){for(const t in this.cards)this.cards[t].hass=this._hass;("template"===this._config.entity||ot(this._config.entity))&&rt(this.templateRenderer,ot(this._config.entity)?this._config.entity:this._config.template,{config:this._config})}}disconnectedCallback(){super.disconnectedCallback(),at(this.templateRenderer)}async buildCards(){const t=await window.loadCardHelpers();for(let e in this._config.states)this.cards[e]=await t.createCardElement(this._config.states[e]),this.cards[e].hass=this._hass;this.cardsInitialized=!0,this.update_state(),this._updateVisibility()}update_state(){var t,e,i,s,o,n;if(!this.cardsInitialized)return;let r;switch(this._config.entity){case"template":r=this._tmpl;break;case"user":r=null===(e=null===(t=this._hass)||void 0===t?void 0:t.user)||void 0===e?void 0:e.name;break;case"group":r=(null===(s=null===(i=this._hass)||void 0===i?void 0:i.user)||void 0===s?void 0:s.is_admin)?"admin":"user";break;case"deviceID":case"browser":r=it;break;case"hash":r=location.hash.substring(1);break;case"mediaquery":for(const t in this.cards)if(window.matchMedia(t).matches){r=t;break}break;default:r=ot(this._config.entity)?this._tmpl:null===(n=null===(o=this._hass)||void 0===o?void 0:o.states[this._config.entity])||void 0===n?void 0:n.state}void 0!==r&&this.cards.hasOwnProperty(r)||(r=this._config.default),this.state=r}set hass(t){this._hass=t;for(const e in this.cards)this.cards[e].hass=t}_updateVisibility(){this.cards[this.state]?(this.classList.remove("no-match"),this.style.setProperty("display",""),this.removeAttribute("hidden")):(this.classList.add("no-match"),this.style.setProperty("display","none"),this.setAttribute("hidden","")),this.dispatchEvent(new Event("card-visibility-changed",{bubbles:!0,cancelable:!0}))}updated(t){if(t.has("state")){const e=t.get("state");this.cards[e]&&(this.cards[e].parentElement.classList.remove("visible"),this._config.transition&&(this.shadowRoot.querySelector("#root").classList.add("transition"),this.cards[e].parentElement.classList.add("out"),window.setTimeout((()=>{this.cards[e].parentElement.classList.remove("out"),window.setTimeout((()=>{this.shadowRoot.querySelector("#root").classList.remove("transition")}),this._config.transition_time||500)}),this._config.transition_time||500))),this.cards[this.state]&&this.cards[this.state].parentElement.classList.add("visible"),this._updateVisibility()}else this.update_state()}render(){return M` +
+ ${Object.keys(this.cards).map((t=>M`
${this.cards[t]}
`))} +
+ `}async getCardSize(){var t;let e=1;for(const i in this.cards)(null===(t=this.cards[i])||void 0===t?void 0:t.getCardSize)&&(e=Math.max(e,await this.cards[i].getCardSize()));return e}static get styles(){return n` + :host { + perspective: 1000px; + } + :host(.no-match) { + display: none; + } + #root { + margin: -4px; + padding: 4px; + display: grid; + grid-template-rows: auto 0px; + overflow: clip visible; + } + #root.transition { + overflow: hidden; + } + #root * { + grid-column: 1; + grid-row: 2; + overflow: hidden; + min-width: 0; + } + #root *.visible, + #root *.out { + grid-row: 1; + overflow: visible; + } + + #root.slide-down *, + #root.slide-up *, + #root.slide-left *, + #root.slide-right * { + transition-property: transform; + transition-timing-function: linear; + transition-duration: inherit; + transform: translate(0, -110%); + } + #root.slide-up * { + transform: translate(0, 110%); + } + #root.slide-left * { + transform: translate(110%, 0); + } + #root.slide-right * { + transform: translate(-110%, 0); + } + #root.slide-down .visible, + #root.slide-up .visible, + #root.slide-left .visible, + #root.slide-right .visible { + transform: translate(0%); + } + #root.slide-down .out { + transform: translate(0, 110%); + } + #root.slide-up .out { + transform: translate(0, -110%); + } + #root.slide-left .out { + transform: translate(-110%); + } + #root.slide-right .out { + transform: translate(110%); + } + + #root.swap-down *, + #root.swap-up *, + #root.swap-left *, + #root.swap-right * { + transition-property: transform; + transition-timing-function: linear; + transition-duration: inherit; + transform: translate(0, 110%); + } + #root.swap-up * { + transform: translate(0, -110%); + } + #root.swap-left * { + transform: translate(-110%, 0); + } + #root.swap-right * { + transform: translate(110%, 0); + } + #root.swap-down .visible, + #root.swap-up .visible, + #root.swap-left .visible, + #root.swap-right .visible { + transition-delay: inherit; + transform: translate(0%); + } + + #root.flip, + #root.flip-x, + #root.flip-y { + position: relative; + perspective: 1000px; + } + #root.flip *, + #root.flip-x *, + #root.flip-y * { + transform: rotate3d(0, 1, 0, -180deg); + transition-property: transform; + transition-timing-function: linear; + transition-duration: inherit; + transform-style: preserve-3d; + backface-visibility: hidden; + z-index: 100; + } + #root.flip-y * { + transform: rotate3d(1, 0, 0, -180deg); + } + #root.flip .visible, + #root.flip-x .visible, + #root.flip-y .visible { + backface-visibility: hidden; + transform: rotate3d(0, 0, 0, 0deg); + } + #root.flip .out, + #root.flip-x .out, + #root.flip-y .out { + pointer-events: none; + transform: rotate3d(0, 1, 0, 180deg); + } + #root.flip-y .out { + transform: rotate3d(1, 0, 0, 180deg); + } + `}}t([tt()],ht.prototype,"_config",void 0),t([tt()],ht.prototype,"_hass",void 0),t([tt()],ht.prototype,"state",void 0),t([tt()],ht.prototype,"_tmpl",void 0),customElements.get("state-switch")||(customElements.define("state-switch",ht),console.info(`%cSTATE-SWITCH ${lt} IS INSTALLED`,"color: green; font-weight: bold","")); diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-graph-card/mini-graph-card.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-graph-card/mini-graph-card.js new file mode 100644 index 0000000..4b69870 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-graph-card/mini-graph-card.js @@ -0,0 +1 @@ +(function(){"use strict";function a(){var b=S([""]);return a=function(){return b},b}function b(){var a=S(["\n
\n ","\n \n "," ","\n \n \n ","\n \n
\n "]);return b=function(){return a},a}function c(){var a=S(["\n
\n ","\n
\n "]);return c=function(){return a},a}function d(){var a=S(["\n
\n ","\n ","\n
\n "]);return d=function(){return a},a}function e(){var a=S(["\n
\n ","\n ","\n
\n "]);return e=function(){return a},a}function f(){var a=S(["\n \n \n \n ","\n \n ","\n ","\n ","\n ","\n ","\n \n ","\n "]);return f=function(){return a},a}function g(){var a=S(["",""]);return g=function(){return a},a}function h(){var a=S(["\n \n ","\n "]);return h=function(){return a},a}function j(){var a=S(["\n \n "]);return j=function(){return a},a}function k(){var a=S(["\n "]);return k=function(){return a},a}function l(){var a=S(["\n "]);return l=function(){return a},a}function m(){var a=S(["",""]);return m=function(){return a},a}function n(){var a=S(["\n \n "]);return n=function(){return a},a}function o(){var a=S(["\n \n ","\n "]);return o=function(){return a},a}function p(){var a=S(["\n \n ","\n "]);return p=function(){return a},a}function q(){var a=S(["\n \n "]);return q=function(){return a},a}function r(){var a=S(["\n \n ","\n \n "]);return r=function(){return a},a}function s(){var a=S(["\n "]);return s=function(){return a},a}function t(){var a=S(["\n \n \n \n \n \n \n \n \n \n \n \n "]);return t=function(){return a},a}function u(){var a=S(["\n \n \n \n "]);return u=function(){return a},a}function v(){var a=S(["\n
\n ","\n ","\n
\n "]);return v=function(){return a},a}function w(){var a=S(["\n
\n ","\n
\n "]);return w=function(){return a},a}function x(){var a=S([""]);return x=function(){return a},a}function y(){var a=S(["\n
\n ","\n ","\n
\n ","\n
\n
\n ","\n "]);return y=function(){return a},a}function z(){var a=S(["\n
\n ","\n
"]);return z=function(){return a},a}function A(){var a=S(["\n "," -\n ","\n "]);return A=function(){return a},a}function B(){var a=S(["\n ","\n "]);return B=function(){return a},a}function C(){var a=S(["\n
\n ","\n
\n "]);return C=function(){return a},a}function D(){var a=S(["\n \n ","\n \n ","\n \n \n ","\n \n ","\n \n "]);return D=function(){return a},a}function E(){var a=S(["\n
\n ","\n
","
\n ","\n
\n "]);return E=function(){return a},a}function F(){var a=S(["\n
\n ","\n
\n "]);return F=function(){return a},a}function G(){var a=S(["\n
\n \n
\n "]);return G=function(){return a},a}function H(){var a=S(["\n
\n \n
\n "]);return H=function(){return a},a}function I(){var a=S(["\n
\n "," ","\n
\n "]);return I=function(){return a},a}function J(){var a=S([""]);return J=function(){return a},a}function K(){var a=S(["\n
\n Entity not available: ","\n
\n "]);return K=function(){return a},a}function L(){var a=S(["\n \n
mini-graph-card
\n ","\n
\n "]);return L=function(){return a},a}function M(){var a=S(["\n \n "," "," "," ","\n \n "]);return M=function(){return a},a}function N(){var a=S([""]);return N=function(){return a},a}function O(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);b&&(d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable})),c.push.apply(c,d)}return c}function P(a){for(var b,c=1;c div {\n padding: 0px 16px 16px 16px;\n }\n ha-card > div:last-child {\n padding-bottom: 0;\n }\n ha-card .graph {\n padding: 0;\n order: 10;\n }\n ha-card[points] .line--points,\n ha-card[labels] .graph__labels.--primary {\n opacity: 0;\n transition: opacity .25s;\n animation: none;\n }\n ha-card[labels-secondary] .graph__labels.--secondary {\n opacity: 0;\n transition: opacity .25s;\n animation: none;\n }\n ha-card[points]:hover .line--points,\n ha-card:hover .graph__labels.--primary,\n ha-card:hover .graph__labels.--secondary {\n opacity: 1;\n }\n ha-card[fill] path {\n stroke-linecap: initial;\n stroke-linejoin: initial;\n }\n ha-card .graph__legend {\n order: -1;\n padding: 0 16px 8px 16px;\n }\n ha-card[group] {\n box-shadow: none;\n border: none;\n padding: 0;\n }\n ha-card[group] > div {\n padding-left: 0;\n padding-right: 0;\n }\n ha-card[group] .graph__legend {\n padding-left: 0;\n padding-right: 0;\n }\n ha-card[hover] {\n cursor: pointer;\n }\n ha-spinner {\n margin: 4px auto;\n }\n .flex {\n display: flex;\n display: -webkit-flex;\n min-width: 0;\n }\n .header {\n justify-content: space-between;\n }\n .header[loc=\"center\"] {\n justify-content: space-around;\n }\n .header[loc=\"left\"] {\n align-self: flex-start;\n }\n .header[loc=\"right\"] {\n align-self: flex-end;\n }\n .name {\n align-items: center;\n min-width: 0;\n letter-spacing: var(--mcg-title-letter-spacing, normal);\n }\n .name > span {\n font-size: 1.2em;\n font-weight: var(--mcg-title-font-weight, 500);\n max-height: 1.4em;\n min-height: 1.4em;\n opacity: .65;\n }\n .icon {\n color: var(--state-icon-color, #44739e);\n display: inline-block;\n flex: 0 0 1.7em;\n text-align: center;\n }\n .icon > ha-icon {\n height: 1.7em;\n width: 1.7em;\n }\n .icon[loc=\"left\"] {\n order: -1;\n margin-right: .6em;\n margin-left: 0;\n }\n .icon[loc=\"state\"] {\n align-self: center;\n }\n .states {\n align-items: flex-start;\n font-weight: 300;\n justify-content: space-between;\n flex-wrap: nowrap;\n }\n .states .icon {\n align-self: center;\n margin-left: 0;\n }\n .states[loc=\"center\"] {\n justify-content: space-evenly;\n }\n .states[loc=\"right\"] > .state {\n margin-left: auto;\n order: 2;\n }\n .states[loc=\"center\"] .states--secondary,\n .states[loc=\"right\"] .states--secondary {\n margin-left: 0;\n }\n .states[loc=\"center\"] .states--secondary {\n align-items: center;\n }\n .states[loc=\"right\"] .states--secondary {\n align-items: flex-start;\n }\n .states[loc=\"center\"] .state__time {\n left: 50%;\n transform: translateX(-50%);\n }\n .states > .icon > ha-icon {\n height: 2em !important;\n width: 2em !important;\n }\n .states--secondary {\n display: flex;\n flex-flow: column;\n flex-wrap: wrap;\n align-items: flex-end;\n margin-left: 1rem;\n min-width: 0;\n margin-left: 1.4em;\n }\n .states--secondary:empty {\n display: none;\n }\n .state {\n position: relative;\n display: flex;\n flex-wrap: nowrap;\n max-width: 100%;\n min-width: 0;\n }\n .state > svg {\n align-self: center;\n border-radius: 100%;\n }\n .state--small {\n font-size: .6em;\n margin-bottom: .6rem;\n flex-wrap: nowrap;\n }\n .state--small > svg {\n position: absolute;\n left: -1.6em;\n align-self: center;\n height: 1em;\n width: 1em;\n border-radius: 100%;\n margin-right: 1em;\n }\n .state--small:last-child {\n margin-bottom: 0;\n }\n .states--secondary > :only-child {\n font-size: 1em;\n margin-bottom: 0;\n }\n .states--secondary > :only-child svg {\n display: none;\n }\n .state__value {\n display: inline-block;\n font-size: 2.4em;\n margin-right: .25rem;\n line-height: 1.2em;\n }\n .state__uom {\n flex: 1;\n align-self: flex-end;\n display: inline-block;\n font-size: 1.4em;\n font-weight: 400;\n line-height: 1.6em;\n margin-top: .1em;\n opacity: .6;\n vertical-align: bottom;\n }\n .state--small .state__uom {\n flex: 1;\n }\n .state__time {\n font-size: .95rem;\n font-weight: 500;\n bottom: -1.1rem;\n left: 0;\n opacity: .75;\n position: absolute;\n white-space: nowrap;\n animation: fade .15s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .states[loc=\"right\"] .state__time {\n left: initial;\n right: 0;\n }\n .graph {\n align-self: flex-end;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n margin-top: auto;\n width: 100%;\n }\n .graph__container {\n display: flex;\n flex-direction: row;\n position: relative;\n }\n .graph__container__svg {\n cursor: default;\n flex: 1;\n }\n svg {\n overflow: hidden;\n display: block;\n }\n path {\n stroke-linecap: round;\n stroke-linejoin: round;\n }\n .fill[anim=\"false\"] {\n animation: reveal .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"false\"][type=\"fade\"] {\n animation: reveal-2 .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line--points[anim=\"false\"],\n .line[anim=\"false\"] {\n animation: pop .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line--points[inactive],\n .line--rect[inactive],\n .fill--rect[inactive] {\n opacity: 0 !important;\n animation: none !important;\n transition: all .15s !important;\n }\n .line--points[tooltip] .line--point[inactive] {\n opacity: 0;\n }\n .line--point {\n cursor: pointer;\n fill: var(--primary-background-color, white);\n stroke-width: inherit;\n }\n .line--point:hover {\n fill: var(--mcg-hover, inherit) !important;\n }\n .bars {\n animation: pop .25s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bars[anim] {\n animation: bars .5s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bar {\n transition: opacity .25s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bar:hover {\n opacity: .5;\n cursor: pointer;\n }\n ha-card[gradient] .line--point:hover {\n fill: var(--primary-text-color, white);\n }\n path,\n .line--points,\n .fill {\n opacity: 0;\n }\n .line--points[anim=\"true\"][init] {\n animation: pop .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"true\"][init] {\n animation: reveal .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"true\"][init][type=\"fade\"] {\n animation: reveal-2 .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line[anim=\"true\"][init] {\n animation: dash 1s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .graph__labels.--secondary {\n right: 0;\n margin-right: 0px;\n align-items: flex-end;\n }\n .graph__labels {\n align-items: flex-start;\n flex-direction: column;\n font-size: calc(.15em + 8.5px);\n font-weight: 400;\n justify-content: space-between;\n margin-right: 10px;\n padding: .6em;\n position: absolute;\n pointer-events: none;\n top: 0; bottom: 0;\n opacity: .75;\n }\n .graph__labels > span {\n cursor: pointer;\n background: var(--primary-background-color, white);\n border-radius: 1em;\n padding: .2em .6em;\n box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24);\n }\n .graph__legend {\n display: flex;\n flex-direction: row;\n justify-content: space-evenly;\n padding-top: 16px;\n flex-wrap: wrap;\n }\n .graph__legend__item {\n cursor: pointer;\n display: flex;\n min-width: 0;\n margin: .4em;\n align-items: center\n }\n .graph__legend__item span {\n opacity: .75;\n margin-left: .4em;\n }\n .graph__legend__item svg {\n border-radius: 100%;\n min-width: 10px;\n }\n .info {\n justify-content: space-between;\n align-items: middle;\n }\n .info__item {\n display: flex;\n flex-flow: column;\n text-align: center;\n }\n .info__item:last-child {\n align-items: flex-end;\n text-align: right;\n }\n .info__item:first-child {\n align-items: flex-start;\n text-align: left;\n }\n .info__item__type {\n text-transform: capitalize;\n font-weight: 500;\n opacity: .9;\n }\n .info__item__time,\n .info__item__value {\n opacity: .75;\n }\n .ellipsis {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n @keyframes fade {\n 0% { opacity: 0; }\n }\n @keyframes reveal {\n 0% { opacity: 0; }\n 100% { opacity: .15; }\n }\n @keyframes reveal-2 {\n 0% { opacity: 0; }\n 100% { opacity: .4; }\n }\n @keyframes pop {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n }\n @keyframes bars {\n 0% { opacity: 0; }\n 50% { opacity: 0; }\n 100% { opacity: 1; }\n }\n @keyframes dash {\n 0% {\n opacity: 0;\n }\n 25% {\n opacity: 1;\n }\n 100% {\n opacity: 1;\n stroke-dashoffset: 0;\n }\n }"]);return R=function(){return a},a}function S(a,b){return b||(b=a.slice(0)),Object.freeze(Object.defineProperties(a,{raw:{value:Object.freeze(b)}}))}function T(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){return void c(a)}h.done?b(i):Promise.resolve(i).then(d,e)}function U(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){function f(a){T(h,d,e,f,g,"next",a)}function g(a){T(h,d,e,f,g,"throw",a)}var h=a.apply(b,c);f(void 0)})}}function W(a){return $(a)||Z(a)||Y(a)||X()}function X(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function Y(a,b){if(a){if("string"==typeof a)return _(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);return"Object"===c&&a.constructor&&(c=a.constructor.name),"Map"===c||"Set"===c?Array.from(a):"Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c)?_(a,b):void 0}}function Z(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function $(a){if(Array.isArray(a))return _(a)}function _(a,b){(null==b||b>a.length)&&(b=a.length);for(var c=0,d=Array(b);ca.parentNode.removeChild(a))}function S(a,b){var c=2=b.oldVersion&&c.createObjectStore("local-forage-detect-blob-support")}catch(c){if("ConstraintError"===c.name)console.warn("The database \""+a.name+"\" has been upgraded from version "+b.oldVersion+" to version "+b.newVersion+", but the storage \""+a.storeName+"\" already exists.");else throw c}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){c(f.result),ga(a)}})}function ja(a){return ia(a,!1)}function ka(a){return ia(a,!0)}function la(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.versiona.db.version;if(d&&(a.version!==b&&console.warn("The database \""+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function ma(a){return new Vc(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function na(a){var b=ca(atob(a.data));return Z([b],{type:a.type})}function oa(a){return a&&a.__local_forage_encoded_blob}function pa(a){var b=this,c=b._initReady().then(function(){var a=Wc[b._dbInfo.name];if(a&&a.dbReady)return a.dbReady});return _(c,a,a),c}function qa(a){fa(a);for(var b,c=Wc[a.name],d=c.forages,e=0;e(a.db=b,la(a)?ka(a):b)).then(b=>{a.db=c.db=b;for(var e=0;e{throw ha(a,b),b})}function ra(a,b,c,d){d===void 0&&(d=1);try{var e=a.db.transaction(a.storeName,b);c(null,e)}catch(e){if(0{if(!a.db||"NotFoundError"===e.name&&!a.db.objectStoreNames.contains(a.storeName)&&a.version<=a.db.version)return a.db&&(a.version=a.db.version+1),ka(a)}).then(()=>qa(a).then(function(){ra(a,b,c,d-1)})).catch(c);c(e)}}function sa(){return{forages:[],db:null,dbReady:null,deferredOperations:[]}}function ta(a){var b,c,d,e,f,g=.75*a.length,h=a.length,j=0;"="===a[a.length-1]&&(g--,"="===a[a.length-2]&&g--);var k=new ArrayBuffer(g),l=new Uint8Array(k);for(b=0;b>4,l[j++]=(15&d)<<4|e>>2,l[j++]=(3&e)<<6|63&f;return k}function ua(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(3&c[b])<<4|c[b+1]>>4],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(15&c[b+1])<<2|c[b+2]>>6],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[63&c[b+2]];return 2==c.length%3?d=d.substring(0,d.length-1)+"=":1==c.length%3&&(d=d.substring(0,d.length-2)+"=="),d}function va(a,b,c,d){a.executeSql("CREATE TABLE IF NOT EXISTS ".concat(b.storeName," ")+"(id INTEGER PRIMARY KEY, key unique, value)",[],c,d)}function wa(a,b,c,d,e,f){a.executeSql(c,d,e,function(a,g){g.code===g.SYNTAX_ERR?a.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[b.storeName],function(a,h){h.rows.length?f(a,g):va(a,b,function(){a.executeSql(c,d,e,f)},f)},f):f(a,g)},f)}function xa(a,b,c,d){var e=this;a=aa(a);var f=new Vc(function(f,g){e.ready().then(function(){void 0===b&&(b=null);var h=b,i=e._dbInfo;i.serializer.serialize(b,function(b,j){j?g(j):i.db.transaction(function(c){wa(c,i,"INSERT OR REPLACE INTO ".concat(i.storeName," ")+"(key, value) VALUES (?, ?)",[a,b],function(){f(h)},function(a,b){g(b)})},function(b){if(b.code===b.QUOTA_ERR){if(0 '__WebKitDatabaseInfoTable__'",[],function(c,d){for(var e=[],f=0;f>8|240&b>>4,15&b>>4|240&b,(15&b)<<4|15&b,1):8===c?Qa(255&b>>24,255&b>>16,255&b>>8,(255&b)/255):4===c?Qa(15&b>>12|240&b>>8,15&b>>8|240&b>>4,15&b>>4|240&b,((15&b)<<4|15&b)/255):null):(b=ae.exec(a))?new Ta(b[1],b[2],b[3],1):(b=be.exec(a))?new Ta(255*b[1]/100,255*b[2]/100,255*b[3]/100,1):(b=ce.exec(a))?Qa(b[1],b[2],b[3],b[4]):(b=de.exec(a))?Qa(255*b[1]/100,255*b[2]/100,255*b[3]/100,b[4]):(b=ee.exec(a))?Za(b[1],b[2]/100,b[3]/100,1):(b=fe.exec(a))?Za(b[1],b[2]/100,b[3]/100,b[4]):ge.hasOwnProperty(a)?Pa(ge[a]):"transparent"===a?new Ta(NaN,NaN,NaN,0):null}function Pa(a){return new Ta(255&a>>16,255&a>>8,255&a,1)}function Qa(c,d,e,f){return 0>=f&&(c=d=e=NaN),new Ta(c,d,e,f)}function Ra(a){return(a instanceof La||(a=Oa(a)),!a)?new Ta:(a=a.rgb(),new Ta(a.r,a.g,a.b,a.opacity))}function Sa(a,c,d,b){return 1===arguments.length?Ra(a):new Ta(a,c,d,null==b?1:b)}function Ta(a,c,d,b){this.r=+a,this.g=+c,this.b=+d,this.opacity=+b}function Ua(){return"#".concat(Ya(this.r)).concat(Ya(this.g)).concat(Ya(this.b))}function Va(){var b=Wa(this.opacity);return"".concat(1===b?"rgb(":"rgba(").concat(Xa(this.r),", ").concat(Xa(this.g),", ").concat(Xa(this.b)).concat(1===b?")":", ".concat(b,")"))}function Wa(a){return isNaN(a)?1:Gb(0,Hb(1,a))}function Xa(a){return Gb(0,Hb(255,Fb(a)||0))}function Ya(a){return a=Xa(a),(16>a?"0":"")+a.toString(16)}function Za(b,c,d,e){return 0>=e?b=c=d=NaN:0>=d||1<=d?b=c=NaN:0>=c&&(b=NaN),new ab(b,c,d,e)}function $a(a){if(a instanceof ab)return new ab(a.h,a.s,a.l,a.opacity);if(a instanceof La||(a=Oa(a)),!a)return new ab;if(a instanceof ab)return a;a=a.rgb();var c=a.r/255,d=a.g/255,e=a.b/255,b=Hb(c,d,e),f=Gb(c,d,e),g=NaN,i=f-b,j=(f+b)/2;return i?(g=c===f?(d-e)/i+6*(dj?f+b:2-f-b,g*=60):i=0j?0:g,new ab(g,i,j,a.opacity)}function ab(a,b,c,d){this.h=+a,this.s=+b,this.l=+c,this.opacity=+d}function bb(a){return a=(a||0)%360,0>a?a+360:a}function cb(a){return Gb(0,Hb(1,a||0))}function db(a,b,c){return 255*(60>a?b+(c-b)*a/60:180>a?c:240>a?b+(c-b)*(240-a)/60:b)}function eb(a){if(a instanceof fb)return new fb(a.l,a.a,a.b,a.opacity);if(a instanceof lb)return mb(a);a instanceof Ta||(a=Ra(a));var c,d,e=jb(a.r),f=jb(a.g),g=jb(a.b),b=gb((.2225045*e+.7168786*f+.0606169*g)/1);return e===f&&f===g?c=d=b:(c=gb((.4360747*e+.3850649*f+.1430804*g)/ke),d=gb((.0139322*e+.0971045*f+.7141733*g)/le)),new fb(116*b-16,500*(c-b),200*(b-d),a.opacity)}function fb(c,d,a,b){this.l=+c,this.a=+d,this.b=+a,this.opacity=+b}function gb(a){return a>pe?Eb(a,1/3):a/oe+me}function hb(a){return a>ne?a*a*a:oe*(a-me)}function ib(a){return 255*(.0031308>=a?12.92*a:1.055*Eb(a,1/2.4)-.055)}function jb(a){return .04045>=(a/=255)?a/12.92:Eb((a+.055)/1.055,2.4)}function kb(a){if(a instanceof lb)return new lb(a.h,a.c,a.l,a.opacity);if(a instanceof fb||(a=eb(a)),0===a.a&&0===a.b)return new lb(NaN,0a.l?0:NaN,a.l,a.opacity);var b=Db(a.b,a.a)*ie;return new lb(0>b?b+360:b,Cb(a.a*a.a+a.b*a.b),a.l,a.opacity)}function lb(a,b,c,d){this.h=+a,this.c=+b,this.l=+c,this.opacity=+d}function mb(a){if(isNaN(a.h))return new fb(a.l,0,0,a.opacity);var b=a.h*he;return new fb(a.l,Bb(b)*a.c,Ab(b)*a.c,a.opacity)}function nb(a){if(a instanceof ob)return new ob(a.h,a.s,a.l,a.opacity);a instanceof Ta||(a=Ra(a));var c=a.r/255,d=a.g/255,e=a.b/255,b=(xe*e+ve*c-we*d)/(xe+ve-we),f=e-b,g=(ue*(d-b)-se*f)/te,i=Cb(g*g+f*f)/(ue*b*(1-b)),j=i?Db(g,f)*ie-120:NaN;return new ob(0>j?j+360:j,i,b,a.opacity)}function ob(a,b,c,d){this.h=+a,this.s=+b,this.l=+c,this.opacity=+d}function pb(b,a){return function(c){return b+c*a}}function qb(c,d,e){return c=Eb(c,e),d=Eb(d,e)-c,e=1/e,function(a){return Eb(c+a*d,e)}}function rb(c){return 1==(c=+c)?sb:function(d,a){return a-d?qb(d,a,c):ye(isNaN(d)?a:d)}}function sb(c,a){var b=a-c;return b?pb(c,b):ye(isNaN(c)?a:c)}var Ib="undefined"!=typeof window&&null!=window.customElements&&window.customElements.polyfillWrapFlushCallback!==void 0,Jb=function(a,b){for(var c=2"),Nb=new RegExp("".concat(Lb,"|").concat(Mb)),Ob="$lit$";class Pb{constructor(a,b){this.parts=[],this.element=b;for(var c,d=[],e=[],f=document.createTreeWalker(b.content,133,null,!1),g=0,h=-1,j=0,{strings:k,values:{length:l}}=a;j{var c=a.length-b.length;return 0<=c&&a.slice(c)===b},Rb=a=>-1!==a.index,Sb=()=>document.createComment(""),Tb=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/,Ub=a=>{for(var b=11===a.nodeType?0:1,c=document.createTreeWalker(a,133,null,!1);c.nextNode();)b++;return b},Vb=function(a){for(var b,c=1"function"==typeof a&&Wb.has(a),Yb={},Zb={};class $b{constructor(a,b,c){this.__parts=[],this.template=a,this.processor=b,this.options=c}update(a){var b=0;for(var c of this.__parts)void 0!==c&&c.setValue(a[b]),b++;for(var d of this.__parts)void 0!==d&&d.commit()}_clone(){for(var a,b=Ib?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),c=[],d=this.template.parts,e=document.createTreeWalker(b,133,null,!1),f=0,g=0,h=e.nextNode();fa}),ac=" ".concat(Lb," ");class bc{constructor(a,b,c,d){this.strings=a,this.values=b,this.type=c,this.processor=d}getHTML(){for(var a=this.strings.length-1,b="",c=!1,d=0;d",f+1);var g=Tb.exec(e);b+=null===g?e+(c?ac:Mb):e.substr(0,g.index)+g[1]+g[2]+Ob+g[3]+Lb}return b+=this.strings[a],b}getTemplateElement(){var a=document.createElement("template"),b=this.getHTML();return void 0!==_b&&(b=_b.createHTML(b)),a.innerHTML=b,a}}class cc extends bc{getHTML(){return"".concat(super.getHTML(),"")}getTemplateElement(){var a=super.getTemplateElement(),b=a.content,c=b.firstChild;return b.removeChild(c),Jb(b,c.firstChild),a}}var dc=a=>null===a||"object"!=typeof a&&"function"!=typeof a,ec=a=>Array.isArray(a)||!!(a&&a[Symbol.iterator]);class fc{constructor(a,b,c){this.dirty=!0,this.element=a,this.name=b,this.strings=c,this.parts=[];for(var d=0;d{try{var a={get capture(){return lc=!0,!1}};window.addEventListener("test",a,a),window.removeEventListener("test",a,a)}catch(a){}})();class mc{constructor(a,b,c){this.value=void 0,this.__pendingValue=void 0,this.element=a,this.eventName=b,this.eventContext=c,this.__boundHandleEvent=a=>this.handleEvent(a)}setValue(a){this.__pendingValue=a}commit(){for(;Xb(this.__pendingValue);){var d=this.__pendingValue;this.__pendingValue=Yb,d(this)}if(this.__pendingValue!==Yb){var a=this.__pendingValue,b=this.value,c=null==a||null!=b&&(a.capture!==b.capture||a.once!==b.once||a.passive!==b.passive);c&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),null!=a&&(null==b||c)&&(this.__options=nc(a),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=a,this.__pendingValue=Yb}}handleEvent(a){"function"==typeof this.value?this.value.call(this.eventContext||this.element,a):this.value.handleEvent(a)}}var nc=a=>a&&(lc?{capture:a.capture,passive:a.passive,once:a.once}:a.capture),oc=new Map,pc=new WeakMap,qc=(a,b,c)=>{var d=pc.get(b);d===void 0&&(Kb(b,b.firstChild),pc.set(b,d=new hc(Object.assign({templateFactory:T},c))),d.appendInto(b)),d.setValue(a),d.commit()};var rc=new class a{handleAttributeExpressions(a,b,c,d){var e=b[0];if("."===e){var g=new jc(a,b.slice(1),c);return g.parts}if("@"===e)return[new mc(a,b.slice(1),d.eventContext)];if("?"===e)return[new ic(a,b.slice(1),c)];var f=new fc(a,b,c);return f.parts}handleTextExpression(a){return new hc(a)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");var sc=function(a){for(var b=arguments.length,c=Array(1"".concat(a,"--").concat(b),vc=!0;"undefined"==typeof window.ShadyCSS?vc=!1:"undefined"==typeof 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."),vc=!1);var wc,xc=a=>b=>{var c=uc(b.type,a),d=oc.get(c);void 0===d&&(d={stringsArray:new WeakMap,keyString:new Map},oc.set(c,d));var e=d.stringsArray.get(b.strings);if(void 0!==e)return e;var f=b.strings.join(Lb);if(e=d.keyString.get(f),void 0===e){var g=b.getTemplateElement();vc&&window.ShadyCSS.prepareTemplateDom(g,a),e=new Pb(b,g),d.keyString.set(f,e)}return d.stringsArray.set(b.strings,e),e},yc=["html","svg"],zc=a=>{yc.forEach(b=>{var c=oc.get(uc(b,a));c!==void 0&&c.keyString.forEach(a=>{var{element:{content:b}}=a,c=new Set;Array.from(b.querySelectorAll("style")).forEach(a=>{c.add(a)}),i(a,c)})})},Ac=new Set,Bc=(a,b,c)=>{Ac.add(a);var d=!c?document.createElement("template"):c.element,e=b.querySelectorAll("style"),{length:f}=e;if(0===f)return void window.ShadyCSS.prepareTemplateStyles(d,a);for(var g,h=document.createElement("style"),j=0;j{if(!c||"object"!=typeof c||!c.scopeName)throw new Error("The `scopeName` option is required.");var d=c.scopeName,e=pc.has(b),f=vc&&11===b.nodeType&&!!b.host,g=f&&!Ac.has(d),h=g?document.createDocumentFragment():b;if(qc(a,h,Object.assign({templateFactory:xc(d)},c)),g){var i=pc.get(h);pc.delete(h);var j=i.value instanceof $b?i.value.template:void 0;Bc(d,h,j),Kb(b,b.firstChild),b.appendChild(h),pc.set(b,i)}!e&&f&&window.ShadyCSS.styleElement(b.host)};window.JSCompiler_renameProperty=a=>a;var Dc={toAttribute(a,b){return b===Boolean?a?"":null:b===Object||b===Array?null==a?a:JSON.stringify(a):a},fromAttribute(a,b){return b===Boolean?null!==a:b===Number?null===a?null:+a:b===Object||b===Array?JSON.parse(a):a}},Ec=(a,b)=>b!==a&&(b===b||a===a),Fc={attribute:!0,type:String,converter:Dc,reflect:!1,hasChanged:Ec},Gc=1,Hc=4,Ic=8,Jc=16,Kc="finalized";class Lc extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();var a=[];return this._classProperties.forEach((b,c)=>{var d=this._attributeNameForProperty(c,b);void 0!==d&&(this._attributeToPropertyMap.set(d,c),a.push(d))}),a}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;var a=Object.getPrototypeOf(this)._classProperties;a!==void 0&&a.forEach((a,b)=>this._classProperties.set(b,a))}}static createProperty(a){var b=1this._enableUpdatingResolver=a),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach((a,b)=>{if(this.hasOwnProperty(b)){var c=this[b];delete this[b],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(b,c)}})}_applyInstanceProperties(){this._instanceProperties.forEach((a,b)=>this[b]=a),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){this._enableUpdatingResolver!==void 0&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(a,b,c){b!==c&&this._attributeToProperty(a,c)}_propertyToAttribute(a,b){var c=2this._propertyToAttribute(b,this[b],a)),this._reflectingProperties=void 0),this._markUpdated()}updated(){}firstUpdated(){}}wc=Kc,Lc[wc]=!0;var Mc=window.ShadowRoot&&(window.ShadyCSS===void 0||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Nc=Symbol();class Oc{constructor(a,b){if(b!==Nc)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=a}get styleSheet(){return void 0===this._styleSheet&&(Mc?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}var Pc=a=>new Oc(a+"",Nc),Qc=a=>{if(a instanceof Oc)return a.cssText;if("number"==typeof a)return a;throw new Error("Value passed to 'css' function must be a 'css' function result: ".concat(a,". Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security."))};(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");var Rc={};class Sc extends Lc{static getStyles(){return this.styles}static _getUniqueStyles(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_styles",this))){var a=this.getStyles();if(Array.isArray(a)){var b=(a,c)=>a.reduceRight((a,c)=>Array.isArray(c)?b(c,a):(a.add(c),a),c),c=b(a,new Set),d=[];c.forEach(a=>d.unshift(a)),this._styles=d}else this._styles=void 0===a?[]:[a];this._styles=this._styles.map(a=>{if(a instanceof CSSStyleSheet&&!Mc){var b=Array.prototype.slice.call(a.cssRules).reduce((a,b)=>a+b.cssText,"");return Pc(b)}return a})}}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(){var a=this.constructor._styles;0===a.length||(window.ShadyCSS===void 0||window.ShadyCSS.nativeShadow?Mc?this.renderRoot.adoptedStyleSheets=a.map(a=>a instanceof CSSStyleSheet?a:a.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(a.map(a=>a.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&window.ShadyCSS!==void 0&&window.ShadyCSS.styleElement(this)}update(a){var b=this.render();super.update(a),b!==Rc&&this.constructor.render(b,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(a=>{var b=document.createElement("style");b.textContent=a.cssText,this.renderRoot.appendChild(b)}))}render(){return Rc}}Sc.finalized=!0,Sc.render=Cc;var Tc=function(){try{if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof webkitIndexedDB)return webkitIndexedDB;if("undefined"!=typeof mozIndexedDB)return mozIndexedDB;if("undefined"!=typeof OIndexedDB)return OIndexedDB;if("undefined"!=typeof msIndexedDB)return msIndexedDB}catch(a){}}();"undefined"==typeof Promise&&require("lie/polyfill");var Uc,Vc=Promise,Wc={},Xc=Object.prototype.toString,Yc={_driver:"asyncStorage",_initStorage:function(a){function b(){return Vc.resolve()}var c=this,d={db:null};if(a)for(var e in a)d[e]=a[e];var f=Wc[d.name];f||(f=sa(),Wc[d.name]=f),f.forages.push(c),c._initReady||(c._initReady=c.ready,c.ready=pa);for(var g,h=[],i=0;ia?void b(null):void c.ready().then(function(){ra(c._dbInfo,"readonly",function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=!1,i=g.openKeyCursor();i.onsuccess=function(){var c=i.result;return c?void(0===a?b(c.key):h?b(c.key):(h=!0,c.advance(a))):void b(null)},i.onerror=function(){d(i.error)}}catch(a){d(a)}})}).catch(d)});return $(d,b),d},keys:function(a){var b=this,c=new Vc(function(a,c){b.ready().then(function(){ra(b._dbInfo,"readonly",function(d,e){if(d)return c(d);try{var f=e.objectStore(b._dbInfo.storeName),g=f.openKeyCursor(),h=[];g.onsuccess=function(){var b=g.result;return b?void(h.push(b.key),b.continue()):void a(h)},g.onerror=function(){c(g.error)}}catch(a){c(a)}})}).catch(c)});return $(c,a),c},dropInstance:function(a,b){b=ba.apply(this,arguments);var c=this.config();a="function"!=typeof a&&a||{},a.name||(a.name=a.name||c.name,a.storeName=a.storeName||c.storeName);var d,e=this;if(!a.name)d=Vc.reject("Invalid arguments");else{var f=a.name===c.name&&e._dbInfo.db,g=f?Vc.resolve(e._dbInfo.db):ja(a).then(b=>{var c=Wc[a.name],d=c.forages;c.db=b;for(var e=0;e{if(b.objectStoreNames.contains(a.storeName)){var c=b.version+1;fa(a);var d=Wc[a.name],e=d.forages;b.close();for(var g,h=0;h{var e=Tc.open(a.name,c);e.onerror=a=>{var b=e.result;b.close(),d(a)},e.onupgradeneeded=()=>{var b=e.result;b.deleteObjectStore(a.storeName)},e.onsuccess=()=>{var a=e.result;a.close(),b(a)}});return f.then(a=>{d.db=a;for(var b,c=0;c{throw(ha(a,b)||Vc.resolve()).catch(()=>{}),b})}}):g.then(b=>{fa(a);var c=Wc[a.name],d=c.forages;b.close();for(var e,f=0;f{var d=Tc.deleteDatabase(a.name);d.onerror=d.onblocked=a=>{var b=d.result;b&&b.close(),c(a)},d.onsuccess=()=>{var a=d.result;a&&a.close(),b(a)}});return g.then(a=>{c.db=a;for(var b,e=0;e{throw(ha(a,b)||Vc.resolve()).catch(()=>{}),b})})}return $(d,b),d}},Zc=/^~~local_forage_type~([^~]+)~/,$c="__lfsc__:".length,_c=$c+"arbf".length,ad=Object.prototype.toString,bd={serialize:function(a,b){var c="";if(a&&(c=ad.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===ad.call(a.buffer))){var d,e="__lfsc__:";a instanceof ArrayBuffer?(d=a,e+="arbf"):(d=a.buffer,"[object Int8Array]"===c?e+="si08":"[object Uint8Array]"===c?e+="ui08":"[object Uint8ClampedArray]"===c?e+="uic8":"[object Int16Array]"===c?e+="si16":"[object Uint16Array]"===c?e+="ur16":"[object Int32Array]"===c?e+="si32":"[object Uint32Array]"===c?e+="ui32":"[object Float32Array]"===c?e+="fl32":"[object Float64Array]"===c?e+="fl64":b(new Error("Failed to get type for BinaryArray"))),b(e+ua(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c="~~local_forage_type~"+a.type+"~"+ua(this.result);b("__lfsc__:blob"+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(c){console.error("Couldn't convert value into a JSON string: ",a),b(null,c)}},deserialize:function(a){if(a.substring(0,$c)!=="__lfsc__:")return JSON.parse(a);var b,c=a.substring(_c),d=a.substring($c,_c);if(d==="blob"&&Zc.test(c)){var e=c.match(Zc);b=e[1],c=c.substring(e[0].length)}var f=ta(c);switch(d){case"arbf":return f;case"blob":return Z([f],{type:b});case"si08":return new Int8Array(f);case"ui08":return new Uint8Array(f);case"uic8":return new Uint8ClampedArray(f);case"si16":return new Int16Array(f);case"ur16":return new Uint16Array(f);case"si32":return new Int32Array(f);case"ui32":return new Uint32Array(f);case"fl32":return new Float32Array(f);case"fl64":return new Float64Array(f);default:throw new Error("Unkown type: "+d);}},stringToBuffer:ta,bufferToString:ua},cd={_driver:"webSQLStorage",_initStorage:function(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"==typeof a[d]?a[d]:a[d].toString();var e=new Vc(function(a,d){try{c.db=openDatabase(c.name,c.version+"",c.description,c.size)}catch(a){return d(a)}c.db.transaction(function(e){va(e,c,function(){b._dbInfo=c,a()},function(a,b){d(b)})},d)});return c.serializer=bd,e},_support:function(){return"function"==typeof openDatabase}(),iterate:function(a,b){var c=this,d=new Vc(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){wa(c,e,"SELECT * FROM ".concat(e.storeName),[],function(c,d){for(var f=d.rows,g=f.length,h=0;ha===b||"number"==typeof a&&"number"==typeof b&&isNaN(a)&&isNaN(b),fd=(a,b)=>{for(var c=a.length,d=0;d{})}config(a){if("object"==typeof a){if(this._ready)return new Error("Can't call config() after localforage has been used.");for(var b in a){if("storeName"==b&&(a[b]=a[b].replace(/\W/g,"_")),"version"==b&&"number"!=typeof a[b])return new Error("Database version must be a number.");this._config[b]=a[b]}return!("driver"in a&&a.driver)||this.setDriver(this._config.driver)}return"string"==typeof a?this._config[a]:this._config}defineDriver(a,b,c){var d=new Vc(function(b,c){try{var d=a._driver,e=new Error("Custom driver not compliant; see https://mozilla.github.io/localForage/#definedriver");if(!a._driver)return void c(e);for(var f=md.concat("_initStorage"),g=0,h=f.length;g(null===b._ready&&(b._ready=b._initDriver()),b._ready));return _(c,a,a),c}setDriver(a,b,c){function d(){g._config.driver=g.driver()}function e(a){return g._extend(a),d(),g._ready=g._initStorage(g._config),g._ready}function f(a){return function(){function b(){for(;cVc.resolve());return this._driverSet=i.then(()=>{var a=h[0];return g._dbInfo=null,g._ready=null,g.getDriver(a).then(a=>{g._driver=a._driver,d(),g._wrapLibraryMethodsWithReady(),g._initDriver=f(h)})}).catch(()=>{d();var a=new Error("No available storage method found.");return g._driverSet=Vc.reject(a),g._driverSet}),_(this._driverSet,b,c),this._driverSet}supports(a){return!!id[a]}_extend(a){Ea(this,a)}_getSupportedDrivers(a){for(var b,c=[],d=0,e=a.length;da.getHours()?b.amPm[0]:b.amPm[1]},A:function(a,b){return 12>a.getHours()?b.amPm[0].toUpperCase():b.amPm[1].toUpperCase()},ZZ:function(a){var b=a.getTimezoneOffset();return(0=a?"hass:battery-alert":"hass:battery-"+a}var e=b.attributes.unit_of_measurement;return"\xB0C"===e||"\xB0F"===e?"hass:thermometer":Ja("sensor")},input_datetime:function(a){return a.attributes.has_date?a.attributes.has_time?Ja("input_datetime"):"hass:calendar":"hass:clock"}},Vd=function(a){if(!a)return"hass:bookmark";if(a.attributes.icon)return a.attributes.icon;var b=Ia(a.entity_id);return b in Ud?Ud[b](a):Ja(b,a.state)},Q=function(a){var b={exports:{}};return a(b,b.exports),b.exports}(function(a){(function(b){a.exports=b()})(function(a){function b(e,f){var g=e[0],h=e[1],i=e[2],j=e[3];g+=0|(h&i|~h&j)+f[0]-680876936,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[1]-389564586,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[2]+606105819,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[3]-1044525330,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[4]-176418897,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[5]+1200080426,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[6]-1473231341,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[7]-45705983,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[8]+1770035416,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[9]-1958414417,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[10]-42063,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[11]-1990404162,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[12]+1804603682,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[13]-40341101,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[14]-1502002290,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[15]+1236535329,h=0|(h<<22|h>>>10)+i,g+=0|(h&j|i&~j)+f[1]-165796510,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[6]-1069501632,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[11]+643717713,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[0]-373897302,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[5]-701558691,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[10]+38016083,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[15]-660478335,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[4]-405537848,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[9]+568446438,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[14]-1019803690,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[3]-187363961,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[8]+1163531501,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[13]-1444681467,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[2]-51403784,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[7]+1735328473,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[12]-1926607734,h=0|(h<<20|h>>>12)+i,g+=0|(h^i^j)+f[5]-378558,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[8]-2022574463,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[11]+1839030562,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[14]-35309556,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[1]-1530992060,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[4]+1272893353,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[7]-155497632,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[10]-1094730640,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[13]+681279174,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[0]-358537222,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[3]-722521979,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[6]+76029189,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[9]-640364487,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[12]-421815835,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[15]+530742520,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[2]-995338651,h=0|(h<<23|h>>>9)+i,g+=0|(i^(h|~j))+f[0]-198630844,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[7]+1126891415,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[14]-1416354905,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[5]-57434055,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[12]+1700485571,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[3]-1894986606,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[10]-1051523,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[1]-2054922799,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[8]+1873313359,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[15]-30611744,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[6]-1560198380,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[13]+1309151649,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[4]-145523070,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[11]-1120210379,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[2]+718787259,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[9]-343485551,h=0|(h<<21|h>>>11)+i,e[0]=0|g+e[0],e[1]=0|h+e[1],e[2]=0|i+e[2],e[3]=0|j+e[3]}function c(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c}function d(b){var a,c=[];for(a=0;64>a;a+=4)c[a>>2]=b[a]+(b[a+1]<<8)+(b[a+2]<<16)+(b[a+3]<<24);return c}function e(a){var d,e,f,g,h,j,k=a.length,l=[1732584193,-271733879,-1732584194,271733878];for(d=64;d<=k;d+=64)b(l,c(a.substring(d-64,d)));for(a=a.substring(d-64),e=a.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],d=0;d>2]|=a.charCodeAt(d)<<(d%4<<3);if(f[d>>2]|=128<<(d%4<<3),55d;d+=1)f[d]=0;return g=8*k,g=g.toString(16).match(/(.*?)(.{0,8})$/),h=parseInt(g[2],16),j=parseInt(g[1],16)||0,f[14]=h,f[15]=j,b(l,f),l}function f(c){var e,f,g,h,j,k,l=c.length,m=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=l;e+=64)b(m,d(c.subarray(e-64,e)));for(c=e-64>2]|=c[e]<<(e%4<<3);if(g[e>>2]|=128<<(e%4<<3),55e;e+=1)g[e]=0;return h=8*l,h=h.toString(16).match(/(.*?)(.{0,8})$/),j=parseInt(h[2],16),k=parseInt(h[1],16)||0,g[14]=j,g[15]=k,b(m,g),m}function g(a){var b,c="";for(b=0;4>b;b+=1)c+=o[15&a>>8*b+4]+o[15&a>>8*b];return c}function h(a){var b;for(b=0;ba?Gb(a+b,0):Hb(a,b)}ArrayBuffer.prototype.slice=function(c,d){var e,f,g,h,i=this.byteLength,j=b(c,i),k=i;return(d!==a&&(k=b(d,i)),j>k)?new ArrayBuffer(0):(e=k-j,f=new ArrayBuffer(e),g=new Uint8Array(f),h=new Uint8Array(this,j,e),g.set(h),f)}}(),n.prototype.append=function(a){return this.appendBinary(i(a)),this},n.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var d,e=this._buff.length;for(d=64;d<=e;d+=64)b(this._hash,c(this._buff.substring(d-64,d)));return this._buff=this._buff.substring(d-64),this},n.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;b>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=h(this._hash),a&&(c=m(c)),this.reset(),c},n.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},n.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}},n.prototype.setState=function(a){return this._buff=a.buff,this._length=a.length,this._hash=a.hash,this},n.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},n.prototype._finish=function(a,c){var d,e,f,g=c;if(a[g>>2]|=128<<(g%4<<3),55g;g+=1)a[g]=0;d=8*this._length,d=d.toString(16).match(/(.*?)(.{0,8})$/),e=parseInt(d[2],16),f=parseInt(d[1],16)||0,a[14]=e,a[15]=f,b(this._hash,a)},n.hash=function(a,b){return n.hashBinary(i(a),b)},n.hashBinary=function(a,b){var c=e(a),d=h(c);return b?m(d):d},n.ArrayBuffer=function(){this.reset()},n.ArrayBuffer.prototype.append=function(a){var c,e=l(this._buff.buffer,a,!0),f=e.length;for(this._length+=a.byteLength,c=64;c<=f;c+=64)b(this._hash,d(e.subarray(c-64,c)));return this._buff=c-64>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=h(this._hash),a&&(c=m(c)),this.reset(),c},n.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},n.ArrayBuffer.prototype.getState=function(){var a=n.prototype.getState.call(this);return a.buff=k(a.buff),a},n.ArrayBuffer.prototype.setState=function(a){return a.buff=j(a.buff,!0),n.prototype.setState.call(this,a)},n.ArrayBuffer.prototype.destroy=n.prototype.destroy,n.ArrayBuffer.prototype._finish=n.prototype._finish,n.ArrayBuffer.hash=function(a,b){var c=f(new Uint8Array(a)),d=h(c);return b?m(d):d},n})}),Wd=.7,Xd=1/Wd,Yd="\\s*([+-]?\\d+)\\s*",Zd="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",$d="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",_d=/^#([0-9a-f]{3,8})$/,ae=new RegExp("^rgb\\(".concat(Yd,",").concat(Yd,",").concat(Yd,"\\)$")),be=new RegExp("^rgb\\(".concat($d,",").concat($d,",").concat($d,"\\)$")),ce=new RegExp("^rgba\\(".concat(Yd,",").concat(Yd,",").concat(Yd,",").concat(Zd,"\\)$")),de=new RegExp("^rgba\\(".concat($d,",").concat($d,",").concat($d,",").concat(Zd,"\\)$")),ee=new RegExp("^hsl\\(".concat(Zd,",").concat($d,",").concat($d,"\\)$")),fe=new RegExp("^hsla\\(".concat(Zd,",").concat($d,",").concat($d,",").concat(Zd,"\\)$")),ge={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};O(La,Oa,{copy(a){return Object.assign(new this.constructor,this,a)},displayable(){return this.rgb().displayable()},hex:Ma,formatHex:Ma,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return $a(this).formatHsl()},formatRgb:Na,toString:Na}),O(Ta,Sa,Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new Ta(this.r*a,this.g*a,this.b*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new Ta(this.r*a,this.g*a,this.b*a,this.opacity)},rgb(){return this},clamp(){return new Ta(Xa(this.r),Xa(this.g),Xa(this.b),Wa(this.opacity))},displayable(){return-.5<=this.r&&255.5>this.r&&-.5<=this.g&&255.5>this.g&&-.5<=this.b&&255.5>this.b&&0<=this.opacity&&1>=this.opacity},hex:Ua,formatHex:Ua,formatHex8:function(){return"#".concat(Ya(this.r)).concat(Ya(this.g)).concat(Ya(this.b)).concat(Ya(255*(isNaN(this.opacity)?1:this.opacity)))},formatRgb:Va,toString:Va})),O(ab,function(a,b,c,d){return 1===arguments.length?$a(a):new ab(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new ab(this.h,this.s,this.l*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new ab(this.h,this.s,this.l*a,this.opacity)},rgb(){var a=this.h%360+360*(0>this.h),b=isNaN(a)||isNaN(this.s)?0:this.s,c=this.l,d=c+(.5>c?c:1-c)*b,e=2*c-d;return new Ta(db(240<=a?a-240:a+120,e,d),db(a,e,d),db(120>a?a+240:a-120,e,d),this.opacity)},clamp(){return new ab(bb(this.h),cb(this.s),cb(this.l),Wa(this.opacity))},displayable(){return(0<=this.s&&1>=this.s||isNaN(this.s))&&0<=this.l&&1>=this.l&&0<=this.opacity&&1>=this.opacity},formatHsl(){var b=Wa(this.opacity);return"".concat(1===b?"hsl(":"hsla(").concat(bb(this.h),", ").concat(100*cb(this.s),"%, ").concat(100*cb(this.l),"%").concat(1===b?")":", ".concat(b,")"))}}));var he=wb/180,ie=180/wb,je=18,ke=.96422,le=.82521,me=4/29,ne=6/29,oe=3*ne*ne,pe=ne*ne*ne;O(fb,function(c,d,a,b){return 1===arguments.length?eb(c):new fb(c,d,a,null==b?1:b)},Ka(La,{brighter(a){return new fb(this.l+je*(null==a?1:a),this.a,this.b,this.opacity)},darker(a){return new fb(this.l-je*(null==a?1:a),this.a,this.b,this.opacity)},rgb(){var a=(this.l+16)/116,b=isNaN(this.a)?a:a+this.a/500,c=isNaN(this.b)?a:a-this.b/200;return b=ke*hb(b),a=1*hb(a),c=le*hb(c),new Ta(ib(3.1338561*b-1.6168667*a-.4906146*c),ib(-.9787684*b+1.9161415*a+.033454*c),ib(.0719453*b-.2289914*a+1.4052427*c),this.opacity)}})),O(lb,function(a,b,c,d){return 1===arguments.length?kb(a):new lb(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return new lb(this.h,this.c,this.l+je*(null==a?1:a),this.opacity)},darker(a){return new lb(this.h,this.c,this.l-je*(null==a?1:a),this.opacity)},rgb(){return mb(this).rgb()}}));var qe=-.14861,re=+1.78277,se=-.29227,te=-.90649,ue=+1.97294,ve=ue*te,we=ue*re,xe=re*se-te*qe;O(ob,function(a,b,c,d){return 1===arguments.length?nb(a):new ob(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new ob(this.h,this.s,this.l*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new ob(this.h,this.s,this.l*a,this.opacity)},rgb(){var b=isNaN(this.h)?0:(this.h+120)*he,c=+this.l,d=isNaN(this.s)?0:this.s*c*(1-c),a=Bb(b),e=Ab(b);return new Ta(255*(c+d*(qe*a+re*e)),255*(c+d*(se*a+te*e)),255*(c+d*(ue*a)),this.opacity)}}));var ye=a=>()=>a,ze=function a(b){function c(a,c){var e=d((a=Sa(a)).r,(c=Sa(c)).r),f=d(a.g,c.g),g=d(a.b,c.b),b=sb(a.opacity,c.opacity);return function(c){return a.r=e(c),a.g=f(c),a.b=g(c),a.opacity=b(c),a+""}}var d=rb(b);return c.gamma=a,c}(1),Ae="https://github.com/kalkih/mini-graph-card/blob/master/README.md",Be=14,Ce=96,De={humidity:"hass:water-percent",illuminance:"hass:brightness-5",temperature:"hass:thermometer",battery:"hass:battery",pressure:"hass:gauge",power:"hass:flash",signal_strength:"hass:wifi",motion:"hass:walk",door:"hass:door-closed",window:"hass:window-closed",presence:"hass:account",light:"hass:lightbulb"},Ee=["var(--accent-color)","#3498db","#e74c3c","#9b59b6","#f1c40f","#2ecc71","#1abc9c","#34495e","#e67e22","#7f8c8d","#27ae60","#2980b9","#8e44ad"],Fe=["entity","line","length","fill","points","tooltip","abs","config"],Ge={name:!0,icon:!0,state:!0,graph:"line",labels:"hover",labels_secondary:"hover",extrema:!1,legend:!0,fill:!0,points:"hover"},He=0,X=1,Y=2,V=3600000;class Ie{constructor(a,b,c){var d=3this._reducer(a,b),[]),c=vb(this.hours*this.points);b.length=c,this.coords=this._calcPoints(b),this.min=Hb.apply(Math,W(this.coords.map(a=>+a[Y]))),this.max=Gb.apply(Math,W(this.coords.map(a=>+a[Y])))}}_reducer(a,b){var c=this._endTime-new Date(b.last_changed).getTime(),d=c/V*this.points-this.hours*this.points;if(0>d){var e=yb(zb(d));a[e]||(a[e]=[]),a[e].push(b)}else a[0]=[b];return a}_calcPoints(a){var b=this.width/(this.hours*this.points-1);b=Number.isFinite(b)?b:this.width;for(var c,d=[],e=a.filter(Boolean)[0],f=0;f{var b=this._logarithmic?ub(Gb(1,a[Y])):a[Y],e=this.height-(b-c)/d+2*this.margin[X];return[a[He],e,a[Y]]});return e}getPoints(){var{coords:a}=this;if(1===a.length&&(a[1]=[this.width+this.margin[He],0,a[0][Y]]),a=this._calcY(this.coords),this._smoothing){var b=a[0];return a.shift(),a.map((a,c)=>{var d=this._midPoint(b[He],b[X],a[He],a[X]),e=(b[Y]+a[Y])/2;return b=a,[d[He],d[X],e,c+1]})}return a.map((a,b)=>[a[He],a[X],a[Y],b])}getPath(){var{coords:a}=this;1===a.length&&(a[1]=[this.width+this.margin[He],0,a[0][Y]]),a=this._calcY(this.coords);var b,c,d="",e=a[0];return d+="M".concat(e[He],",").concat(e[X]),a.forEach(a=>{b=a,c=this._smoothing?this._midPoint(e[He],e[X],b[He],b[X]):b,d+=" ".concat(c[He],",").concat(c[X]),d+=" Q ".concat(b[He],",").concat(b[X]),e=b}),d+=" ".concat(b[He],",").concat(b[X]),d}computeGradient(a,b){var c=b?ub(Gb(1,this._max))-ub(Gb(1,this._min)):this._max-this._min;return a.map((a,d,e)=>{var f;if(a.value>this._max&&e[d+1]){var h=(this._max-e[d+1].value)/(a.value-e[d+1].value);f=ze(e[d+1].color,a.color)(h)}else if(a.value=c?0:b?(ub(Gb(1,this._max))-ub(Gb(1,a.value)))*(100/c):(this._max-a.value)*(100/c),{color:f||a.color,offset:g}})}getFill(a){var b=this.height+4*this.margin[X],c=a;return c+=" L ".concat(this.width-2*this.margin[He],", ").concat(b),c+=" L ".concat(this.coords[0][He],", ").concat(b," z"),c}getBars(a,b){var c=2({x:e*f*b+e*a+c,y:d[X],height:this.height-d[X]+4*this.margin[X],width:e-c,value:d[Y]}))}_midPoint(a,b,c,d){return[(a-c)/2+c,(b-d)/2+d]}_average(a){return a.reduce((a,b)=>a+parseFloat(b.state),0)/a.length}_median(a){var b=W(a).sort((c,a)=>parseFloat(c)-parseFloat(a)),c=yb((b.length-1)/2);return 1==b.length%2?parseFloat(b[c].state):(parseFloat(b[c].state)+parseFloat(b[c+1].state))/2}_maximum(a){return Gb.apply(Math,W(a.map(a=>a.state)))}_minimum(a){return Hb.apply(Math,W(a.map(a=>a.state)))}_first(a){return parseFloat(a[0].state)}_last(a){return parseFloat(a[a.length-1].state)}_sum(a){return a.reduce((a,b)=>a+parseFloat(b.state),0)}_delta(a){return this._maximum(a)-this._minimum(a)}_diff(a){return this._last(a)-this._first(a)}_lastValue(a){return["delta","diff"].includes(this.aggregateFuncName)?0:parseFloat(a[a.length-1].state)||0}_updateEndTime(){switch(this._endTime=new Date,this._groupBy){case"month":this._endTime.setMonth(this._endTime.getMonth()+1),this._endTime.setDate(1);break;case"date":this._endTime.setDate(this._endTime.getDate()+1),this._endTime.setHours(0,0,0,0);break;case"hour":this._endTime.setHours(this._endTime.getHours()+1),this._endTime.setMinutes(0,0,0);}}}var Je=function css(a){for(var b=arguments.length,c=Array(1b+Qc(c)+a[d+1],a[0]);return new Oc(e,Nc)}(R()),Ke=(a,b,c,d,f)=>{var g;switch(d.action){case"more-info":{g=new Event("hass-more-info",{composed:!0}),g.detail={entityId:f},a.dispatchEvent(g);break}case"navigate":{if(!d.navigation_path)return;window.history.pushState(null,"",d.navigation_path),g=new Event("location-changed",{composed:!0}),g.detail={replace:!1},window.dispatchEvent(g);break}case"call-service":{if(!d.service)return;var[h,i]=d.service.split(".",2),j=P({},d.service_data);b.callService(h,i,j);break}case"url":{if(!d.url)return;window.location.href=d.url;break}case"fire-dom-event":{g=new Event("ll-custom",{composed:!0,bubbles:!0}),g.detail=d,a.dispatchEvent(g);break}}},Le=xb,Me=a=>Ne(a,16,function(b){return Le(b)}),Ne=(a,b,c)=>{if(null==a)return"";var d,e,f,g={},h={},j="",k="",l="",m=2,n=3,o=2,p=[],q=0,r=0;for(f=0;fl.charCodeAt(0)){for(d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}else{for(e=1,d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}m--,0==m&&(m=Eb(2,o),o++),delete h[l]}else for(e=g[l],d=0;d>=1;m--,0==m&&(m=Eb(2,o),o++),g[k]=n++,l=j+""}if(""!==l){if(Object.prototype.hasOwnProperty.call(h,l)){if(256>l.charCodeAt(0)){for(d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}else{for(e=1,d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}m--,0==m&&(m=Eb(2,o),o++),delete h[l]}else for(e=g[l],d=0;d>=1;m--,0==m&&(m=Eb(2,o),o++)}for(e=2,d=0;d>=1;for(;;)if(q<<=1,r==b-1){p.push(c(q));break}else r++;return p.join("")},Oe=a=>null==a?"":""==a?null:Pe(a.length,32768,function(b){return a.charCodeAt(b)}),Pe=(a,b,d)=>{var e,f,g,h,j,k,l,m=[],n=4,o=4,p=3,q="",r=[],s={val:d(0),position:b,index:1};for(e=0;3>e;e+=1)m[e]=e;for(g=0,j=Eb(2,2),k=1;k!=j;)h=s.val&s.position,s.position>>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0a)return"";for(g=0,j=Eb(2,p),k=1;k!=j;)h=s.val&s.position,s.position>>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0a.reduce((a,c)=>+c[b]<+a[b]?c:a,a[0]),Re=(a,b)=>a.reduce((a,c)=>a+ +c[b],0)/a.length,Se=(a,b)=>a.reduce((a,c)=>+c[b]>+a[b]?c:a,a[0]),Te=function(a,b){var c=2a*3600*1000,Ve=a=>Me(JSON.stringify(a)),We=a=>"string"==typeof a?JSON.parse(Oe(a)):a,Xe=function(){for(var a=arguments.length,b=Array(a),c=0;c"undefined"!=typeof a)},Ye=(c,a)=>c.length===a.length&&c.every((b,c)=>b===a[c]),Ze=a=>{console.warn("mini-graph-card: ",a)},$e=(a,b)=>{for(var c=b,d=a.length;c{if(!a||!a.length)return a;if(null==a[0].value||null==a[a.length-1].value)throw new Error("The first and last thresholds must have a set \"value\".\n See ".concat(Ae));var b=0,c=null;return a.map((d,e)=>{if(null!=d.value)return b=e,P({},d);null==c?c=$e(a,e):e>c&&(b=c,c=$e(a,e));var f=a[b].value,g=a[c].value,h=(g-f)/(c-b);return{color:"string"==typeof d?d:d.color,value:h*e+f}})},af=(a,b)=>{var c=_e(a);if(c.sort((c,a)=>a.value-c.value),"smooth"===b)return c;var d,e=(d=[]).concat.apply(d,W(c.map((a,b)=>[a,{value:a.value-1e-4,color:c[b+1]?c[b+1].color:a.color}])));return e},bf=a=>{if(!Array.isArray(a.entities))throw new Error("Please provide the \"entities\" option as a list.\n See ".concat(Ae));if(a.line_color_above||a.line_color_below)throw new Error("\"line_color_above/line_color_below\" was removed, please use \"color_thresholds\".\n See ".concat(Ae));var b=P(P({animate:!1,hour24:!1,font_size:Be,font_size_header:14,height:100,hours_to_show:24,points_per_hour:.5,aggregate_func:"avg",group_by:"interval",line_color:[].concat(Ee),color_thresholds:[],color_thresholds_transition:"smooth",line_width:5,bar_spacing:4,compress:!0,smoothing:!0,state_map:[],cache:!0,value_factor:0,tap_action:{action:"more-info"}},JSON.parse(JSON.stringify(a))),{},{show:P(P({},Ge),a.show)});b.entities.forEach((a,c)=>{"string"==typeof a&&(b.entities[c]={entity:a})}),b.state_map.forEach((a,c)=>{"string"==typeof a&&(b.state_map[c]={value:a,label:a}),b.state_map[c].label=b.state_map[c].label||b.state_map[c].value}),"string"==typeof a.line_color&&(b.line_color=[a.line_color].concat(Ee)),b.font_size=a.font_size/100*Be||Be,b.color_thresholds=af(b.color_thresholds,b.color_thresholds_transition);var c=24Ce&&(b.points_per_hour=Ce/(b.hours_to_show*e),Ze("Not enough space, adjusting points_per_hour to ".concat(b.points_per_hour)))}return b},cf="0.13.0";pd.config({name:"mini-graph-card",version:1,storeName:"entity_history_cache",description:"Mini graph card uses caching for the entity history"}),pd.iterate((a,b)=>{var c=b.endsWith("-raw")?a:We(a),d=new Date;d.setHours(d.getHours()-c.hours_to_show),(a.version!==cf||new Date(c.last_fetched){console.warn("Purging has errored: ",a)}),console.info("%c MINI-GRAPH-CARD %c ".concat(cf," "),"color: white; background: coral; font-weight: 700;","color: coral; background: white; font-weight: 700;");class df extends Sc{constructor(){super(),this.id=Math.random().toString(36).substr(2,9),this.config={},this.bound=[0,0],this.boundSecondary=[0,0],this.length=[],this.entity=[],this.line=[],this.bar=[],this.abs=[],this.fill=[],this.points=[],this.gradient=[],this.tooltip={},this.updateQueue=[],this.updating=!1,this.stateChanged=!1,this.initial=!0,this._md5Config=void 0}static get styles(){return Je}set hass(a){this._hass=a;var b=!1,c=[];this.config.entities.forEach((d,e)=>{this.config.entities[e].index=e;var f=a&&a.states[d.entity]||void 0;f&&this.entity[e]!==f&&(this.entity[e]=f,c.push("".concat(f.entity_id,"-").concat(e)),b=!0)}),b&&(this.stateChanged=!0,this.entity=W(this.entity),this.config.update_interval||this.updating?this.updateQueue=[].concat(c,W(this.updateQueue)):setTimeout(()=>{this.updateQueue=[].concat(c,W(this.updateQueue)),this.updateData()},this.initial?0:1e3))}static get properties(){return{id:String,_hass:{},config:{},entity:[],Graph:[],line:[],shadow:[],length:Number,bound:[],boundSecondary:[],abs:[],tooltip:{},updateQueue:[],color:String}}setConfig(a){this.config=bf(a,this.config),this._md5Config=Q.hash(JSON.stringify(this.config));var b=!Ye(this.config.entities||[],a.entities);(!this.Graph||b)&&(this._hass&&(this.hass=this._hass),this.Graph=this.config.entities.map(a=>new Ie(500,this.config.height,[this.config.show.fill?0:this.config.line_width,this.config.line_width],this.config.hours_to_show,this.config.points_per_hour,a.aggregate_func||this.config.aggregate_func,this.config.group_by,Xe(a.smoothing,this.config.smoothing,!a.entity.startsWith("binary_sensor.")),this.config.logarithmic)))}connectedCallback(){super.connectedCallback(),this.config.update_interval&&(window.requestAnimationFrame(()=>{this.updateOnInterval()}),this.interval=setInterval(()=>this.updateOnInterval(),1e3*this.config.update_interval))}disconnectedCallback(){this.interval&&clearInterval(this.interval),super.disconnectedCallback()}shouldUpdate(a){if(Fe.some(b=>a.has(b)))return this.color=this.computeColor(void 0===this.tooltip.value?this.getEntityState(0):this.tooltip.value,this.tooltip.entity||0),!0}firstUpdated(){this.initial=!1}updated(a){this.config.animate&&a.has("line")&&(this.length.length{this.length[a.id]=a.getTotalLength()}),this.length=W(this.length)):this.length=Array(this.entity.length).fill("none"))}render(){var{config:a}=0void 0===this.entity[b])?this.renderWarnings():sc(M(),a.group,a.show.graph&&a.show.fill,"hover"===a.show.points,"hover"===a.show.labels,"hover"===a.show.labels_secondary,0this.handlePopup(b,a.tap_action.entity||this.entity[0]),this.renderHeader(),this.renderStates(),this.renderGraph(),this.renderInfo()):sc(N())}renderWarnings(){return sc(L(),this.config.entities.map((a,b)=>this.entity[b]?sc(J()):sc(K(),this.config.entities[b].entity)))}renderHeader(){var{show:a,align_icon:b,align_header:c,font_size_header:d}=this.config;return a.name||a.icon&&"state"!==b?sc(I(),c,d,this.renderName(),"state"===b?"":this.renderIcon()):""}renderIcon(){if(this.config.icon_image!==void 0)return sc(H(),this.config.icon_image);var{icon:a,icon_adaptive_color:b}=this.config.show;return a?sc(G(),this.config.align_icon,b?"color: ".concat(this.color,";"):"",this.computeIcon(this.entity[0])):""}renderName(){if(this.config.show.name){var a=void 0===this.tooltip.entity?this.config.name||this.computeName(0):this.computeName(this.tooltip.entity),b=this.config.show.name_adaptive_color?"opacity: 1; color: ".concat(this.color,";"):"";return sc(F(),b,a)}}renderStates(){if(this.config.show.state)return sc(E(),this.config.align_state,this.renderState(0),this.config.entities.map((a,b)=>0a&&a[b],a)}getEntityState(a){var b=this.config.entities[a];return"last"===this.config.show.state?this.points[a][this.points[a].length-1][Y]:b.attribute?this.getObjectAttr(this.entity[a].attributes,b.attribute):this.entity[a].state}renderState(a){var b=0===a;if(b||this.config.entities[a].show_state){var c=this.getEntityState(a),{entity:d,value:e}=this.tooltip,f=b&&d!==void 0,g=f?e:c,h=f?d:a,i=this.config.entities[h];return sc(D(),!b&&"state--small",b=>this.handlePopup(b,this.entity[a]),i.state_adaptive_color?"color: ".concat(this.computeColor(g,h)):"",i.show_indicator?this.renderIndicator(g,h):"",this.computeState(g),this.computeUom(h),b&&this.renderStateTime()||"")}}renderStateTime(){return void 0===this.tooltip.value?void 0:sc(C(),this.tooltip.label?sc(B(),this.tooltip.label):sc(A(),this.tooltip.time[0],this.tooltip.time[1]))}renderGraph(){var a=this.entity[0]&&!this.Graph.some((a,b)=>a._history===void 0&&!1!==this.config.entities[b].show_graph)||!1===this.config.show.loading_indicator;return this.config.show.graph?sc(z(),a?sc(y(),this.renderLabels(),this.renderLabelsSecondary(),this.renderSvg(),this.renderLegend()):sc(x())):""}computeLegend(a){var b=this.computeName(a),c=this.getEntityState(a),{show_legend_state:d=!1}=this.config.entities[a];if(d){if(b+=" (".concat(this.computeState(c)),!["unavailable"].includes(c)){var e=this.computeUom(a);["%",""].includes(e)||(b+=" "),b+="".concat(e)}b+=")"}return b}renderLegend(){return 1>=this.visibleLegends.length||!this.config.show.legend?void 0:sc(w(),this.visibleLegends.map(a=>{var b=this.computeLegend(a.index);return sc(v(),b=>this.handlePopup(b,this.entity[a.index]),()=>this.setTooltip(a.index,-1,this.getEntityState(a.index),"Current"),()=>this.tooltip={},this.renderIndicator(this.getEntityState(a.index),a.index),b)}))}renderIndicator(a,b){return tc(u(),this.computeColor(a,b))}renderSvgFill(a,b){if(a){var c="fade"===this.config.show.fill,d=this.length[b]||!1===this.config.entities[b].show_line;return tc(t(),"fill-grad-".concat(this.id,"-").concat(b),"fill-grad-mask-".concat(this.id,"-").concat(b),"url(#fill-grad-".concat(this.id,"-").concat(b,")"),"fill-".concat(this.id,"-").concat(b),this.config.show.fill,b,this.config.animate,d,this.config.animate?"".concat(.5*b,"s"):"0s",c?"url(#fill-grad-mask-".concat(this.id,"-").concat(b,")"):"",this.fill[b])}}renderSvgLine(a,b){if(a){var c=tc(s(),b,this.config.animate,this.length[b],this.config.animate?"".concat(.5*b,"s"):"0s",this.length[b]||"none",this.length[b]||"none","white",this.config.line_width,this.line[b]);return tc(r(),"line-".concat(this.id,"-").concat(b),c)}}renderSvgPoint(a,b){var c=this.gradient[b]?this.computeColor(a[Y],b):"inherit";return tc(q(),this.tooltip.index!==a[3],"--mcg-hover: ".concat(c,";"),c,c,a[He],a[X],this.config.line_width,()=>this.setTooltip(b,a[3],a[Y]),()=>this.tooltip={})}renderSvgPoints(a,b){if(a){var c=this.computeColor(this.entity[b].state,b);return tc(p(),this.tooltip.entity===b,void 0!==this.tooltip.entity&&this.tooltip.entity!==b,this.length[b],this.config.animate&&"hover"!==this.config.show.points,this.config.animate?"".concat(.5*b+.5,"s"):"0s",c,c,this.config.line_width/2,a.map(a=>this.renderSvgPoint(a,b)))}}renderSvgGradient(a){if(a){var b=a.map((a,b)=>a?tc(o(),"grad-".concat(this.id,"-").concat(b),a.map(a=>tc(n(),a.color,"".concat(a.offset,"%")))):void 0);return tc(m(),b)}}renderSvgLineRect(a,b){if(a){var c=this.gradient[b]?"url(#grad-".concat(this.id,"-").concat(b,")"):this.computeColor(this.entity[b].state,b);return tc(l(),void 0!==this.tooltip.entity&&this.tooltip.entity!==b,"rect-".concat(this.id,"-").concat(b),c,"url(#line-".concat(this.id,"-").concat(b,")"))}}renderSvgFillRect(a,b){if(a){var c=this.gradient[b]?"url(#grad-".concat(this.id,"-").concat(b,")"):this.computeColor(this.entity[b].state,b);return tc(k(),void 0!==this.tooltip.entity&&this.tooltip.entity!==b,"fill-rect-".concat(this.id,"-").concat(b),c,"url(#fill-".concat(this.id,"-").concat(b,")"))}}renderSvgBars(a,b){if(a){var c=a.map((a,c)=>{var d=this.config.animate?tc(j(),this.config.height,a.y):"",e=this.computeColor(a.value,b);return tc(h(),a.x,a.y,a.height,a.width,e,()=>this.setTooltip(b,c,a.value),()=>this.tooltip={},d)});return tc(g(),this.config.animate,c)}}renderSvg(){var{height:a}=this.config;return tc(f(),0===a?0:"100%",a,a=>a.stopPropagation(),this.renderSvgGradient(this.gradient),this.fill.map((a,b)=>this.renderSvgFill(a,b)),this.fill.map((a,b)=>this.renderSvgFillRect(a,b)),this.line.map((a,b)=>this.renderSvgLine(a,b)),this.line.map((a,b)=>this.renderSvgLineRect(a,b)),this.bar.map((a,b)=>this.renderSvgBars(a,b)),this.points.map((a,b)=>this.renderSvgPoints(a,b)))}setTooltip(a,b,c){var d=3sc(b(),a.type,this.computeState(a.state),this.computeUom(0),"avg"===a.type?"":Te(new Date(a.last_changed),this.config.format,this._hass.language)))):sc(a())}handlePopup(a,b){a.stopPropagation(),Ke(this,this._hass,this.config,this.config.tap_action,b.entity_id||b)}get visibleEntities(){return this.config.entities.filter(a=>!1!==a.show_graph)}get primaryYaxisEntities(){return this.visibleEntities.filter(a=>a.y_axis===void 0||"primary"===a.y_axis)}get secondaryYaxisEntities(){return this.visibleEntities.filter(a=>"secondary"===a.y_axis)}get visibleLegends(){return this.visibleEntities.filter(a=>!1!==a.show_legend)}get primaryYaxisSeries(){return this.primaryYaxisEntities.map(a=>this.Graph[a.index])}get secondaryYaxisSeries(){return this.secondaryYaxisEntities.map(a=>this.Graph[a.index])}computeColor(a,b){var c,{color_thresholds:d,line_color:e}=this.config,f=+a||0;if(0a.valuea.valueb.value===a);if(f)return f.label;Ze("value [".concat(a,"] not found in state_map"))}var b="string"==typeof a?parseFloat(a.replace(/,/g,".")):+a;var c=this.config.decimals,d=10**this.config.value_factor;if(c===void 0||tb(c)||tb(b))return this.numberFormat(Fb(100*(b*d))/100,this._hass.language);var e=10**c;return this.numberFormat((Fb(b*d*e)/e).toFixed(c),this._hass.language,c)}numberFormat(a,b,c){return!tb(+a)&&Intl?new Intl.NumberFormat(b,{minimumFractionDigits:c}).format(+a):a.toString()}updateOnInterval(){this.stateChanged&&!this.updating&&(this.stateChanged=!1,this.updateData())}updateData(){var a=arguments,b=this;return U(function*(){var{config:c}=0b.updateEntity(a,c,e,d));yield Promise.all(f)}catch(a){Ze(a)}if(c.show.graph&&b.entity.forEach((a,c)=>{a&&b.Graph[c].update()}),b.updateBounds(),c.show.graph){var g=0;b.entity.forEach((a,d)=>{if(a&&0!==b.Graph[d].coords.length){var e="secondary"===c.entities[d].y_axis?b.boundSecondary:b.bound;if([b.Graph[d].min,b.Graph[d].max]=[e[0],e[1]],"bar"===c.show.graph){var f=b.visibleEntities.length;b.bar[d]=b.Graph[d].getBars(g,f,c.bar_spacing),g+=1}else{var h=b.Graph[d].getPath();!1!==c.entities[d].show_line&&(b.line[d]=h),c.show.fill&&!1!==c.entities[d].show_fill&&(b.fill[d]=b.Graph[d].getFill(h)),c.show.points&&!1!==c.entities[d].show_points&&(b.points[d]=b.Graph[d].getPoints()),0b[a])))||d:"~"===c[0]?Math[a].apply(Math,[+c.substr(1)].concat(W(b.map(b=>b[a])))):c}getBoundaries(a,b,c,d,e){var f=[this.getBoundary("min",a,b,d[0]),this.getBoundary("max",a,c,d[1])];if(e){var g=zb(f[0]-f[1]),h=parseFloat(e)-g;if(0c!=="".concat(a.entity_id,"-").concat(b));var f=[],g=c,h=!1,i=e.config.cache?yield e.getCache("".concat(a.entity_id,"_").concat(b),e.config.useCompress):void 0;if(i&&i.hours_to_show===e.config.hours_to_show){f=i.data;var k=f.findIndex(a=>new Date(a.last_changed)>c);-1===k?f=[]:(0g&&(g=new Date(l-1))}var j=yield e.fetchRecent(a.entity_id,g,d,!e.config.entities[b].attribute&&h,!!e.config.entities[b].attribute);if(j[0]&&0{e.config.entities[b].attribute&&(a.state=e.getObjectAttr(a.attributes,e.config.entities[b].attribute),delete a.attributes),0!tb(parseFloat(a.state))),j=j.map(a=>({last_changed:e.config.entities[b].attribute?a.last_updated:a.last_changed,state:a.state})),f=[].concat(W(f),W(j)),e.config.cache&&e.setCache("".concat(a.entity_id,"_").concat(b),{hours_to_show:e.config.hours_to_show,last_fetched:new Date,data:f,version:cf},e.config.useCompress).catch(a=>{Ze(a),pd.clear()})),0!==f.length)if(e.entity[0]&&a.entity_id===e.entity[0].entity_id&&e.updateExtrema(f),!0===e.config.entities[b].fixed_value){var m=f[f.length-1];e.Graph[b].history=[m,m]}else e.Graph[b].history=f}})()}fetchRecent(a,b,c,d,e){var f=this;return U(function*(){var g="history/period";return b&&(g+="/".concat(b.toISOString())),g+="?filter_entity_id=".concat(a),c&&(g+="&end_time=".concat(c.toISOString())),d&&(g+="&skip_initial_state"),e||(g+="&minimal_response&no_attributes"),e&&(g+="&significant_changes_only=0"),f._hass.callApi("GET",g)})()}updateExtrema(a){var{extrema:b,average:c}=this.config.show;this.abs=[].concat(W(b?[P({type:"min"},Qe(a,"state"))]:[]),W(c?[{type:"avg",state:Re(a,"state")}]:[]),W(b?[P({type:"max"},Se(a,"state"))]:[]))}_convertState(a){var b=this.config.state_map.findIndex(b=>b.value===a.state);-1===b||(a.state=b)}getEndDate(){var a=new Date;switch(this.config.group_by){case"date":a.setDate(a.getDate()+1),a.setHours(0,0,0);break;case"hour":a.setHours(a.getHours()+1),a.setMinutes(0,0);}return a}setNextUpdate(){if(!this.config.update_interval){var a=1/this.config.points_per_hour;clearInterval(this.interval),this.interval=setInterval(()=>{this.updating||this.updateData()},a*V)}}getCardSize(){return 3}}customElements.define("mini-graph-card",df),window.customCards=window.customCards||[],window.customCards.push({type:"mini-graph-card",name:"Mini Graph Card",preview:!1,description:"The Mini Graph card is a minimalistic and customizable graph card"})})})(); diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-media-player/mini-media-player.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-media-player/mini-media-player.js new file mode 100644 index 0000000..502cf51 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/mini-media-player/mini-media-player.js @@ -0,0 +1,1649 @@ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +function t(t,e,i,r){var o,n=arguments.length,s=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,i):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,i,r);else for(var a=t.length-1;a>=0;a--)(o=t[a])&&(s=(n<3?o(s):n>3?o(e,i,s):o(e,i))||s);return n>3&&s&&Object.defineProperty(e,i,s),s +/** + * @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 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}},r=`{{lit-${String(Math.random()).slice(2)}}}`,o=`\x3c!--${r}--\x3e`,n=new RegExp(`${r}|${o}`);class s{constructor(t,e){this.parts=[],this.element=e;const i=[],o=[],s=document.createTreeWalker(e.content,133,null,!1);let l=0,h=-1,p=0;const{strings:d,values:{length:m}}=t;for(;p0;){const e=d[p],i=u.exec(e)[2],r=i.toLowerCase()+"$lit$",o=t.getAttribute(r);t.removeAttribute(r);const s=o.split(n);this.parts.push({type:"attribute",index:h,name:i,strings:s}),p+=s.length-1}}"TEMPLATE"===t.tagName&&(o.push(t),s.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(r)>=0){const r=t.parentNode,o=e.split(n),s=o.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},l=t=>-1!==t.index,c=()=>document.createComment(""),u=/([ \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:r}=t,o=document.createTreeWalker(i,133,null,!1);let n=d(r),s=r[n],a=-1,l=0;const c=[];let u=null;for(;o.nextNode();){a++;const t=o.currentNode;for(t.previousSibling===u&&(u=null),e.has(t)&&(c.push(t),null===u&&(u=t)),null!==u&&l++;void 0!==s&&s.index===a;)s.index=null!==u?-1:s.index-l,n=d(r,n),s=r[n]}c.forEach((t=>t.parentNode.removeChild(t)))}const p=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},d=(t,e=-1)=>{for(let i=e+1;i(...e)=>{const i=t(...e);return m.set(i,!0),i},f=t=>"function"==typeof t&&m.has(t),v={},_={}; +/** + * @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 y{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=[],r=this.template.parts,o=document.createTreeWalker(t,133,null,!1);let n,s=0,a=0,c=o.nextNode();for(;st}),w=` ${r} `;class x{constructor(t,e,i,r){this.strings=t,this.values=e,this.type=i,this.processor=r}getHTML(){const t=this.strings.length-1;let e="",i=!1;for(let n=0;n-1||i)&&-1===t.indexOf("--\x3e",s+1);const a=u.exec(t);e+=null===a?t+(i?w:o):t.substr(0,a.index)+a[1]+a[2]+"$lit$"+a[3]+r}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==b&&(e=b.createHTML(e)),t.innerHTML=e,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 k=t=>null===t||!("object"==typeof t||"function"==typeof t),S=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class ${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 O=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class A{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(;f(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=v,t(this)}if(this.__pendingValue===v)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),r=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),r&&(this.__options=V(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=v}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const V=t=>t&&(O?{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 N(t){let e=L.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},L.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const o=t.strings.join(r);return i=e.keyString.get(o),void 0===i&&(i=new s(t,t.getTemplateElement()),e.keyString.set(o,i)),e.stringsArray.set(t.strings,i),i}const L=new Map,j=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 D=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,i,r){const o=e[0];if("."===o){return new E(t,e.slice(1),i).parts}if("@"===o)return[new A(t,e.slice(1),r.eventContext)];if("?"===o)return[new M(t,e.slice(1),i)];return new $(t,e,i).parts}handleTextExpression(t){return new C(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.4.1");const I=(t,...e)=>new x(t,e,"html",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 + */,R=(t,e)=>`${t}--${e}`;let z=!0;void 0===window.ShadyCSS?z=!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."),z=!1);const U=t=>e=>{const i=R(e.type,t);let o=L.get(i);void 0===o&&(o={stringsArray:new WeakMap,keyString:new Map},L.set(i,o));let n=o.stringsArray.get(e.strings);if(void 0!==n)return n;const a=e.strings.join(r);if(n=o.keyString.get(a),void 0===n){const i=e.getTemplateElement();z&&window.ShadyCSS.prepareTemplateDom(i,t),n=new s(e,i),o.keyString.set(a,n)}return o.stringsArray.set(e.strings,n),n},q=["html","svg"],B=new Set,F=(t,e,i)=>{B.add(t);const r=i?i.element:document.createElement("template"),o=e.querySelectorAll("style"),{length:n}=o;if(0===n)return void window.ShadyCSS.prepareTemplateStyles(r,t);const s=document.createElement("style");for(let t=0;t{q.forEach((e=>{const i=L.get(R(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=r.content;i?function(t,e,i=null){const{element:{content:r},parts:o}=t;if(null==i)return void r.appendChild(e);const n=document.createTreeWalker(r,133,null,!1);let s=d(o),a=0,l=-1;for(;n.nextNode();)for(l++,n.currentNode===i&&(a=p(e),i.parentNode.insertBefore(e,i));-1!==s&&o[s].index===l;){if(a>0){for(;-1!==s;)o[s].index+=a,s=d(o,s);return}s=d(o,s)}}(i,s,a.firstChild):a.insertBefore(s,a.firstChild),window.ShadyCSS.prepareTemplateStyles(r,t);const l=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(s,a.firstChild);const t=new Set;t.add(s),h(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const G={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}},H=(t,e)=>e!==t&&(e==e||t==t),W={attribute:!0,type:String,converter:G,reflect:!1,hasChanged:H};class Z extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const r=this._attributeNameForProperty(i,e);void 0!==r&&(this._attributeToPropertyMap.set(r,i),t.push(r))})),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=W){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():`__${t}`,r=this.getPropertyDescriptor(t,i,e);void 0!==r&&Object.defineProperty(this.prototype,t,r)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(r){const o=this[t];this[e]=r,this.requestUpdateInternal(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||W}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=H){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,r=e.converter||G,o="function"==typeof r?r:r.fromAttribute;return o?o(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,r=e.converter;return(r&&r.toAttribute||G.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=W){const r=this.constructor,o=r._attributeNameForProperty(t,i);if(void 0!==o){const t=r._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(o):this.setAttribute(o,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,r=i._attributeToPropertyMap.get(t);if(void 0!==r){const t=i.getPropertyOptions(r);this._updateState=16|this._updateState,this[r]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let r=!0;if(void 0!==t){const o=this.constructor;i=i||o.getPropertyOptions(t),o._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))):r=!1}!this._hasRequestedUpdate&&r&&(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.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){}}Z.finalized=!0; +/** + * @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 J=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:r}=e;return{kind:i,elements:r,finisher(e){window.customElements.define(t,e)}}})(t,e),X=(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 Y(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):X(t,e)}const K=t=>function(t){return Y({attribute:!1,hasChanged:null==t?void 0:t.hasChanged})}(t) +/** +@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 +*/,Q=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&&(Q?(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,r)=>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[r+1]),t[0]);return new et(i,tt)}; +/** + * @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.5.1");const rt={};class ot extends Z{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),r=[];i.forEach((t=>r.unshift(t))),this._styles=r}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!Q){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(this.constructor.shadowRootOptions)}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?Q?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!==rt&&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 rt}}ot.finalized=!0,ot.render=(t,e,r)=>{if(!r||"object"!=typeof r||!r.scopeName)throw new Error("The `scopeName` option is required.");const o=r.scopeName,n=j.has(e),s=z&&11===e.nodeType&&!!e.host,a=s&&!B.has(o),l=a?document.createDocumentFragment():e;if(((t,e,r)=>{let o=j.get(e);void 0===o&&(i(e,e.firstChild),j.set(e,o=new C(Object.assign({templateFactory:N},r))),o.appendInto(e)),o.setValue(t),o.commit()})(t,l,Object.assign({templateFactory:U(o)},r)),a){const t=j.get(l);j.delete(l);const r=t.value instanceof y?t.value.template:void 0;F(o,l,r),i(e,e.firstChild),e.appendChild(l),j.set(e,t)}!n&&s&&window.ShadyCSS.styleElement(e.host)},ot.shadowRootOptions={mode:"open"}; +/** + * @license + * Copyright (c) 2018 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 nt{constructor(t){this.classes=new Set,this.changed=!1,this.element=t;const e=(t.getAttribute("class")||"").split(/\s+/);for(const t of e)this.classes.add(t)}add(t){this.classes.add(t),this.changed=!0}remove(t){this.classes.delete(t),this.changed=!0}commit(){if(this.changed){let t="";this.classes.forEach((e=>t+=e+" ")),this.element.setAttribute("class",t)}}}const st=new WeakMap,at=g((t=>e=>{if(!(e instanceof P)||e instanceof T||"class"!==e.committer.name||e.committer.parts.length>1)throw new Error("The `classMap` directive must be used in the `class` attribute and must be the only part in the attribute.");const{committer:i}=e,{element:r}=i;let o=st.get(e);void 0===o&&(r.setAttribute("class",i.strings.join(" ")),st.set(e,o=new Set));const n=r.classList||new nt(r);o.forEach((e=>{e in t||(n.remove(e),o.delete(e))}));for(const e in t){const i=t[e];i!=o.has(e)&&(i?(n.add(e),o.add(e)):(n.remove(e),o.delete(e)))}"function"==typeof n.commit&&n.commit()})),lt=new WeakMap,ct=g((t=>e=>{if(!(e instanceof P)||e instanceof T||"style"!==e.committer.name||e.committer.parts.length>1)throw new Error("The `styleMap` directive must be used in the style attribute and must be the only part in the attribute.");const{committer:i}=e,{style:r}=i.element;let o=lt.get(e);void 0===o&&(r.cssText=i.strings.join(" "),lt.set(e,o=new Set)),o.forEach((e=>{e in t||(o.delete(e),-1===e.indexOf("-")?r[e]=null:r.removeProperty(e))}));for(const e in t)o.add(e),-1===e.indexOf("-")?r[e]=t[e]:r.setProperty(e,t[e])}));var ut=function(){if("undefined"!=typeof Map)return Map;function t(t,e){var i=-1;return t.some((function(t,r){return t[0]===e&&(i=r,!0)})),i}return function(){function e(){this.__entries__=[]}return Object.defineProperty(e.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),e.prototype.get=function(e){var i=t(this.__entries__,e),r=this.__entries__[i];return r&&r[1]},e.prototype.set=function(e,i){var r=t(this.__entries__,e);~r?this.__entries__[r][1]=i:this.__entries__.push([e,i])},e.prototype.delete=function(e){var i=this.__entries__,r=t(i,e);~r&&i.splice(r,1)},e.prototype.has=function(e){return!!~t(this.__entries__,e)},e.prototype.clear=function(){this.__entries__.splice(0)},e.prototype.forEach=function(t,e){void 0===e&&(e=null);for(var i=0,r=this.__entries__;i0},t.prototype.connect_=function(){ht&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),gt?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},t.prototype.disconnect_=function(){ht&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},t.prototype.onTransitionEnd_=function(t){var e=t.propertyName,i=void 0===e?"":e;mt.some((function(t){return!!~i.indexOf(t)}))&&this.refresh()},t.getInstance=function(){return this.instance_||(this.instance_=new t),this.instance_},t.instance_=null,t}(),vt=function(t,e){for(var i=0,r=Object.keys(e);i0},t}(),Et="undefined"!=typeof WeakMap?new WeakMap:new ut,Tt=function t(e){if(!(this instanceof t))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var i=ft.getInstance(),r=new Mt(e,i,this);Et.set(this,r)};["observe","unobserve","disconnect"].forEach((function(t){Tt.prototype[t]=function(){var e;return(e=Et.get(this))[t].apply(e,arguments)}}));var Ot=void 0!==pt.ResizeObserver?pt.ResizeObserver:Tt;const At={repeat:!0,shuffle:!0,power_state:!0,artwork_border:!0,icon_state:!0,sound_mode:!0,group_button:!1,runtime:!0,runtime_remaining:!0,volume:!1,volume_level:!0,controls:!1,play_pause:!1,play_stop:!0,prev:!1,next:!1,jump:!0,state_label:!1,progress:!1,icon:!1,name:!1,info:!1},Vt={OFF:"off",ALL:"all",ONE:"one"},Nt="mdi:chevron-down",Lt="mdi:speaker-multiple",jt={true:"mdi:volume-off",false:"mdi:volume-high"},Dt="mdi:skip-next",It={true:"mdi:pause",false:"mdi:play"},Rt="mdi:power",zt="mdi:skip-previous",Ut="mdi:shuffle",qt={[Vt.OFF]:"mdi:repeat-off",[Vt.ONE]:"mdi:repeat-once",[Vt.ALL]:"mdi:repeat"},Bt={true:"mdi:stop",false:"mdi:play"},Ft="mdi:volume-minus",Gt="mdi:volume-plus",Ht="mdi:fast-forward",Wt="mdi:rewind",Zt=["entity","groupMgmtEntity","_overflow","break","thumbnail","prevThumbnail","edit","idle","cardHeight","backgroundColor","foregroundColor"],Jt=["media_duration","media_position","media_position_updated_at"],Xt=[{attr:"media_title"},{attr:"media_artist"},{attr:"media_series_title"},{attr:"media_season",prefix:"S"},{attr:"media_episode",prefix:"E"},{attr:"media_channel"},{attr:"app_name"}],Yt="sonos",Kt="squeezebox",Qt="soundtouch",te="media_player",ee="heos";var ie;!function(t){t.MORE_INFO="more-info",t.NAVIGATE="navigate",t.CALL_SERVICE="call-service",t.URL="url",t.FIRE_DOM_EVENT="fire-dom-event",t.NONE="none"}(ie||(ie={}));const re=t=>{var e;(t=>{if(void 0===t.entity)throw new Error("You need to specify the required entity option.");if("media_player"!==t.entity.split(".")[0])throw new Error("Specify an entity from within the media_player domain.");if(void 0===t.type)throw new Error("You need to specify the required type option.")})(t);const i=Object.assign(Object.assign({artwork:"default",info:"default",group:!1,volume_stateless:!1,more_info:!0,source:"default",sound_mode:"default",toggle_power:!0,tap_action:{action:ie.MORE_INFO},jump_amount:10},t),{hide:Object.assign(Object.assign({},At),t.hide),speaker_group:Object.assign(Object.assign({show_group_count:!0,platform:"sonos",supports_master:!0,entities:[]},t.sonos),t.speaker_group),shortcuts:Object.assign({label:"Shortcuts..."},t.shortcuts),max_volume:null!==(e=Number(t.max_volume))&&void 0!==e?e:100,min_volume:Number(t.min_volume)||0});return i.collapse=i.hide.controls||i.hide.volume,i.info=i.collapse&&"scroll"!==i.info?"short":i.info,i.flow=i.hide.icon&&i.hide.name&&i.hide.info,i};var oe;!function(t){t.PLAYING="playing",t.PAUSED="paused",t.IDLE="idle",t.OFF="off",t.ON="on",t.UNAVAILABLE="unavailable",t.UNKNOWN="unknown",t.STANDBY="standby"}(oe||(oe={}));class ne{constructor(t,e,i){this.hass=t||{},this.config=e||{},this.entity=i||{},this.state=i.state,this._entityId=i&&i.entity_id||this.config.entity,this._attr=i.attributes||{},this.idle=!!e.idle_view&&this.idleView,this._active=this.isActive}get id(){return this.entity.entity_id}get icon(){return this._attr.icon}get isPaused(){return this.state===oe.PAUSED}get isPlaying(){return this.state===oe.PLAYING}get isIdle(){return this.state===oe.IDLE}get isStandby(){return this.state===oe.STANDBY}get isUnavailable(){return this.state===oe.UNAVAILABLE}get isOff(){return this.state===oe.OFF}get isActive(){return!this.isOff&&!this.isUnavailable&&!this.idle||!1}get assumedState(){return this._attr.assumed_state||!1}get shuffle(){return this._attr.shuffle||!1}get repeat(){return this._attr.repeat||Vt.OFF}get content(){return this._attr.media_content_type||"none"}get mediaDuration(){return this._attr.media_duration||0}get updatedAt(){return this._attr.media_position_updated_at||0}get position(){return this._attr.media_position||0}get name(){return this._attr.friendly_name||""}get groupCount(){return this.group.length}get isGrouped(){return this.group.length>1}get group(){return this.platform===Kt?this._attr.sync_group||[]:this.platform===te||this.platform===ee||this.platform===Yt?this._attr.group_members||[]:this._attr[`${this.platform}_group`]||[]}get platform(){return this.config.speaker_group.platform}get master(){return this.supportsMaster&&this.group[0]||this._entityId}get isMaster(){return this.master===this._entityId}get sources(){return this._attr.source_list||[]}get source(){return this._attr.source||""}get soundModes(){return this._attr.sound_mode_list||[]}get soundMode(){return this._attr.sound_mode||""}get muted(){return this._attr.is_volume_muted||!1}get vol(){return this._attr.volume_level||0}get picture(){return this._attr.entity_picture_local||this._attr.entity_picture}get hasArtwork(){return!!this.picture&&"none"!==this.config.artwork&&this._active&&!this.idle}get mediaInfo(){return Xt.map((t=>Object.assign({text:this._attr[t.attr],prefix:""},t))).filter((t=>t.text))}get hasProgress(){var t;return!this.config.hide.progress&&!this.idle&&Jt.every((t=>t in this._attr))&&(null!==(t=this._attr.media_duration)&&void 0!==t?t:-1)>-1}get supportsPrev(){return!!this._attr.supported_features&&(16|this._attr.supported_features)===this._attr.supported_features}get supportsNext(){return!!this._attr.supported_features&&(32|this._attr.supported_features)===this._attr.supported_features}get progress(){return this.isPlaying?this.position+(Date.now()-new Date(this.updatedAt).getTime())/1e3:this.position}get idleView(){const t=this.config.idle_view;return!!((null==t?void 0:t.when_idle)&&this.isIdle||(null==t?void 0:t.when_standby)&&this.isStandby||(null==t?void 0:t.when_paused)&&this.isPaused)||!(!this.updatedAt||!(null==t?void 0:t.after)||this.isPlaying)&&this.checkIdleAfter(t.after)}get trackIdle(){var t,e;return Boolean(this._active&&!this.isPlaying&&this.updatedAt&&(null===(e=null===(t=this.config)||void 0===t?void 0:t.idle_view)||void 0===e?void 0:e.after))}checkIdleAfter(t){const e=(Date.now()-new Date(this.updatedAt).getTime())/1e3;return this.idle=e>60*t,this._active=this.isActive,this.idle}get supportsShuffle(){return void 0!==this._attr.shuffle}get supportsRepeat(){return void 0!==this._attr.repeat}get supportsMute(){return void 0!==this._attr.is_volume_muted}get supportsVolumeSet(){return void 0!==this._attr.volume_level}get supportsMaster(){return this.platform!==Kt&&this.config.speaker_group.supports_master}async fetchArtwork(){const t=this._attr.entity_picture_local?this.hass.hassUrl(this.picture):this.picture;try{const e=await fetch(new Request(t)),i=(t=>{let e="";return[].slice.call(new Uint8Array(t)).forEach((t=>e+=String.fromCharCode(t))),window.btoa(e)})(await e.arrayBuffer());return`url(data:${e.headers.get("Content-Type")||"image/jpeg"};base64,${i})`}catch(t){return!1}}getAttribute(t){return this._attr[t]}toggle(t){return this.config.toggle_power?this.callService(t,"toggle"):this.isOff?this.callService(t,"turn_on"):void this.callService(t,"turn_off")}toggleMute(t){this.config.speaker_group.sync_volume?this.group.forEach((e=>{this.callService(t,"volume_mute",{entity_id:e,is_volume_muted:!this.muted})})):this.callService(t,"volume_mute",{is_volume_muted:!this.muted})}toggleShuffle(t){this.callService(t,"shuffle_set",{shuffle:!this.shuffle})}toggleRepeat(t){const e=Object.values(Vt),{length:i}=e,r=e.indexOf(this.repeat)-1,o=e[(r-1%i+i)%i];this.callService(t,"repeat_set",{repeat:o})}setSource(t,e){this.callService(t,"select_source",{source:e})}setMedia(t,e){this.callService(t,"play_media",Object.assign({},e))}play(t){this.callService(t,"media_play")}pause(t){this.callService(t,"media_pause")}playPause(t){this.callService(t,"media_play_pause")}playStop(t){this.isPlaying?this.callService(t,"media_stop"):this.callService(t,"media_play")}setSoundMode(t,e){this.callService(t,"select_sound_mode",{sound_mode:e})}next(t){this.callService(t,"media_next_track")}prev(t){this.callService(t,"media_previous_track")}stop(t){this.callService(t,"media_stop")}volumeUp(t){this.supportsVolumeSet&&this.config.volume_step&&this.config.volume_step>0?this.callService(t,"volume_set",{entity_id:this._entityId,volume_level:Math.min(this.vol+this.config.volume_step/100,1)}):this.callService(t,"volume_up")}volumeDown(t){this.supportsVolumeSet&&this.config.volume_step&&this.config.volume_step>0?this.callService(t,"volume_set",{entity_id:this._entityId,volume_level:Math.max(this.vol-this.config.volume_step/100,0)}):this.callService(t,"volume_down")}seek(t,e){this.callService(t,"media_seek",{seek_position:e})}jump(t,e){const i=this.progress+e,r=Math.min(Math.max(i,0),Number(this.mediaDuration)||i);this.callService(t,"media_seek",{seek_position:r})}setVolume(t,e){this.config.speaker_group.sync_volume&&this.config.speaker_group.entities?this.group.forEach((i=>{var r;const o=null===(r=this.config.speaker_group.entities)||void 0===r?void 0:r.find((t=>t.entity_id===i));if(void 0===o)return;let n=e;o.volume_offset&&(n+=o.volume_offset/100,n>1&&(n=1),n<0&&(n=0)),this.callService(t,"volume_set",{entity_id:i,volume_level:n})})):this.callService(t,"volume_set",{entity_id:this._entityId,volume_level:e})}handleGroupChange(t,e,i){const{platform:r}=this,o={entity_id:e};if(i)switch(o.master=this._entityId,r){case Qt:return this.handleSoundtouch(t,this.isGrouped?"ADD_ZONE_SLAVE":"CREATE_ZONE",e);case Kt:return this.callService(t,"sync",{entity_id:this._entityId,other_player:e},Kt);case te:case Yt:return this.callService(t,"join",{entity_id:this._entityId,group_members:e},te);case ee:return this.callService(t,"join",{entity_id:this._entityId,group_members:this.group.concat("string"==typeof e?[e]:e)},te);default:return this.callService(t,"join",o,r)}else switch(r){case Qt:return this.handleSoundtouch(t,"REMOVE_ZONE_SLAVE",e);case Kt:return this.callService(t,"unsync",o,Kt);case te:case Yt:return this.callService(t,"unjoin",{entity_id:e},te);case ee:return this.callService(t,"unjoin",{entity_id:"string"==typeof e?e:e[0]},te);default:return this.callService(t,"unjoin",o,r)}}handleSoundtouch(t,e,i){return this.callService(t,e,{master:this.master,slaves:i},Qt,!0)}toggleScript(t,e,i={}){const[,r]=e.split(".");this.callService(t,r,Object.assign({},i),"script")}toggleService(t,e,i={}){t.stopPropagation();const[r,o]=e.split(".");this.hass.callService(r,o,Object.assign({},i))}callService(t,e,i,r="media_player",o=!1){t.stopPropagation(),this.hass.callService(r,e,Object.assign(Object.assign({},!o&&{entity_id:this._entityId}),i))}}const se=it` + :host { + overflow: visible !important; + display: block; + --mmp-scale: var(--mini-media-player-scale, 1); + --mmp-unit: calc(var(--mmp-scale) * 40px); + --mmp-name-font-weight: var(--mini-media-player-name-font-weight, 400); + --mmp-accent-color: var(--mini-media-player-accent-color, var(--accent-color, #f39c12)); + --mmp-base-color: var(--mini-media-player-base-color, var(--primary-text-color, #000)); + --mmp-overlay-color: var(--mini-media-player-overlay-color, rgba(0, 0, 0, 0.5)); + --mmp-overlay-color-stop: var(--mini-media-player-overlay-color-stop, 25%); + --mmp-overlay-base-color: var(--mini-media-player-overlay-base-color, #fff); + --mmp-overlay-accent-color: var(--mini-media-player-overlay-accent-color, --mmp-accent-color); + --mmp-text-color: var(--mini-media-player-base-color, var(--primary-text-color, #000)); + --mmp-media-cover-info-color: var(--mini-media-player-media-cover-info-color, --mmp-text-color); + --mmp-text-color-inverted: var(--disabled-text-color); + --mmp-active-color: var(--mmp-accent-color); + --mmp-button-color: var(--mini-media-player-button-color, rgba(255, 255, 255, 0.25)); + --mmp-icon-color: var( + --mini-media-player-icon-color, + var(--mini-media-player-base-color, var(--paper-item-icon-color, #44739e)) + ); + --mmp-icon-active-color: var(--paper-item-icon-active-color, --mmp-active-color); + --mmp-info-opacity: 0.75; + --mmp-bg-opacity: var(--mini-media-player-background-opacity, 1); + --mmp-artwork-opacity: var(--mini-media-player-artwork-opacity, 1); + --mmp-progress-height: var(--mini-media-player-progress-height, 6px); + --mmp-border-radius: var(--ha-card-border-radius, 12px); + --mdc-theme-primary: var(--mmp-text-color); + --mdc-theme-on-primary: var(--mmp-text-color); + --paper-checkbox-unchecked-color: var(--mmp-text-color); + --paper-checkbox-label-color: var(--mmp-text-color); + color: var(--mmp-text-color); + } + ha-card.--bg { + --mmp-info-opacity: 0.75; + } + ha-card.--has-artwork[artwork='material'], + ha-card.--has-artwork[artwork*='cover'] { + --mmp-accent-color: var( + --mini-media-player-overlay-accent-color, + var(--mini-media-player-accent-color, var(--accent-color, #f39c12)) + ); + --mmp-text-color: var(--mmp-overlay-base-color); + --mmp-text-color-inverted: #000; + --mmp-active-color: rgba(255, 255, 255, 0.5); + --mmp-icon-color: var(--mmp-text-color); + --mmp-icon-active-color: var(--mmp-text-color); + --mmp-info-opacity: 0.75; + --disabled-color: var(--mini-media-player-overlay-color, rgba(255, 255, 255, 0.75)) !important; + --mdc-theme-primary: var(--mmp-text-color); + --mdc-theme-on-primary: var(--mmp-text-color); + --paper-checkbox-unchecked-color: var(--mmp-text-color); + --paper-checkbox-label-color: var(--mmp-text-color); + --switch-checked-color: var(--mmp-accent-color); + --switch-checked-button-color: var(--mmp-accent-color); + --switch-checked-track-color: var(--mmp-accent-color); + --switch-unchecked-color: var(--mmp-text-color); + --switch-unchecked-button-color: var(--mmp-text-color); + --switch-unchecked-track-color: var(--mmp-text-color); + --mdc-text-field-fill-color: transparent; + --mdc-text-field-ink-color: var(--mmp-text-color); + --mdc-text-field-idle-line-color: var(--mmp-text-color); + --mdc-text-field-label-ink-color: var(--mmp-text-color); + --mdc-text-field-hover-line-color: var(--mmp-text-color); + --mdc-ripple-color: var(--mmp-text-color); + --text-field-padding: 0; + color: var(--mmp-text-color); + } + ha-card { + cursor: default; + display: flex; + background: transparent; + overflow: visible; + padding: 0; + position: relative; + color: inherit; + font-size: calc(var(--mmp-unit) * 0.35); + --mdc-icon-button-size: calc(var(--mmp-unit)); + --mdc-icon-size: calc(var(--mmp-unit) * 0.6); + } + ha-card.--group { + box-shadow: none; + border: none; + --mmp-progress-height: var(--mini-media-player-progress-height, 4px); + --mmp-border-radius: 0px + } + ha-card.--more-info { + cursor: pointer; + } + .mmp__bg, + .mmp-player, + .mmp__container { + border-radius: var(--mmp-border-radius); + } + .mmp__container { + overflow: hidden; + height: 100%; + width: 100%; + position: absolute; + pointer-events: none; + -webkit-transform: translateZ(0); + transform: translateZ(0); + } + ha-card:before { + content: ''; + padding-top: 0px; + transition: padding-top 0.5s cubic-bezier(0.21, 0.61, 0.35, 1); + will-change: padding-top; + } + ha-card.--initial .entity__artwork, + ha-card.--initial .entity__icon { + animation-duration: 0.001s; + } + ha-card.--initial:before, + ha-card.--initial .mmp-player { + transition: none; + } + header { + display: none; + } + ha-card[artwork='full-cover'].--has-artwork:before { + padding-top: 56%; + } + ha-card[artwork='full-cover'].--has-artwork[content='music']:before, + ha-card[artwork='full-cover-fit'].--has-artwork:before { + padding-top: 100%; + } + .mmp__bg { + background: var(--ha-card-background, var(--card-background-color, var(--paper-card-background-color, white))); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + overflow: hidden; + -webkit-transform: translateZ(0); + transform: translateZ(0); + opacity: var(--mmp-bg-opacity); + } + ha-card[artwork='material'].--has-artwork .mmp__bg, + ha-card[artwork*='cover'].--has-artwork .mmp__bg { + opacity: var(--mmp-artwork-opacity); + background: transparent; + } + ha-card[artwork='material'].--has-artwork .cover { + height: 100%; + right: 0; + left: unset; + animation: fade-in 4s cubic-bezier(0.21, 0.61, 0.35, 1) !important; + } + ha-card[artwork='material'].--has-artwork .cover.--prev { + animation: fade-in 1s linear reverse forwards !important; + } + ha-card[artwork='material'].--has-artwork .cover-gradient { + position: absolute; + height: 100%; + right: 0; + left: 0; + opacity: 1; + } + ha-card.--group .mmp__bg { + background: transparent; + } + ha-card.--inactive .cover { + opacity: 0; + } + ha-card.--inactive .cover.--bg { + opacity: 1; + } + .cover-gradient { + transition: opacity 0.45s linear; + opacity: 0; + } + .cover, + .cover:before { + display: block; + opacity: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transition: opacity 0.75s linear, width 0.05s cubic-bezier(0.21, 0.61, 0.35, 1); + will-change: opacity; + } + .cover:before { + content: ''; + background: var(--mmp-overlay-color); + } + .cover { + animation: fade-in 0.5s cubic-bezier(0.21, 0.61, 0.35, 1); + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: var(--mmp-border-radius, 0); + overflow: hidden; + } + .cover.--prev { + animation: fade-in 0.5s linear reverse forwards; + } + .cover.--bg { + opacity: 1; + } + ha-card[artwork*='full-cover'].--has-artwork .mmp-player { + background: linear-gradient(to top, var(--mmp-overlay-color) var(--mmp-overlay-color-stop), transparent 100%); + } + ha-card.--has-artwork .cover, + ha-card.--has-artwork[artwork='cover'] .cover:before { + opacity: 0.999; + } + ha-card[artwork='default'] .cover { + display: none; + } + ha-card.--bg .cover { + display: block; + } + ha-card[artwork='material'].--has-artwork .cover { + background-size: cover; + } + ha-card[artwork='full-cover-fit'].--has-artwork .cover { + background-color: black; + background-size: contain; + } + .mmp-player { + align-self: flex-end; + box-sizing: border-box; + position: relative; + padding: 16px; + transition: padding 0.25s ease-out; + width: 100%; + will-change: padding; + } + ha-card.--group .mmp-player { + padding: 2px 0; + } + .flex { + display: flex; + display: -ms-flexbox; + display: -webkit-flex; + flex-direction: row; + } + .mmp-player__core { + position: relative; + } + .entity__info { + justify-content: center; + display: flex; + flex-direction: column; + margin-left: 8px; + position: relative; + overflow: hidden; + user-select: none; + } + ha-card.--rtl .entity__info { + margin-left: auto; + margin-right: calc(var(--mmp-unit) / 5); + } + ha-card[content='movie'] .attr__media_season, + ha-card[content='movie'] .attr__media_episode { + display: none; + } + .entity__icon { + color: var(--mmp-icon-color); + } + .entity__icon[color] { + color: var(--mmp-icon-active-color); + } + .entity__artwork, + .entity__icon { + animation: fade-in 0.25s ease-out; + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + border-radius: 100%; + height: var(--mmp-unit); + width: var(--mmp-unit); + min-width: var(--mmp-unit); + line-height: var(--mmp-unit); + margin-right: calc(var(--mmp-unit) / 5); + position: relative; + text-align: center; + will-change: border-color; + transition: border-color 0.25s ease-out; + } + ha-card.--rtl .entity__artwork, + ha-card.--rtl .entity__icon { + margin-right: auto; + } + .entity__artwork[border] { + border: 2px solid var(--primary-text-color); + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + } + .entity__artwork[border][state='playing'] { + border-color: var(--mmp-accent-color); + } + .entity__info__name, + .entity__info__media[short] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .entity__info__name { + line-height: calc(var(--mmp-unit) / 2); + color: var(--mmp-text-color); + font-weight: var(--mmp-name-font-weight); + } + .entity__info__media { + color: var(--secondary-text-color); + max-height: 6em; + word-break: break-word; + opacity: var(--mmp-info-opacity); + transition: color 0.5s; + -webkit-text-size-adjust: 100%; + } + .entity__info__media[short] { + max-height: calc(var(--mmp-unit) / 2); + overflow: hidden; + } + .attr__app_name { + display: none; + } + .attr__app_name:first-child, + .attr__app_name:first-of-type { + display: inline; + } + .mmp-player__core[inactive] .entity__info__media { + color: var(--mmp-text-color); + max-width: 200px; + opacity: 0.5; + } + .entity__info__media[short-scroll] { + max-height: calc(var(--mmp-unit) / 2); + white-space: nowrap; + } + .entity__info__media[scroll] > span { + visibility: hidden; + } + .entity__info__media[scroll] > div { + animation: move linear infinite; + } + .entity__info__media[scroll] .marquee { + animation: slide linear infinite; + } + .entity__info__media[scroll] .marquee, + .entity__info__media[scroll] > div { + animation-duration: inherit; + visibility: visible; + } + .entity__info__media[scroll] { + animation-duration: 10s; + mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%); + -webkit-mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%); + } + .marquee { + visibility: hidden; + position: absolute; + white-space: nowrap; + } + ha-card[artwork*='cover'].--has-artwork .entity__info__media, + ha-card.--bg .entity__info__media { + color: var(--mmp-media-cover-info-color); + } + .entity__info__media span:before { + content: ' - '; + } + .entity__info__media span:first-of-type:before { + content: ''; + } + .entity__info__media span:empty { + display: none; + } + .mmp-player__adds { + margin-left: calc(var(--mmp-unit) * 1.2); + position: relative; + } + ha-card.--rtl .mmp-player__adds { + margin-left: auto; + margin-right: calc(var(--mmp-unit) * 1.2); + } + .mmp-player__adds > *:nth-child(2) { + margin-top: 0px; + } + mmp-powerstrip { + flex: 1; + justify-content: flex-end; + margin-right: 0; + margin-left: auto; + width: auto; + max-width: 100%; + } + mmp-media-controls { + flex-wrap: wrap; + } + ha-card.--flow mmp-powerstrip { + justify-content: space-between; + margin-left: auto; + } + ha-card.--flow.--rtl mmp-powerstrip { + margin-right: auto; + } + ha-card.--flow .entity__info { + display: none; + } + ha-card.--responsive .mmp-player__adds { + margin-left: 0; + } + ha-card.--responsive.--rtl .mmp-player__adds { + margin-right: 0; + } + ha-card.--responsive .mmp-player__adds > mmp-media-controls { + padding: 0; + } + ha-card.--progress .mmp-player { + padding-bottom: calc(16px + calc(var(--mini-media-player-progress-height, 6px) - 6px)); + } + ha-card.--progress.--group .mmp-player { + padding-bottom: calc(10px + calc(var(--mini-media-player-progress-height, 6px) - 6px)); + } + ha-card.--runtime .mmp-player { + padding-bottom: calc(16px + 16px + var(--mini-media-player-progress-height, 0px)); + } + ha-card.--runtime.--group .mmp-player { + padding-bottom: calc(16px + 12px + var(--mini-media-player-progress-height, 0px)); + } + ha-card.--inactive .mmp-player { + padding: 16px; + } + ha-card.--inactive.--group .mmp-player { + padding: 2px 0; + } + .mmp-player div:empty { + display: none; + } + @keyframes slide { + 100% { + transform: translateX(-100%); + } + } + @keyframes move { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } + } + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + ha-switch { + padding: 16px 6px; + } +`,ae=it` + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .label { + margin: 0 8px; + } + ha-icon { + width: calc(var(--mmp-unit) * 0.6); + height: calc(var(--mmp-unit) * 0.6); + } + ha-icon-button { + width: var(--mmp-unit); + height: var(--mmp-unit); + color: var(--mmp-text-color, var(--primary-text-color)); + transition: color 0.25s; + } + ha-icon-button[color] { + color: var(--mmp-accent-color, var(--accent-color)) !important; + opacity: 1 !important; + } + ha-icon-button[inactive] { + opacity: 0.5; + } + ha-icon-button ha-icon, + mmp-icon-button ha-icon { + display: flex; + } +`;var le=(t,e,i,r,o)=>{let n;switch(r.action){case"more-info":n=new Event("hass-more-info",{composed:!0}),n.detail={entityId:r.entity||o},t.dispatchEvent(n);break;case"navigate":if(!r.navigation_path)return;window.history.pushState(null,"",r.navigation_path),n=new Event("location-changed",{composed:!0}),n.detail={replace:!1},window.dispatchEvent(n);break;case"call-service":{if(!r.service)return;const[t,i]=r.service.split(".",2),o={...r.service_data};e.callService(t,i,o);break}case"url":if(!r.url)return;r.new_tab?window.open(r.url,"_blank"):window.location.href=r.url;break;case"fire-dom-event":n=new Event("ll-custom",{composed:!0,bubbles:!0}),n.detail=r,t.dispatchEvent(n)}r.haptic&&((t,e)=>{const i=new Event("haptic",{composed:!0});i.detail={haptic:e},t.dispatchEvent(i)})(t,r.haptic)};var ce,ue,he,pe=(ce=function(t,e){var i;window,i=function(){return function(t){var e={};function i(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,i),o.l=!0,o.exports}return i.m=t,i.c=e,i.d=function(t,e,r){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)i.d(r,o,function(e){return t[e]}.bind(null,o));return r},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=10)}([function(t,e,i){Object.defineProperty(e,"__esModule",{value:!0}),e.assignDeep=e.mapValues=void 0,e.mapValues=function(t,e){var i={};for(var r in t)if(t.hasOwnProperty(r)){var o=t[r];i[r]=e(o)}return i},e.assignDeep=function t(e){for(var i=[],r=1;r0){var o=Math.max(e,i);o>t.maxDimension&&(r=t.maxDimension/o)}else r=1/t.quality;r<1&&this.resize(e*r,i*r,r)},t}();e.ImageBase=r,e.applyFilters=function(t,e){if(e.length>0)for(var i=t.data,r=i.length/4,o=void 0,n=void 0,s=void 0,a=void 0,l=void 0,c=0;c0?t.filter((function(t){for(var i=t.r,r=t.g,o=t.b,n=0;n.04045?Math.pow((t+.005)/1.055,2.4):t/12.92,e=e>.04045?Math.pow((e+.005)/1.055,2.4):e/12.92,i=i>.04045?Math.pow((i+.005)/1.055,2.4):i/12.92,[.4124*(t*=100)+.3576*(e*=100)+.1805*(i*=100),.2126*t+.7152*e+.0722*i,.0193*t+.1192*e+.9505*i]}function n(t,e,i){return e/=100,i/=108.883,t=(t/=95.047)>.008856?Math.pow(t,1/3):7.787*t+16/116,[116*(e=e>.008856?Math.pow(e,1/3):7.787*e+16/116)-16,500*(t-e),200*(e-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]}function s(t,e,i){var r=o(t,e,i);return n(r[0],r[1],r[2])}function a(t,e){var i=t[0],r=t[1],o=t[2],n=e[0],s=e[1],a=e[2],l=i-n,c=r-s,u=o-a,h=Math.sqrt(r*r+o*o),p=n-i,d=Math.sqrt(s*s+a*a)-h,m=Math.sqrt(l*l+c*c+u*u),g=Math.sqrt(m)>Math.sqrt(Math.abs(p))+Math.sqrt(Math.abs(d))?Math.sqrt(m*m-p*p-d*d):0;return p/=1,d/=1*(1+.045*h),g/=1*(1+.015*h),Math.sqrt(p*p+d*d+g*g)}function l(t,e){return a(s.apply(void 0,t),s.apply(void 0,e))}Object.defineProperty(e,"__esModule",{value:!0}),e.getColorDiffStatus=e.hexDiff=e.rgbDiff=e.deltaE94=e.rgbToCIELab=e.xyzToCIELab=e.rgbToXyz=e.hslToRgb=e.rgbToHsl=e.rgbToHex=e.hexToRgb=e.DELTAE94_DIFF_STATUS=void 0,e.DELTAE94_DIFF_STATUS={NA:0,PERFECT:1,CLOSE:2,GOOD:10,SIMILAR:50},e.hexToRgb=r,e.rgbToHex=function(t,e,i){return"#"+((1<<24)+(t<<16)+(e<<8)+i).toString(16).slice(1,7)},e.rgbToHsl=function(t,e,i){t/=255,e/=255,i/=255;var r=Math.max(t,e,i),o=Math.min(t,e,i),n=0,s=0,a=(r+o)/2;if(r!==o){var l=r-o;switch(s=a>.5?l/(2-r-o):l/(r+o),r){case t:n=(e-i)/l+(e1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}if(0===e)r=o=n=i;else{var a=i<.5?i*(1+e):i+e-i*e,l=2*i-a;r=s(l,a,t+1/3),o=s(l,a,t),n=s(l,a,t-1/3)}return[255*r,255*o,255*n]},e.rgbToXyz=o,e.xyzToCIELab=n,e.rgbToCIELab=s,e.deltaE94=a,e.rgbDiff=l,e.hexDiff=function(t,e){return l(r(t),r(e))},e.getColorDiffStatus=function(t){return t0&&this._opts.filters.splice(e)}return this},t.prototype.clearFilters=function(){return this._opts.filters=[],this},t.prototype.quality=function(t){return this._opts.quality=t,this},t.prototype.useImageClass=function(t){return this._opts.ImageClass=t,this},t.prototype.useGenerator=function(t,e){return this._opts.generators||(this._opts.generators=[]),this._opts.generators.push(e?{name:t,options:e}:t),this},t.prototype.useQuantizer=function(t,e){return this._opts.quantizer=e?{name:t,options:e}:t,this},t.prototype.build=function(){return new o.default(this._src,this._opts)},t.prototype.getPalette=function(t){return this.build().getPalette(t)},t.prototype.getSwatches=function(t){return this.build().getPalette(t)},t}();e.default=s},function(t,e,i){var r,o=this&&this.__extends||(r=function(t,e){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])},r(t,e)},function(t,e){function i(){this.constructor=t}r(t,e),t.prototype=null===e?Object.create(e):(i.prototype=e.prototype,new i)});Object.defineProperty(e,"__esModule",{value:!0});var n=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return o(e,t),e.prototype._initCanvas=function(){var t=this.image,e=this._canvas=document.createElement("canvas"),i=e.getContext("2d");if(!i)throw new ReferenceError("Failed to create canvas context");this._context=i,e.className="@vibrant/canvas",e.style.display="none",this._width=e.width=t.width,this._height=e.height=t.height,i.drawImage(t,0,0),document.body.appendChild(e)},e.prototype.load=function(t){var e,i,r,o,n,s,a,l=this;if("string"==typeof t)e=document.createElement("img"),i=t,(a=new URL(i,location.href)).protocol===location.protocol&&a.host===location.host&&a.port===location.port||(r=window.location.href,o=i,n=new URL(r),s=new URL(o),n.protocol===s.protocol&&n.hostname===s.hostname&&n.port===s.port)||(e.crossOrigin="anonymous"),e.src=i;else{if(!(t instanceof HTMLImageElement))return Promise.reject(new Error("Cannot load buffer as an image in browser"));e=t,i=t.src}return this.image=e,new Promise((function(t,r){var o=function(){l._initCanvas(),t(l)};e.complete?o():(e.onload=o,e.onerror=function(t){return r(new Error("Fail to load image: "+i))})}))},e.prototype.clear=function(){this._context.clearRect(0,0,this._width,this._height)},e.prototype.update=function(t){this._context.putImageData(t,0,0)},e.prototype.getWidth=function(){return this._width},e.prototype.getHeight=function(){return this._height},e.prototype.resize=function(t,e,i){var r=this,o=r._canvas,n=r._context,s=r.image;this._width=o.width=t,this._height=o.height=e,n.scale(i,i),n.drawImage(s,0,0)},e.prototype.getPixelCount=function(){return this._width*this._height},e.prototype.getImageData=function(){return this._context.getImageData(0,0,this._width,this._height)},e.prototype.remove=function(){this._canvas&&this._canvas.parentNode&&this._canvas.parentNode.removeChild(this._canvas)},e}(i(2).ImageBase);e.default=n},function(t,e,i){var r=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}},o=i(5),n=r(i(11));o.use(n.default),t.exports=o},function(t,e,i){var r=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});var o=r(i(12)),n=r(i(16)),s=(new(i(17).BasicPipeline)).filter.register("default",(function(t,e,i,r){return r>=125&&!(t>250&&e>250&&i>250)})).quantizer.register("mmcq",o.default).generator.register("default",n.default);e.default=s},function(t,e,i){var r=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});var o=i(3),n=r(i(13)),s=r(i(15));function a(t,e){for(var i=t.size();t.size()0))break;var o=r.split(),n=o[0],s=o[1];if(t.push(n),s&&s.count()>0&&t.push(s),t.size()===i)break;i=t.size()}}e.default=function(t,e){if(0===t.length||e.colorCount<2||e.colorCount>256)throw new Error("Wrong MMCQ parameters");var i=n.default.build(t);i.histogram.colorCount;var r=new s.default((function(t,e){return t.count()-e.count()}));r.push(i),a(r,.75*e.colorCount);var l=new s.default((function(t,e){return t.count()*t.volume()-e.count()*e.volume()}));return l.contents=r.contents,a(l,e.colorCount-l.size()),function(t){for(var e=[];t.size();){var i=t.pop(),r=i.avg();r[0],r[1],r[2],e.push(new o.Swatch(r,i.count()))}return e}(l)}},function(t,e,i){var r=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});var o=r(i(14)),n=function(){function t(t,e,i,r,o,n,s){this.histogram=s,this._volume=-1,this._count=-1,this.dimension={r1:t,r2:e,g1:i,g2:r,b1:o,b2:n}}return t.build=function(e){var i=new o.default(e,{sigBits:5});return new t(i.rmin,i.rmax,i.gmin,i.gmax,i.bmin,i.bmax,i)},t.prototype.invalidate=function(){this._volume=this._count=-1,this._avg=null},t.prototype.volume=function(){if(this._volume<0){var t=this.dimension,e=t.r1,i=t.r2,r=t.g1,o=t.g2,n=t.b1,s=t.b2;this._volume=(i-e+1)*(o-r+1)*(s-n+1)}return this._volume},t.prototype.count=function(){if(this._count<0){for(var t=this.histogram,e=t.hist,i=t.getColorIndex,r=this.dimension,o=r.r1,n=r.r2,s=r.g1,a=r.g2,l=r.b1,c=r.b2,u=0,h=o;h<=n;h++)for(var p=s;p<=a;p++)for(var d=l;d<=c;d++)u+=e[i(h,p,d)];this._count=u}return this._count},t.prototype.clone=function(){var e=this.histogram,i=this.dimension;return new t(i.r1,i.r2,i.g1,i.g2,i.b1,i.b2,e)},t.prototype.avg=function(){if(!this._avg){var t=this.histogram,e=t.hist,i=t.getColorIndex,r=this.dimension,o=r.r1,n=r.r2,s=r.g1,a=r.g2,l=r.b1,c=r.b2,u=0,h=void 0,p=void 0,d=void 0;h=p=d=0;for(var m=o;m<=n;m++)for(var g=s;g<=a;g++)for(var f=l;f<=c;f++){var v=e[i(m,g,f)];u+=v,h+=v*(m+.5)*8,p+=v*(g+.5)*8,d+=v*(f+.5)*8}this._avg=u?[~~(h/u),~~(p/u),~~(d/u)]:[~~(8*(o+n+1)/2),~~(8*(s+a+1)/2),~~(8*(l+c+1)/2)]}return this._avg},t.prototype.contains=function(t){var e=t[0],i=t[1],r=t[2],o=this.dimension,n=o.r1,s=o.r2,a=o.g1,l=o.g2,c=o.b1,u=o.b2;return i>>=3,r>>=3,(e>>=3)>=n&&e<=s&&i>=a&&i<=l&&r>=c&&r<=u},t.prototype.split=function(){var t=this.histogram,e=t.hist,i=t.getColorIndex,r=this.dimension,o=r.r1,n=r.r2,s=r.g1,a=r.g2,l=r.b1,c=r.b2,u=this.count();if(!u)return[];if(1===u)return[this.clone()];var h,p,d=n-o+1,m=a-s+1,g=c-l+1,f=Math.max(d,m,g),v=null;h=p=0;var _=null;if(f===d){_="r",v=new Uint32Array(n+1);for(var y=o;y<=n;y++){h=0;for(var b=s;b<=a;b++)for(var w=l;w<=c;w++)h+=e[i(y,b,w)];p+=h,v[y]=p}}else if(f===m)for(_="g",v=new Uint32Array(a+1),b=s;b<=a;b++){for(h=0,y=o;y<=n;y++)for(w=l;w<=c;w++)h+=e[i(y,b,w)];p+=h,v[b]=p}else for(_="b",v=new Uint32Array(c+1),w=l;w<=c;w++){for(h=0,y=o;y<=n;y++)for(b=s;b<=a;b++)h+=e[i(y,b,w)];p+=h,v[w]=p}for(var x=-1,k=new Uint32Array(v.length),S=0;Sp/2&&(x=S),k[S]=p-$}var P=this;return function(t){var e=t+"1",i=t+"2",r=P.dimension[e],o=P.dimension[i],n=P.clone(),s=P.clone(),a=x-r,l=o-x;for(a<=l?(o=Math.min(o-1,~~(x+l/2)),o=Math.max(0,o)):(o=Math.max(r,~~(x-1-a/2)),o=Math.min(P.dimension[i],o));!v[o];)o++;for(var c=k[o];!c&&v[o-1];)c=k[--o];return n.dimension[i]=o,s.dimension[e]=o+1,[n,s]}(_)},t}();e.default=n},function(t,e,i){Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){this.pixels=t,this.opts=e;var i=e.sigBits,r=function(t,e,r){return(t<<2*i)+(e<>=d,h>>=d,p>>=d)]+=1,u>o&&(o=u),us&&(s=h),hl&&(l=p),p0?t+1:t}),0),this.hist=m,this.rmax=o,this.rmin=n,this.gmax=s,this.gmin=a,this.bmax=l,this.bmin=c}return Object.defineProperty(t.prototype,"colorCount",{get:function(){return this._colorCount},enumerable:!1,configurable:!0}),t}();e.default=r},function(t,e,i){Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t){this._comparator=t,this.contents=[],this._sorted=!1}return t.prototype._sort=function(){this._sorted||(this.contents.sort(this._comparator),this._sorted=!0)},t.prototype.push=function(t){this.contents.push(t),this._sorted=!1},t.prototype.peek=function(t){return this._sort(),t="number"==typeof t?t:this.contents.length-1,this.contents[t]},t.prototype.pop=function(){return this._sort(),this.contents.pop()},t.prototype.size=function(){return this.contents.length},t.prototype.map=function(t){return this._sort(),this.contents.map(t)},t}();e.default=r},function(t,e,i){Object.defineProperty(e,"__esModule",{value:!0});var r=i(3),o=i(4),n={targetDarkLuma:.26,maxDarkLuma:.45,minLightLuma:.55,targetLightLuma:.74,minNormalLuma:.3,targetNormalLuma:.5,maxNormalLuma:.7,targetMutesSaturation:.3,maxMutesSaturation:.4,targetVibrantSaturation:1,minVibrantSaturation:.35,weightSaturation:3,weightLuma:6.5,weightPopulation:.5};function s(t,e,i,r,o,n,s,a,l,c){var u=null,h=0;return e.forEach((function(e){var p=e.hsl,d=p[1],m=p[2];if(d>=a&&d<=l&&m>=o&&m<=n&&!function(t,e){return t.Vibrant===e||t.DarkVibrant===e||t.LightVibrant===e||t.Muted===e||t.DarkMuted===e||t.LightMuted===e}(t,e)){var g=function(t,e,i,r,o,n,s){function a(t,e){return 1-Math.abs(t-e)}return function(){for(var t=[],e=0;eh)&&(u=e,h=g)}})),u}e.default=function(t,e){e=Object.assign({},n,e);var i=function(t){var e=0;return t.forEach((function(t){e=Math.max(e,t.population)})),e}(t),a=function(t,e,i){var r={Vibrant:null,DarkVibrant:null,LightVibrant:null,Muted:null,DarkMuted:null,LightMuted:null};return r.Vibrant=s(r,t,e,i.targetNormalLuma,i.minNormalLuma,i.maxNormalLuma,i.targetVibrantSaturation,i.minVibrantSaturation,1,i),r.LightVibrant=s(r,t,e,i.targetLightLuma,i.minLightLuma,1,i.targetVibrantSaturation,i.minVibrantSaturation,1,i),r.DarkVibrant=s(r,t,e,i.targetDarkLuma,0,i.maxDarkLuma,i.targetVibrantSaturation,i.minVibrantSaturation,1,i),r.Muted=s(r,t,e,i.targetNormalLuma,i.minNormalLuma,i.maxNormalLuma,i.targetMutesSaturation,0,i.maxMutesSaturation,i),r.LightMuted=s(r,t,e,i.targetLightLuma,i.minLightLuma,1,i.targetMutesSaturation,0,i.maxMutesSaturation,i),r.DarkMuted=s(r,t,e,i.targetDarkLuma,0,i.maxDarkLuma,i.targetMutesSaturation,0,i.maxMutesSaturation,i),r}(t,i,e);return function(t,e,i){if(!t.Vibrant&&!t.DarkVibrant&&!t.LightVibrant){if(!t.DarkVibrant&&t.DarkMuted){var n=t.DarkMuted.hsl,s=n[0],a=n[1],l=n[2];l=i.targetDarkLuma,t.DarkVibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.LightVibrant&&t.LightMuted){var c=t.LightMuted.hsl;s=c[0],a=c[1],l=c[2],l=i.targetDarkLuma,t.DarkVibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}}if(!t.Vibrant&&t.DarkVibrant){var u=t.DarkVibrant.hsl;s=u[0],a=u[1],l=u[2],l=i.targetNormalLuma,t.Vibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}else if(!t.Vibrant&&t.LightVibrant){var h=t.LightVibrant.hsl;s=h[0],a=h[1],l=h[2],l=i.targetNormalLuma,t.Vibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.DarkVibrant&&t.Vibrant){var p=t.Vibrant.hsl;s=p[0],a=p[1],l=p[2],l=i.targetDarkLuma,t.DarkVibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.LightVibrant&&t.Vibrant){var d=t.Vibrant.hsl;s=d[0],a=d[1],l=d[2],l=i.targetLightLuma,t.LightVibrant=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.Muted&&t.Vibrant){var m=t.Vibrant.hsl;s=m[0],a=m[1],l=m[2],l=i.targetMutesSaturation,t.Muted=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.DarkMuted&&t.DarkVibrant){var g=t.DarkVibrant.hsl;s=g[0],a=g[1],l=g[2],l=i.targetMutesSaturation,t.DarkMuted=new r.Swatch(o.hslToRgb(s,a,l),0)}if(!t.LightMuted&&t.LightVibrant){var f=t.LightVibrant.hsl;s=f[0],a=f[1],l=f[2],l=i.targetMutesSaturation,t.LightMuted=new r.Swatch(o.hslToRgb(s,a,l),0)}}(a,0,e),a}},function(t,e,i){var r=this&&this.__awaiter||function(t,e,i,r){return new(i||(i=Promise))((function(o,n){function s(t){try{l(r.next(t))}catch(t){n(t)}}function a(t){try{l(r.throw(t))}catch(t){n(t)}}function l(t){var e;t.done?o(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(s,a)}l((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var i,r,o,n,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return n={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(n[Symbol.iterator]=function(){return this}),n;function a(n){return function(a){return function(n){if(i)throw new TypeError("Generator is already executing.");for(;s;)try{if(i=1,r&&(o=2&n[0]?r.return:n[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,n[1])).done)return o;switch(r=0,o&&(n=[2&n[0],o.value]),n[0]){case 0:case 1:o=n;break;case 4:return s.label++,{value:n[1],done:!1};case 5:s.label++,r=n[1],n=[0];continue;case 7:n=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==n[0]&&2!==n[0])){s=0;continue}if(3===n[0]&&(!o||n[1]>o[0]&&n[1]{const r=[t,e,i].map((t=>{let e=t;return e/=255,e<=.03928?e/12.92:((e+.055)/1.055)**2.4}));return.2126*r[0]+.7152*r[1]+.0722*r[2]},ge=(t,e)=>Math.round(100*(((t,e)=>{const i=me(...t),r=me(...e);return(Math.max(i,r)+.05)/(Math.min(i,r)+.05)})(t,e)+Number.EPSILON))/100;de._pipeline.generator.register("default",(t=>{t.sort(((t,e)=>e.population-t.population));const e=t[0];let i;const r=new Map,o=(t,i)=>(r.has(t)||r.set(t,ge(e.rgb,i)),r.get(t)>4.5);for(let e=1;e150)&&o(e.hex,e.rgb)){i=e.rgb;break}}}return void 0===i&&(i=e.getYiq()<200?[255,255,255]:[0,0,0]),[new e.constructor(i,0).hex,e.hex]}));customElements.get("ha-slider")||customElements.define("ha-slider",class extends(customElements.get("paper-slider")){}),customElements.get("ha-icon-button")||customElements.define("ha-icon-button",class extends(customElements.get("paper-icon-button")){}),customElements.get("ha-icon")||customElements.define("ha-icon",class extends(customElements.get("iron-icon")){});const fe={en:{placeholder:{tts:"Text to speech"},label:{leave:"Leave",ungroup:"Ungroup",group_all:"Group all",send:"Send",master:"Master"},state:{idle:"Idle",unavailable:"Unavailable"},title:{speaker_management:"Group management"}},de:{placeholder:{tts:"Text zum Sprechen"},label:{leave:"Verlassen",ungroup:"Teilen",group_all:"Gruppieren",send:"Senden",master:"Master"},state:{idle:"Pause",unavailable:"Nicht verfügbar"},title:{speaker_management:"Wiedergabe auf"}},fi:{placeholder:{tts:"Teksti puheeksi"},label:{leave:"Jätä",ungroup:"Pura ryhmä",group_all:"Liitä kaikki",send:"Lähetä",master:"Master"},state:{idle:"Tauko",unavailable:"Ei käytettävissä"},title:{speaker_management:"Ryhmän hallinta"}},fr:{placeholder:{tts:"Texte à lire"},label:{leave:"Quitter",ungroup:"Dégrouper",group_all:"Grouper tous",send:"Envoyer"},state:{idle:"Inactif",unavailable:"Indisponible"},title:{speaker_management:"Gestion des groupes"}},he:{placeholder:{tts:"טקסט לדיבור"},label:{leave:"לעזוב",ungroup:"ביטול קבוצה",group_all:"לקבץ את כולם",send:"שליחה",master:"ראשי"},state:{idle:"לא פעיל",unavailable:"לא זמין"},title:{speaker_management:"ניהול קבוצות"}},hu:{placeholder:{tts:"Szövegfelolvasás"},label:{leave:"Kilépés",ungroup:"Összes ki",group_all:"Összes be",send:"Küldés",master:"Forrás"},state:{idle:"Tétlen",unavailable:"Nem elérhető"},title:{speaker_management:"Hangszórók csoportosítása"}},it:{placeholder:{tts:"Conversione testo in voce"},label:{leave:"Lascia",ungroup:"Separa",group_all:"Raggruppa tutti",send:"Invia",master:"Master"},state:{idle:"Inattivo",unavailable:"Non disponibile"},title:{speaker_management:"Gestione gruppo"}},is:{placeholder:{tts:"Texti sem á að segja"},label:{leave:"Yfirgefa",ungroup:"Aðskilja",group_all:"Sameina alla",send:"Senda",master:"Stjórnandi"},state:{idle:"Aðgerðalaus",unavailable:"Ekki tiltækt"},title:{speaker_management:"Stjórnun hópa"}},no:{placeholder:{tts:"Tekst til tale"},label:{leave:"Forlat",ungroup:"Oppløs gruppe",group_all:"Grupper alle",send:"Send",master:"Master"},state:{idle:"Inaktiv",unavailable:"Utilgjengelig"},title:{speaker_management:"Gruppestyring"}},pl:{placeholder:{tts:"Zamień tekst na mowę"},label:{leave:"Opuść",ungroup:"Usuń grupę",group_all:"Grupuj wszystkie",send:"Wyślij",master:"Główny"},state:{idle:"brak aktywności",unavailable:"niedostępny"},title:{speaker_management:"Zarządzanie grupą"}},sv:{placeholder:{tts:"Text till tal"},label:{leave:"Lämna",ungroup:"Avgruppera",group_all:"Gruppera alla",send:"Skicka",master:"Master"},state:{idle:"Inaktiv",unavailable:"Otillgänglig"},title:{speaker_management:"Gruppstyrning"}},uk:{placeholder:{tts:"Текст для відтворення"},label:{leave:"Залишити",ungroup:"Розгрупувати",group_all:"Згрупувати всі",send:"Надіслати",master:"Головний"},state:{idle:"бездіяльність",unavailable:"недоступний"},title:{speaker_management:"Управління групою"}},cz:{placeholder:{tts:"Převeď text na řeč"},label:{leave:"Odejít",ungroup:"Zrušit seskupení",group_all:"Seskupit vše",send:"Poslat",master:"Master"},state:{idle:"Nečinný",unavailable:"Nedostupný"},title:{speaker_management:"Správa skupin"}},ru:{placeholder:{tts:"Преобразование текста в речь"},label:{leave:"Покинуть",ungroup:"Разгруппировать",group_all:"Сгруппировать все",send:"Отправить",master:"Мастер"},state:{idle:"Бездействие",unavailable:"Недоступен"},title:{speaker_management:"Управление группой"}},es:{placeholder:{tts:"Texto a voz"},label:{leave:"Salir",ungroup:"Desagrupar",group_all:"Agrupar todos",send:"Enviar",master:"Maestro"},state:{idle:"Inactivo",unavailable:"No disponible"},title:{speaker_management:"Gestión de grupo"}},zh:{placeholder:{tts:"播放文本"},label:{leave:"退出",ungroup:"取消组合",group_all:"组合全部",send:"发送",master:"主要的"},state:{idle:"空闲",unavailable:"不可用"},title:{speaker_management:"组合管理"}},sk:{placeholder:{tts:"Prevod textu na reč"},label:{leave:"Odísť",ungroup:"Zrušiť zoskupenie",group_all:"Zoskupiť všetky",send:"Poslať",master:"Master"},state:{idle:"Nečinný",unavailable:"Nedostupné"},title:{speaker_management:"Manažment skupiny"}},ca:{placeholder:{tts:"Text a veu"},label:{leave:"Sortir",ungroup:"Desagrupar",group_all:"Agrupar-los tots",send:"Enviar",master:"Mestre"},state:{idle:"Inactiu",unavailable:"No disponible"},title:{speaker_management:"Gestió del grup"}},nl:{placeholder:{tts:"Tekst naar spraak"},label:{leave:"Verlaten",ungroup:"Ontgroeperen",group_all:"Alles groeperen",send:"Verzenden",master:"Master"},state:{idle:"Inactief",unavailable:"Niet beschikbaar"},title:{speaker_management:"Groepsbeheer"}},pt:{placeholder:{tts:"Texto para fala"},label:{leave:"Sair",ungroup:"Desagrupar",group_all:"Agrupar tudo",send:"Enviar",master:"Master"},state:{idle:"Ocioso",unavailable:"Indisponível"},title:{speaker_management:"Gerenciamento de grupo"}},cs:{placeholder:{tts:"Převod textu na řeč"},label:{leave:"Opustit",ungroup:"Zrušit seskupení",group_all:"Seskupit vše",send:"Poslat",master:"Master"},state:{idle:"Nečinný",unavailable:"Nedostupné"},title:{speaker_management:"Správa skupiny"}}},ve=(t,e)=>e.split(".").reduce(((t,e)=>t&&t[e]||null),t),_e=(t,e,i,r="unknown")=>{const o=t.selectedLanguage||t.language,n=o.split("-")[0];return fe[o]&&ve(fe[o],e)||t.resources[o]&&i&&t.resources[o][i]||fe[n]&&ve(fe[n],e)||ve(fe.en,e)||r};let ye=class extends ot{render(){return I` + + + ${this.label} + + `}static get styles(){return it` + :host { + display: flex; + padding: 0.6em 0; + align-items: center; + } + span { + margin-left: 1em; + font-weight: 400; + } + span[disabled] { + opacity: 0.65; + } + `}};t([Y({attribute:!1})],ye.prototype,"checked",void 0),t([Y({attribute:!1})],ye.prototype,"disabled",void 0),t([Y({attribute:!1})],ye.prototype,"label",void 0),ye=t([J("mmp-checkbox")],ye);let be=class extends ot{render(){return I` + + ${this.item.name} ${this.master?I`(${_e(this.hass,"label.master")})`:""} + + `}handleClick(t){t.stopPropagation(),t.preventDefault(),this.disabled||this.dispatchEvent(new CustomEvent("change",{detail:{entity:this.item.entity_id,checked:!this.checked}}))}static get styles(){return it` + .master { + font-weight: 500; + } + `}};t([Y({attribute:!1})],be.prototype,"hass",void 0),t([Y({attribute:!1})],be.prototype,"item",void 0),t([Y({attribute:!1})],be.prototype,"checked",void 0),t([Y({attribute:!1})],be.prototype,"disabled",void 0),t([Y({attribute:!1})],be.prototype,"master",void 0),be=t([J("mmp-group-item")],be);let we=class extends ot{render(){return I` +
+
+ +
+ +
+ `}static get styles(){return it` + :host { + position: relative; + box-sizing: border-box; + margin: 4px; + min-width: 0; + overflow: hidden; + transition: background 0.5s; + border-radius: 4px; + font-weight: 500; + } + :host([raised]) { + background: var(--mmp-button-color); + min-height: calc(var(--mmp-unit) * 0.8); + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + } + :host([color]) { + background: var(--mmp-active-color); + transition: background 0.25s; + opacity: 1; + } + :host([faded]) { + opacity: 0.75; + } + :host([disabled]) { + opacity: 0.25; + pointer-events: none; + } + .container { + height: 100%; + width: 100%; + } + .slot-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 8px; + width: auto; + } + paper-ripple { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + } + `}};we=t([J("mmp-button")],we);let xe=class extends ot{get group(){return this.player.group}get master(){return this.player.master}get isMaster(){return this.player.isMaster}get isGrouped(){return this.player.isGrouped}handleGroupChange(t){const{entity:e,checked:i}=t.detail;this.player.handleGroupChange(t,e,i)}render(){if(!this.visible)return I``;const{group:t,isMaster:e,isGrouped:i}=this,{id:r}=this.player;return I` +
+ ${_e(this.hass,"title.speaker_management")} + ${this.entities.map((t=>this.renderItem(t,r)))} +
+ this.player.handleGroupChange(t,r,!1)}> + ${_e(this.hass,"label.leave")} + + ${i&&e?I` + this.player.handleGroupChange(e,t,!1)}> + ${_e(this.hass,"label.ungroup")} + + `:I``} + this.player.handleGroupChange(t,this.entities.map((t=>t.entity_id)),!0)} + > + ${_e(this.hass,"label.group_all")} + +
+
+ `}renderItem(t,e){const i=t.entity_id;return I` `}static get styles(){return it` + .mmp-group-list { + display: flex; + flex-direction: column; + margin-left: 8px; + margin-bottom: 8px; + } + .mmp-group-list__title { + font-weight: 500; + letter-spacing: 0.1em; + margin: 8px 0 4px; + text-transform: uppercase; + } + .mmp-group-list__buttons { + display: flex; + } + mmp-button { + margin: 8px 8px 0 0; + min-width: 0; + text-transform: uppercase; + text-align: center; + width: 50%; + --mdc-theme-primary: transparent; + } + `}};t([Y({attribute:!1})],xe.prototype,"hass",void 0),t([Y({attribute:!1})],xe.prototype,"entities",void 0),t([Y({attribute:!1})],xe.prototype,"player",void 0),t([Y({attribute:!1})],xe.prototype,"visible",void 0),xe=t([J("mmp-group-list")],xe);customElements.define("mmp-dropdown",class extends ot{static get properties(){return{items:[],label:String,selected:String,id:String,isOpen:Boolean}}get selectedIndex(){return this.items.map((t=>t.id)).indexOf(this.selected)}firstUpdated(){const t=this.shadowRoot.querySelector("#menu"),e=this.shadowRoot.querySelector("#button");t.anchor=e}render(){return I` +
t.stopPropagation()} + ?open=${this.isOpen}> + ${this.icon?I` + + + + `:I` + +
+ + ${this.selected||this.label} + + +
+
+ `} + + ${this.items.map((t=>I` + + ${t.icon?I``:""} + ${t.name?I`${t.name}`:""} + `))} + +
+ `}onChange(t){const{index:e}=t.detail;e!==this.selectedIndex&&this.items[e]&&this.dispatchEvent(new CustomEvent("change",{detail:this.items[e]}))}handleClose(t){t.stopPropagation(),this.isOpen=!1}toggleMenu(){const t=this.shadowRoot.querySelector("#menu");t.open=!t.open,this.isOpen=t.open}static get styles(){return[ae,it` + :host { + display: block; + } + :host([faded]) { + opacity: .75; + } + :host[small] .mmp-dropdown__label { + max-width: 60px; + display: block; + position: relative; + width: auto; + text-transform: initial; + } + :host[full] .mmp-dropdown__label { + max-width: none; + } + .mmp-dropdown { + padding: 0; + display: block; + position: relative; + } + .mmp-dropdown__button { + display: flex; + font-size: 1em; + justify-content: space-between; + align-items: center; + height: calc(var(--mmp-unit) - 4px); + margin: 2px 0; + } + .mmp-dropdown__button.icon { + height: var(--mmp-unit); + margin: 0; + } + .mmp-dropdown__button > div { + display: flex; + flex: 1; + justify-content: space-between; + align-items: center; + height: calc(var(--mmp-unit) - 4px); + max-width: 100%; + } + .mmp-dropdown__label { + text-align: left; + text-transform: none; + } + .mmp-dropdown__icon { + height: auto; + width: calc(var(--mmp-unit) * .6); + min-width: calc(var(--mmp-unit) * .6); + } + mwc-list-item > *:nth-child(2) { + margin-left: 4px; + } + .mmp-dropdown[open] mmp-button ha-icon { + color: var(--mmp-accent-color); + transform: rotate(180deg); + } + .mmp-dropdown[open] mmp-icon-button { + color: var(--mmp-accent-color); + transform: rotate(180deg); + } + .mmp-dropdown[open] mmp-icon-button[focused] { + color: var(--mmp-text-color); + transform: rotate(0deg); + } + `]}});customElements.define("mmp-shortcuts",class extends ot{static get properties(){return{player:{},shortcuts:{}}}get buttons(){return this.shortcuts.buttons}get list(){return this.shortcuts.list}get show(){return!this.shortcuts.hide_when_off||this.player.isActive}get active(){return this.player.getAttribute(this.shortcuts.attribute)}get height(){return this.shortcuts.column_height||36}render(){if(!this.show)return I``;const{active:t}=this,e=this.list?I` + + + `:"",i=this.buttons?I` +
+ ${this.buttons.map((e=>I` + this.handleShortcut(t,e)}> +
+ ${e.icon?I``:""} + ${e.image?I``:""} + ${e.name?I`${e.name}`:""} +
+
`))} +
+ `:"";return I` + ${i} + ${e} + `}handleShortcut(t,e){const{type:i,id:r,data:o}=e||t.detail;if("source"===i)return this.player.setSource(t,r);if("service"===i)return this.player.toggleService(t,r,o);if("script"===i)return this.player.toggleScript(t,r,o);if("sound_mode"===i)return this.player.setSoundMode(t,r);const n={media_content_type:i,media_content_id:r};this.player.setMedia(t,n)}shortcutStyle(t){return{"min-height":`${this.height}px`,...t.cover&&{"background-image":`url(${t.cover})`}}}static get styles(){return[ae,it` + .mmp-shortcuts__buttons { + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + margin-top: 8px; + } + .mmp-shortcuts__button { + min-width: calc(50% - 8px); + flex: 1; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + } + .mmp-shortcuts__button > div { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: .2em 0; + } + .mmp-shortcuts__button > div[align='left'] { + justify-content: flex-start; + } + .mmp-shortcuts__button > div[align='right'] { + justify-content: flex-end; + } + .mmp-shortcuts__button[columns='1'] { + min-width: calc(100% - 8px); + } + .mmp-shortcuts__button[columns='3'] { + min-width: calc(33.33% - 8px); + } + .mmp-shortcuts__button[columns='4'] { + min-width: calc(25% - 8px); + } + .mmp-shortcuts__button[columns='5'] { + min-width: calc(20% - 8px); + } + .mmp-shortcuts__button[columns='6'] { + min-width: calc(16.66% - 8px); + } + .mmp-shortcuts__button > div > span { + line-height: calc(var(--mmp-unit) * .6); + text-transform: initial; + } + .mmp-shortcuts__button > div > ha-icon { + width: calc(var(--mmp-unit) * .6); + height: calc(var(--mmp-unit) * .6); + } + .mmp-shortcuts__button > div > *:nth-child(2) { + margin-left: 4px; + } + .mmp-shortcuts__button > div > img { + height: 24px; + } + `]}});customElements.define("mmp-tts",class extends ot{static get properties(){return{hass:{},config:{},player:{}}}get label(){return _e(this.hass,"placeholder.tts","ui.card.media_player.text_to_speak","Say")}get input(){return this.shadowRoot.getElementById("tts-input")}get message(){return this.input.value}render(){return I` + t.stopPropagation()} + > + + + ${_e(this.hass,"label.send")} + + `}handleTts(t){const{config:e,message:i}=this,r={message:i,entity_id:e.entity_id||this.player.id,..."group"===e.entity_id&&{entity_id:this.player.group},...e.data};switch(e.language&&(r.language=e.language),e.platform){case"alexa":this.hass.callService("notify","alexa_media",{message:i,data:{type:e.type||"tts",...e.data},target:r.entity_id});break;case"sonos":this.hass.callService("script","sonos_say",{sonos_entity:r.entity_id,volume:e.volume||.5,message:i,...e.data});break;case"webos":this.hass.callService("notify",r.entity_id.split(".").slice(-1)[0],{message:i,...e.data});break;case"ga":this.hass.callService("notify","ga_broadcast",{message:i,...e.data});break;case"service":{const[t,o]=(e.data.service||"").split("."),n={[e.data.message_field||"message"]:i,entity_id:r.entity_id,...e.language?{language:r.language}:{},...e.data.service_data||{}};this.hass.callService(t,o,n);break}default:this.hass.callService("tts",`${e.platform}_say`,r)}t.stopPropagation(),this.reset()}reset(){this.input.value=""}static get styles(){return it` + :host { + align-items: center; + margin: 8px 4px 0px; + display: flex; + } + .mmp-tts__input { + cursor: text; + flex: 1; + margin-right: 8px; + } + ha-card[rtl] .mmp-tts__input { + margin-right: auto; + margin-left: 8px; + } + .mmp-tts__button { + margin: 0; + height: 30px; + padding: 0 .4em; + } + `}});var ke=t=>{let e=Math.abs(parseInt(""+t%60,10)),i=Math.abs(parseInt(""+t/60%60,10)),r=Math.abs(parseInt(""+t/3600%24,10));return r=r<10?`0${r}`:r,i=i<10?`0${i}`:i,e=e<10?`0${e}`:e,`${"00"!==r?`${r}:`:""}${i}:${e}`};customElements.define("mmp-progress",class extends ot{static get properties(){return{_player:{},showTime:Boolean,showRemainingTime:Boolean,progress:Number,duration:Number,tracker:{},seekProgress:Number,seekWidth:Number,track:Boolean}}set player(t){this._player=t,this.hasProgress&&this.trackProgress()}get duration(){return this.player.mediaDuration}get player(){return this._player}get hasProgress(){return this.player.hasProgress}get width(){return this.shadowRoot.querySelector(".mmp-progress").offsetWidth}get offset(){return this.getBoundingClientRect().left}get classes(){return at({transiting:!this.seekProgress,seeking:this.seekProgress})}render(){return I` +
t.stopPropagation()} + ?paused=${!this.player.isPlaying}> + ${this.showTime?I` +
+ ${ke(this.seekProgress||this.progress)} +
+ ${this.showTime?I` + + -${ke(this.duration-(this.seekProgress||this.progress))} | + + `:""} + ${ke(this.duration)} +
+
+ `:""} +
+
+ `}progressBarStyle(){return ct({width:(this.seekProgress||this.progress)/this.duration*100+"%"})}trackProgress(){this.progress=this.player.progress,this.tracker||(this.tracker=setInterval((()=>this.trackProgress()),1e3)),this.player.isPlaying||(clearInterval(this.tracker),this.tracker=null)}initSeek(t){const e=t.offsetX||t.touches[0].pageX-this.offset;this.seekWidth=this.width,this.seekProgress=this.calcProgress(e),this.addEventListener("touchmove",this.moveSeek),this.addEventListener("mousemove",this.moveSeek)}resetSeek(){this.seekProgress=null,this.removeEventListener("touchmove",this.moveSeek),this.removeEventListener("mousemove",this.moveSeek)}moveSeek(t){t.preventDefault();const e=t.offsetX||t.touches[0].pageX-this.offset;this.seekProgress=this.calcProgress(e)}handleSeek(t){this.resetSeek();const e=t.offsetX||t.changedTouches[0].pageX-this.offset,i=this.calcProgress(e);this.player.seek(t,i)}disconnectedCallback(){super.disconnectedCallback(),this.resetSeek(),clearInterval(this.tracker),this.tracker=null}connectedCallback(){super.connectedCallback(),this.hasProgress&&this.trackProgress()}calcProgress(t){const e=t/this.seekWidth*this.duration;return Math.min(Math.max(e,.1),this.duration)}static get styles(){return it` + .mmp-progress { + cursor: pointer; + left: 0; right: 0; bottom: 0; + position: absolute; + pointer-events: auto; + min-height: calc(var(--mmp-progress-height) + 10px); + } + .mmp-progress:before { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: var(--mmp-progress-height); + background-color: rgba(100,100,100,.15); + } + .mmp-progress__duration { + left: calc(var(--ha-card-border-radius, 4px) / 2); + right: calc(var(--ha-card-border-radius, 4px) / 2); + bottom: calc(var(--mmp-progress-height) + 6px); + position: absolute; + display: flex; + justify-content: space-between; + font-size: .8em; + padding: 0 6px; + z-index: 2 + } + .mmp-progress__duration__remaining { + opacity: .5; + } + .progress-bar { + height: var(--mmp-progress-height); + bottom: 0; + position: absolute; + width: 0; + transition: height 0; + z-index: 1; + background-color: var(--mmp-accent-color); + } + .progress-bar.seeking { + transition: height .15s ease-out; + height: calc(var(--mmp-progress-height) + 4px); + } + .mmp-progress[paused] .progress-bar { + background-color: var(--disabled-text-color, rgba(150,150,150,.5)); + } + `}});let Se=class extends ot{get source(){return this.player.source}get alternatives(){return this.player.sources.map((t=>({name:t,id:t,type:"source"})))}render(){return I` + + `}handleSource(t){const{id:e}=t.detail;this.player.setSource(t,e)}static get styles(){return it` + :host { + max-width: 120px; + min-width: var(--mmp-unit); + } + :host([full]) { + max-width: none; + } + `}};t([Y({attribute:!1})],Se.prototype,"player",void 0),t([Y({attribute:!1})],Se.prototype,"icon",void 0),Se=t([J("mmp-source-menu")],Se);let $e=class extends ot{constructor(){super(...arguments),this.selected=void 0}get mode(){return this.player.soundMode}get alternatives(){return this.player.soundModes.map((t=>({name:t,id:t,type:"soundMode"})))}render(){return I` + + `}handleChange(t){const{id:e}=t.detail;this.player.setSoundMode(t,e),this.selected=e}static get styles(){return it` + :host { + max-width: 120px; + min-width: var(--mmp-unit); + } + :host([full]) { + max-width: none; + } + `}};t([Y({attribute:!1})],$e.prototype,"player",void 0),t([Y({attribute:!1})],$e.prototype,"icon",void 0),t([K()],$e.prototype,"selected",void 0),$e=t([J("mmp-sound-menu")],$e);customElements.define("mmp-media-controls",class extends ot{static get properties(){return{player:{},config:{},break:Boolean}}get showShuffle(){return!this.config.hide.shuffle&&this.player.supportsShuffle}get showRepeat(){return!this.config.hide.repeat&&this.player.supportsRepeat}get maxVol(){return this.config.max_volume||100}get minVol(){return this.config.min_volume||0}get vol(){return Math.round(100*this.player.vol)}get jumpAmount(){return this.config.jump_amount||10}render(){const{hide:t}=this.config;return I` + ${t.volume?I``:this.renderVolControls(this.player.muted)} + ${this.renderShuffleButton()} + ${this.renderRepeatButton()} + ${t.controls?I``:I` +
+ ${!t.prev&&this.player.supportsPrev?I` + this.player.prev(t)} + .icon=${zt}> + + `:""} + ${this.renderJumpBackwardButton()} + ${this.renderPlayButtons()} + ${this.renderJumpForwardButton()} + ${!t.next&&this.player.supportsNext?I` + this.player.next(t)} + .icon=${Dt}> + + `:""} +
+ `} + `}renderShuffleButton(){return this.showShuffle?I` +
+ this.player.toggleShuffle(t)} + .icon=${Ut} + ?color=${this.player.shuffle}> + + +
+ `:I``}renderRepeatButton(){if(!this.showRepeat)return I``;const t=[Vt.ONE,Vt.ALL].includes(this.player.repeat);return I` +
+ this.player.toggleRepeat(t)} + .icon=${qt[this.player.repeat]} + ?color=${t}> + + +
+ `}renderVolControls(t){const e=this.config.volume_stateless?this.renderVolButtons(t):this.renderVolSlider(t),i=at({"--buttons":this.config.volume_stateless,"mmp-media-controls__volume":!0,flex:!0}),r=!this.config.hide.volume_level;return I` +
+ ${e} + ${r?this.renderVolLevel():""} +
`}renderVolSlider(t){return I` + ${this.renderMuteButton(t)} + t.stopPropagation()} + ?disabled=${t} + min=${this.minVol} max=${this.maxVol} + .value=${100*this.player.vol} + step=${this.config.volume_step||1} + dir=${"ltr"} + ignore-bar-touch pin labeled> + + `}renderVolButtons(t){return I` + ${this.renderMuteButton(t)} + this.player.volumeDown(t)} + .icon=${Ft}> + + + this.player.volumeUp(t)} + .icon=${Gt}> + + + `}renderVolLevel(){return I` + ${this.vol}% + `}renderMuteButton(t){if(!this.config.hide.mute)switch(this.config.replace_mute){case"play":case"play_pause":return I` + this.player.playPause(t)} + .icon=${It[this.player.isPlaying]}> + + + `;case"stop":return I` + this.player.stop(t)} + .icon=${Bt.true}> + + + `;case"play_stop":return I` + this.player.playStop(t)} + .icon=${Bt[this.player.isPlaying]}> + + + `;case"next":return I` + this.player.next(t)} + .icon=${Dt}> + + + `;default:if(!this.player.supportsMute)return;return I` + this.player.toggleMute(t)} + .icon=${jt[t]}> + + + `}}renderPlayButtons(){const{hide:t}=this.config;return I` + ${t.play_pause?I``:this.player.assumedState?I` + this.player.play(t)} + .icon=${It.false}> + + + this.player.pause(t)} + .icon=${It.true}> + + + `:I` + this.player.playPause(t)} + .icon=${It[this.player.isPlaying]}> + + + `} + ${t.play_stop?I``:I` + this.handleStop(t)} + .icon=${t.play_pause?Bt[this.player.isPlaying]:Bt.true}> + + + `} + `}renderJumpForwardButton(){return this.config.hide.jump||!this.player.hasProgress?I``:I` + this.player.jump(t,this.jumpAmount)} + .icon=${Ht}> + + + `}renderJumpBackwardButton(){return this.config.hide.jump||!this.player.hasProgress?I``:I` + this.player.jump(t,-this.jumpAmount)} + .icon=${Wt}> + + + `}handleStop(t){return this.config.hide.play_pause?this.player.playStop(t):this.player.stop(t)}handleVolumeChange(t){const e=parseFloat(t.target.value)/100;this.player.setVolume(t,e)}static get styles(){return[ae,it` + :host { + display: flex; + width: 100%; + justify-content: space-between; + } + .flex { + display: flex; + flex: 1; + justify-content: space-between; + } + ha-slider { + max-width: none; + min-width: 100px; + width: 100%; + --md-sys-color-primary: var(--mmp-accent-color); /* before 2025.10.0 */ + color: var(--primary-text-color); + } + ha-icon-button { + min-width: var(--mmp-unit); + } + .mmp-media-controls__volume { + flex: 100; + max-height: var(--mmp-unit); + align-items: center; + } + .mmp-media-controls__volume.--buttons { + justify-content: left; + } + .mmp-media-controls__media { + margin-right: 0; + margin-left: auto; + justify-content: inherit; + } + .mmp-media-controls__media[flow] { + max-width: none; + justify-content: space-between; + } + .mmp-media-controls__shuffle, + .mmp-media-controls__repeat { + flex: 3; + flex-shrink: 200; + justify-content: center; + } + `]}});customElements.define("mmp-powerstrip",class extends ot{static get properties(){return{hass:{},player:{},config:{},groupVisible:Boolean,idle:Boolean}}get icon(){return this.config.speaker_group.icon||Lt}get showGroupButton(){return this.config.speaker_group.entities.length>0&&!this.config.hide.group_button}get showPowerButton(){return!this.config.hide.power}get powerColor(){return this.player.isActive&&!this.config.hide.power_state}get sourceSize(){return"icon"===this.config.source||this.hasControls||this.idle}get soundSize(){return"icon"===this.config.sound_mode||this.hasControls||this.idle}get hasControls(){return this.player.isActive&&this.config.hide.controls!==this.config.hide.volume}get hasSource(){return this.player.sources.length>0&&!this.config.hide.source}get hasSoundMode(){return this.player.soundModes.length>0&&!this.config.hide.sound_mode}get showLabel(){return!this.config.hide.state_label}render(){return this.player.isUnavailable&&this.showLabel?I` + ${_e(this.hass,"state.unavailable","state.default.unavailable")} + `:I` + ${this.idle?this.renderIdleView:""} + ${this.hasControls?I` `:""} + ${this.hasSource?I` + `:""} + ${this.hasSoundMode?I` + `:""} + ${this.showGroupButton?I` + + `:""} + ${this.showPowerButton?I` this.player.toggle(t)} + ?color=${this.powerColor} + > + + `:""} + `}handleGroupClick(t){t.stopPropagation(),this.dispatchEvent(new CustomEvent("toggleGroupList"))}get renderIdleView(){return this.player.isPaused?I` this.player.playPause(t)}> + + `:this.showLabel?I` ${_e(this.hass,"state.idle","state.media_player.idle")} `:I``}static get styles(){return[ae,it` + :host { + display: flex; + line-height: var(--mmp-unit); + max-height: var(--mmp-unit); + } + :host([flow]) mmp-media-controls { + max-width: unset; + } + mmp-media-controls { + max-width: calc(var(--mmp-unit) * 5); + line-height: initial; + justify-content: flex-end; + } + .group-button { + --mdc-icon-size: calc(var(--mmp-unit) * 0.5); + } + ha-icon-button { + min-width: var(--mmp-unit); + } + `]}});let Pe=class extends ot{constructor(){super(...arguments),this.initial=!0,this.picture=void 0,this.thumbnail="",this.prevThumbnail="",this.edit=!1,this.rtl=!1,this.cardHeight=0,this.foregroundColor="",this.backgroundColor="",this.break=!1}set hass(t){if(!t)return;const e=t.states[this.config.entity];if(this._hass=t,e&&this.entity!==e&&(this.entity=e,this.player=new ne(t,this.config,e),this.rtl=this.computeRTL(t),this.idle=this.player.idle,this.player.trackIdle&&this.updateIdleStatus()),this.config&&this.config.speaker_group&&this.config.speaker_group.group_mgmt_entity){const e=t.states[this.config.speaker_group.group_mgmt_entity];e&&this.groupMgmtEntity!==e&&(this.groupMgmtEntity=e,this.groupMgmtPlayer=new ne(t,this.config,e))}}get hass(){return this._hass}static async getConfigElement(){return await Promise.resolve().then((function(){return Ne})),document.createElement("mini-media-player-editor")}static get styles(){return[ae,se]}set overflow(t){this._overflow!==t&&(this._overflow=t)}get overflow(){return this._overflow}get name(){return this.config.name||this.player.name}setConfig(t){this.config=re(t)}shouldUpdate(t){return void 0===this.break&&this.computeRect(this),t.has("prevThumbnail")&&this.prevThumbnail&&setTimeout((()=>{this.prevThumbnail=""}),1e3),t.has("player")&&"material"===this.config.artwork&&this.setColors(),Zt.some((e=>t.has(e)))&&Boolean(this.player)}firstUpdated(){new Ot((t=>{t.forEach((t=>{window.requestAnimationFrame((()=>{"scroll"===this.config.info&&this.computeOverflow(),this._resizeTimer||(this.computeRect(t),this._resizeTimer=setTimeout((()=>{this._resizeTimer=void 0,this._resizeEntry&&(this.computeRect(this._resizeEntry),this.measureCard())}),250)),this._resizeEntry=t}))}))})).observe(this),setTimeout((()=>this.initial=!1),250),this.edit=this.config.speaker_group.expanded||!1}updated(){"scroll"===this.config.info&&setTimeout((()=>{this.computeOverflow()}),10)}render({config:t}=this){return this.computeArtwork(),I` + this.handlePopup(t)} + artwork=${t.artwork} + content=${this.player.content} + > +
${this.renderBackground()} ${this.renderArtwork()} ${this.renderGradient()}
+
+
+ ${this.renderIcon()} +
${this.renderEntityName()} ${this.renderMediaInfo()}
+ + +
+
+ ${!t.collapse&&this.player.isActive?I` + + + `:""} + + ${t.tts?I` `:""} + > + +
+
+
+ ${this.player.isActive&&this.player.hasProgress?I` + + + `:""} +
+
+ `}computeClasses({config:t}=this){return at({"--responsive":this.break||t.hide.icon,"--initial":this.initial,"--bg":t.background||!1,"--group":t.group,"--more-info":"none"!==t.tap_action.action,"--has-artwork":this.player.hasArtwork&&this.thumbnail,"--flow":t.flow,"--collapse":t.collapse,"--rtl":this.rtl,"--progress":this.player.hasProgress,"--runtime":!t.hide.runtime&&this.player.hasProgress,"--inactive":!this.player.isActive})}renderArtwork(){if(!this.thumbnail||"default"===this.config.artwork)return;const t={backgroundImage:this.thumbnail,backgroundColor:this.backgroundColor||"",width:"material"===this.config.artwork&&this.player.isActive?`${this.cardHeight}px`:"100%"},e={backgroundImage:this.prevThumbnail,width:"material"===this.config.artwork?`${this.cardHeight}px`:""};return I`
+ ${this.prevThumbnail&&I`
`}`}renderGradient(){if("material"!==this.config.artwork)return;const t={backgroundImage:`linear-gradient(to left,\n transparent 0,\n ${this.backgroundColor} ${this.cardHeight}px,\n ${this.backgroundColor} 100%)`};return I`
`}renderBackground(){if(this.config.background)return I` +
+ `}handlePopup(t){t.stopPropagation(),le(this,this._hass,this.config,this.config.tap_action,this.player.id)}renderIcon(){if(this.config.hide.icon)return;if(this.player.isActive&&this.thumbnail&&"default"===this.config.artwork)return I`
+ ${" "} +
`;if(null!=this.config.icon_image)return I`
+ +
`;const t=!this.config.hide.icon_state&&this.player.isActive;return I`
+ +
`}renderEntityName(){if(!this.config.hide.name)return I`
${this.name} ${this.speakerCount()}
`}renderMediaInfo(){if(this.config.hide.info)return;const t=this.player.mediaInfo;return I`
+ ${"scroll"===this.config.info?I`
+
+ ${t.map((t=>I`${t.prefix+t.text}`))} +
+
`:""} + ${t.map((t=>I`${t.prefix+t.text}`))} +
`}speakerCount(){if(this.config.speaker_group.show_group_count){const t=this.groupMgmtPlayer?this.groupMgmtPlayer.groupCount:this.player.groupCount;return t>1?" +"+(t-1):""}}computeStyles(){const{scale:t}=this.config;return ct(Object.assign(Object.assign({},t&&{"--mmp-unit":40*t+"px"}),this.foregroundColor&&this.player.isActive&&{"--mmp-text-color":this.foregroundColor,"--mmp-icon-color":this.foregroundColor,"--mmp-icon-active-color":this.foregroundColor,"--mmp-accent-color":this.foregroundColor,"--secondary-text-color":this.foregroundColor,"--mmp-media-cover-info-color":this.foregroundColor,"--ha-control-color":this.foregroundColor}))}async computeArtwork(){const{picture:t,hasArtwork:e}=this.player;if(e&&t!==this.picture){this.picture=t;const e=await this.player.fetchArtwork();this.thumbnail&&(this.prevThumbnail=this.thumbnail),this.thumbnail=e||`url(${t})`}}measureCard(){var t;const e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector("ha-card");e&&(this.cardHeight=e.offsetHeight)}computeOverflow(){var t;const e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector(".marquee");if(e&&e.parentNode){const t=e.clientWidth>e.parentNode.clientWidth;this.overflow=t&&this.player.isActive?7.5+e.clientWidth/50:void 0}}computeRect(t){if("contentRect"in t){const{left:e,width:i}=t.contentRect;this.break=i+2*e<390}else{const{left:e,width:i}=t.getBoundingClientRect();this.break=i+2*e<390}}computeRTL(t){const e=t.language||"en";return t.translationMetadata.translations[e]&&t.translationMetadata.translations[e].isRTL||!1}toggleGroupList(){this.edit=!this.edit}updateIdleStatus(){var t,e;const i=null===(e=null===(t=this.config)||void 0===t?void 0:t.idle_view)||void 0===e?void 0:e.after;if(!i)return;this._idleTracker&&clearTimeout(this._idleTracker);const r=(Date.now()-new Date(this.player.updatedAt).getTime())/1e3;this._idleTracker=setTimeout((()=>{this.idle=this.player.checkIdleAfter(i),this.player.idle=this.idle,this._idleTracker=void 0}),1e3*(60*i-r))}getCardSize(){return this.config.collapse?1:2}async setColors(){if(this.player.picture!==this.picture){if(!this.player.picture)return this.foregroundColor="",void(this.backgroundColor="");try{[this.foregroundColor,this.backgroundColor]=await(async t=>new de(t,{colorCount:16}).getPalette())(this.player.picture)}catch(t){console.error("Error getting Image Colors",t),this.foregroundColor="",this.backgroundColor=""}}}};t([Y({attribute:!1})],Pe.prototype,"hass",null),t([K()],Pe.prototype,"_overflow",void 0),t([K()],Pe.prototype,"initial",void 0),t([K()],Pe.prototype,"picture",void 0),t([K()],Pe.prototype,"thumbnail",void 0),t([K()],Pe.prototype,"prevThumbnail",void 0),t([K()],Pe.prototype,"edit",void 0),t([K()],Pe.prototype,"rtl",void 0),t([K()],Pe.prototype,"cardHeight",void 0),t([K()],Pe.prototype,"foregroundColor",void 0),t([K()],Pe.prototype,"backgroundColor",void 0),t([K()],Pe.prototype,"config",void 0),t([K()],Pe.prototype,"_hass",void 0),t([K()],Pe.prototype,"entity",void 0),t([K()],Pe.prototype,"player",void 0),t([K()],Pe.prototype,"idle",void 0),t([K()],Pe.prototype,"groupMgmtPlayer",void 0),t([K()],Pe.prototype,"groupMgmtEntity",void 0),t([K()],Pe.prototype,"break",void 0),t([K()],Pe.prototype,"_resizeEntry",void 0),t([K()],Pe.prototype,"_resizeTimer",void 0),t([K()],Pe.prototype,"_idleTracker",void 0),Pe=t([J("mini-media-player")],Pe),window.customCards=window.customCards||[],window.customCards.push({type:"mini-media-player",name:"Mini Media Player",preview:!1,description:"A minimalistic yet customizable media player card"});const Ce=["cover","full-cover","full-cover-fit","material","none"],Me=["icon","full"],Ee=["icon","full"],Te=["short","scroll"],Oe=["play_pause","stop","play_stop","next"],Ae=(t,e=!1)=>{const i=t.map((t=>({name:t,id:t})));return e&&i.push({name:"Default",id:void 0}),i};class Ve extends ot{static get styles(){return[se,it` + .editor-side-by-side { + display: flex; + margin: 16px 0; + } + .editor-side-by-side > * { + flex: 1; + padding-right: 4px; + } + .editor-label { + margin-left: 6px; + font-size: 0.8em; + opacity: 0.75; + } + `]}static get properties(){return{hass:{},_config:{}}}setConfig(t){this._config=Object.assign({},re,t)}get getMediaPlayerEntities(){return Object.keys(this.hass.states).filter((t=>"media_player"===t.substr(0,t.indexOf("."))))}get _group(){return this._config.group||!1}get _volume_stateless(){return this._config.volume_stateless||!1}get _toggle_power(){return this._config.toggle_power||!0}render(){if(!this.hass)return I``;const t=this.getMediaPlayerEntities.map((t=>({name:t,id:t})));return I` +
+
+ Entity (required) + this.valueChanged({target:{configValue:"entity",value:t.id}})} + .items=${t} + .label=${"Select entity"} + .selected=${this._config.entity} + > + + +
+ + + + + +
+ +
+ + + + + + + + + + + +
+ +
+
+ Artwork + this.valueChanged({target:{configValue:"artwork",value:t.id}})} + .items=${Ae(Ce,!0)} + .label=${"Default"} + .selected=${this._config.artwork} + > + +
+
+ Source + this.valueChanged({target:{configValue:"source",value:t.id}})} + .items=${Ae(Me,!0)} + .label=${"Default"} + .selected=${this._config.source} + > + +
+
+ Sound mode + this.valueChanged({target:{configValue:"sound_mode",value:t.id}})} + .items=${Ae(Ee,!0)} + .label=${"Default"} + .selected=${this._config.sound_mode} + > + +
+
+ +
+
+ Info + this.valueChanged({target:{configValue:"info",value:t.id}})} + .items=${Ae(Te,!0)} + .label=${"Default"} + .selected=${this._config.info} + > + +
+ +
+ Replace Mute + this.valueChanged({target:{configValue:"replace_mute",value:t.id}})} + .items=${Ae(Oe,!0)} + .label=${"Default"} + .selected=${this._config.replace_mute} + > + +
+
+ +
+ + + + + +
+ +
+ + + +
+ +
+ Settings for Tap actions, TTS, hiding UI elements, idle view, speaker groups and shortcuts can only be + configured in the code editor +
+
+
+ `}valueChanged(t){if(!this._config||!this.hass)return;const{target:e}=t;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}),((t,e,i={},r={})=>{const o=new Event(e,{bubbles:void 0===r.bubbles||r.bubbles,cancelable:Boolean(r.cancelable),composed:void 0===r.composed||r.composed});o.detail=i,t.dispatchEvent(o)})(this,"config-changed",{config:this._config}))}}customElements.define("mini-media-player-editor",Ve);var Ne=Object.freeze({__proto__:null,default:Ve}); diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/my-cards/my-cards.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/my-cards/my-cards.js new file mode 100644 index 0000000..7c35a8e --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/my-cards/my-cards.js @@ -0,0 +1,495 @@ +const t=(e=["unavailable","unknown"],(t,i)=>e.includes(t,i));var e,i={version:"Version",invalid_configuration:"Invalid configuration",show_warning:"Show Warning",show_error:"Show Error"},n={common:i},s={version:"Versjon",invalid_configuration:"Ikke gyldig konfiguration",show_warning:"Vis advarsel"},o={common:s};const r={en:Object.freeze({__proto__:null,common:i,default:n}),nb:Object.freeze({__proto__:null,common:s,default:o})};function a(t,e="",i=""){const n=(localStorage.getItem("selectedLanguage")||"en").replace(/['"]+/g,"").replace("-","_");let s;try{s=t.split(".").reduce((t,e)=>t[e],r[n])}catch(e){s=t.split(".").reduce((t,e)=>t[e],r.en)}return void 0===s&&(s=t.split(".").reduce((t,e)=>t[e],r.en)),""!==e&&""!==i&&(s=s.replace(e,i)),s} +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */function l(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 +/** + * @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 c="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,d=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},h=`{{lit-${String(Math.random()).slice(2)}}}`,u=`\x3c!--${h}--\x3e`,g=new RegExp(`${h}|${u}`);class p{constructor(t,e){this.parts=[],this.element=e;const i=[],n=[],s=document.createTreeWalker(e.content,133,null,!1);let o=0,r=-1,a=0;const{strings:l,values:{length:c}}=t;for(;a0;){const e=l[a],i=v.exec(e)[2],n=i.toLowerCase()+"$lit$",s=t.getAttribute(n);t.removeAttribute(n);const o=s.split(g);this.parts.push({type:"attribute",index:r,name:i,strings:o}),a+=o.length-1}}"TEMPLATE"===t.tagName&&(n.push(t),s.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(h)>=0){const n=t.parentNode,s=e.split(g),o=s.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},f=t=>-1!==t.index,_=()=>document.createComment(""),v=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function y(t,e){const{element:{content:i},parts:n}=t,s=document.createTreeWalker(i,133,null,!1);let o=w(n),r=n[o],a=-1,l=0;const c=[];let d=null;for(;s.nextNode();){a++;const t=s.currentNode;for(t.previousSibling===d&&(d=null),e.has(t)&&(c.push(t),null===d&&(d=t)),null!==d&&l++;void 0!==r&&r.index===a;)r.index=null!==d?-1:r.index-l,o=w(n,o),r=n[o]}c.forEach(t=>t.parentNode.removeChild(t))}const b=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},w=(t,e=-1)=>{for(let i=e+1;i(...e)=>{const i=t(...e);return x.set(i,!0),i},k=t=>"function"==typeof t&&x.has(t),$={},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 + */ +class M{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=c?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),e=[],i=this.template.parts,n=document.createTreeWalker(t,133,null,!1);let s,o=0,r=0,a=n.nextNode();for(;ot}),E=` ${h} `;class P{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 n=0;n-1||i)&&-1===t.indexOf("--\x3e",s+1);const o=v.exec(t);e+=null===o?t+(i?E:u):t.substr(0,o.index)+o[1]+o[2]+"$lit$"+o[3]+h}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");let e=this.getHTML();return void 0!==C&&(e=C.createHTML(e)),t.innerHTML=e,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 N=t=>null===t||!("object"==typeof t||"function"==typeof t),O=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class V{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 R=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class Y{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(;k(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=I(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 I=t=>t&&(R?{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 z(t){let e=F.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},F.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const n=t.strings.join(h);return i=e.keyString.get(n),void 0===i&&(i=new p(t,t.getTemplateElement()),e.keyString.set(n,i)),e.stringsArray.set(t.strings,i),i}const F=new Map,U=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 B=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,i,n){const s=e[0];if("."===s){return new D(t,e.slice(1),i).parts}if("@"===s)return[new Y(t,e.slice(1),n.eventContext)];if("?"===s)return[new L(t,e.slice(1),i)];return new V(t,e,i).parts}handleTextExpression(t){return new H(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.4.1");const q=(t,...e)=>new P(t,e,"html",B) +/** + * @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 + */,W=(t,e)=>`${t}--${e}`;let X=!0;void 0===window.ShadyCSS?X=!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."),X=!1);const J=t=>e=>{const i=W(e.type,t);let n=F.get(i);void 0===n&&(n={stringsArray:new WeakMap,keyString:new Map},F.set(i,n));let s=n.stringsArray.get(e.strings);if(void 0!==s)return s;const o=e.strings.join(h);if(s=n.keyString.get(o),void 0===s){const i=e.getTemplateElement();X&&window.ShadyCSS.prepareTemplateDom(i,t),s=new p(e,i),n.keyString.set(o,s)}return n.stringsArray.set(e.strings,s),s},Z=["html","svg"],G=new Set,K=(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{Z.forEach(e=>{const i=F.get(W(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)}),y(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=w(s),a=0,l=-1;for(;o.nextNode();){l++;for(o.currentNode===i&&(a=b(e),i.parentNode.insertBefore(e,i));-1!==r&&s[r].index===l;){if(a>0){for(;-1!==r;)s[r].index+=a,r=w(s,r);return}r=w(s,r)}}}(i,r,a.firstChild):a.insertBefore(r,a.firstChild),window.ShadyCSS.prepareTemplateStyles(n,t);const l=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(r,a.firstChild);const t=new Set;t.add(r),y(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const Q={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}},tt=(t,e)=>e!==t&&(e==e||t==t),et={attribute:!0,type:String,converter:Q,reflect:!1,hasChanged:tt};class it 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=et){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)||et}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=tt){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,n=e.converter||Q,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||Q.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=et){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.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){}}it.finalized=!0; +/** + * @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 nt=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:n}=e;return{kind:i,elements:n,finisher(e){window.customElements.define(t,e)}}})(t,e),st=(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 ot(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):st(t,e)}function rt(t){return ot({attribute:!1,hasChanged:null==t?void 0:t.hasChanged})}const at=t=>rt(t) +/** +@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 +*/,lt=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ct=Symbol();class dt{constructor(t,e){if(e!==ct)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(lt?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const ht=(t,...e)=>{const i=e.reduce((e,i,n)=>e+(t=>{if(t instanceof dt)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 dt(i,ct)}; +/** + * @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.5.1");const ut={};class gt extends it{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&&!lt){const e=Array.prototype.slice.call(t.cssRules).reduce((t,e)=>t+e.cssText,"");return new dt(String(e),ct)}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(this.constructor.shadowRootOptions)}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?lt?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!==ut&&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 ut}}gt.finalized=!0,gt.render=(t,e,i)=>{if(!i||"object"!=typeof i||!i.scopeName)throw new Error("The `scopeName` option is required.");const n=i.scopeName,s=U.has(e),o=X&&11===e.nodeType&&!!e.host,r=o&&!G.has(n),a=r?document.createDocumentFragment():e;if(((t,e,i)=>{let n=U.get(e);void 0===n&&(d(e,e.firstChild),U.set(e,n=new H(Object.assign({templateFactory:z},i))),n.appendInto(e)),n.setValue(t),n.commit()})(t,a,Object.assign({templateFactory:J(n)},i)),r){const t=U.get(a);U.delete(a);const i=t.value instanceof M?t.value.template:void 0;K(n,a,i),d(e,e.firstChild),e.appendChild(a),U.set(e,t)}!s&&o&&window.ShadyCSS.styleElement(e.host)},gt.shadowRootOptions={mode:"open"};var pt=/d{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|ZZ|Z|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g,mt="[^\\s]+",ft=/\[([^]*?)\]/gm;function _t(t,e){for(var i=[],n=0,s=t.length;n-1?n:null}};function yt(t){for(var e=[],i=1;i3?0:(t-t%10!=10?1:0)*t%10]}},kt=yt({},St),$t=function(t,e){for(void 0===e&&(e=2),t=String(t);t.length0?"-":"+")+$t(100*Math.floor(Math.abs(e)/60)+Math.abs(e)%60,4)},Z:function(t){var e=t.getTimezoneOffset();return(e>0?"-":"+")+$t(Math.floor(Math.abs(e)/60),2)+":"+$t(Math.abs(e)%60,2)}},Mt=function(t){return+t-1},Ct=[null,"[1-9]\\d?"],Et=[null,mt],Pt=["isPm",mt,function(t,e){var i=t.toLowerCase();return i===e.amPm[0]?0:i===e.amPm[1]?1:null}],Nt=["timezoneOffset","[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z?",function(t){var e=(t+"").match(/([+-]|\d\d)/gi);if(e){var i=60*+e[1]+parseInt(e[2],10);return"+"===e[0]?i:-i}return 0}],Ot=(vt("monthNamesShort"),vt("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 Vt,At,Ht=function(t,e,i){if(void 0===e&&(e=Ot.default),void 0===i&&(i={}),"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 n=[];e=(e=Ot[e]||e).replace(ft,(function(t,e){return n.push(e),"@@@"}));var s=yt(yt({},kt),i);return(e=e.replace(pt,(function(e){return Tt[e](t,s)}))).replace(/@@@/g,(function(){return n.shift()}))};(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 Lt(t,e,i){if(e.has("config")||i)return!0;if(t.config.entity){var n=e.get("hass");return!n||n.states[t.config.entity]!==t.hass.states[t.config.entity]}return!1}(At=Vt||(Vt={})).language="language",At.system="system",At.comma_decimal="comma_decimal",At.decimal_comma="decimal_comma",At.space_comma="space_comma",At.none="none";var Dt=!1;if("undefined"!=typeof window){var jt={get passive(){Dt=!0}};window.addEventListener("testPassive",null,jt),window.removeEventListener("testPassive",null,jt)}var Rt="undefined"!=typeof window&&window.navigator&&window.navigator.platform&&(/iP(ad|hone|od)/.test(window.navigator.platform)||"MacIntel"===window.navigator.platform&&window.navigator.maxTouchPoints>1),Yt=[],It=!1,zt=-1,Ft=void 0,Ut=void 0,Bt=void 0,qt=function(t){return Yt.some((function(e){return!(!e.options.allowTouchMove||!e.options.allowTouchMove(t))}))},Wt=function(t){var e=t||window.event;return!!qt(e.target)||(e.touches.length>1||(e.preventDefault&&e.preventDefault(),!1))},Xt=function(t,e){if(t){if(!Yt.some((function(e){return e.targetElement===t}))){var i={targetElement:t,options:e||{}};Yt=[].concat(function(t){if(Array.isArray(t)){for(var e=0,i=Array(t.length);e=n&&(document.body.style.top=-(e+t))}))}),300)}})):function(t){if(void 0===Bt){var e=!!t&&!0===t.reserveScrollBarGap,i=window.innerWidth-document.documentElement.clientWidth;if(e&&i>0){var n=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"),10);Bt=document.body.style.paddingRight,document.body.style.paddingRight=n+i+"px"}}void 0===Ft&&(Ft=document.body.style.overflow,document.body.style.overflow="hidden")}(e),Rt&&(t.ontouchstart=function(t){1===t.targetTouches.length&&(zt=t.targetTouches[0].clientY)},t.ontouchmove=function(e){1===e.targetTouches.length&&function(t,e){var i=t.targetTouches[0].clientY-zt;!qt(t.target)&&(e&&0===e.scrollTop&&i>0||function(t){return!!t&&t.scrollHeight-t.scrollTop<=t.clientHeight}(e)&&i<0?Wt(t):t.stopPropagation())}(e,t)},It||(document.addEventListener("touchmove",Wt,Dt?{passive:!1}:void 0),It=!0))}}else console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.")},Jt=function(t){t?(Yt=Yt.filter((function(e){return e.targetElement!==t})),Rt&&(t.ontouchstart=null,t.ontouchmove=null,It&&0===Yt.length&&(document.removeEventListener("touchmove",Wt,Dt?{passive:!1}:void 0),It=!1)),Rt?function(){if(void 0!==Ut){var t=-parseInt(document.body.style.top,10),e=-parseInt(document.body.style.left,10);document.body.style.position=Ut.position,document.body.style.top=Ut.top,document.body.style.left=Ut.left,window.scrollTo(e,t),Ut=void 0}}():(void 0!==Bt&&(document.body.style.paddingRight=Bt,Bt=void 0),void 0!==Ft&&(document.body.style.overflow=Ft,Ft=void 0))):console.error("enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.")};console.info(`%c ---- MY-SLIDER ---- \n%c ${a("common.version")} 3.0.7 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: green"),window.customCards=window.customCards||[],window.customCards.push({type:"my-slider",name:"Slider Card",description:"Custom Slider Card for Lovelace."});let Zt=class extends gt{static getStubConfig(){return{}}static get properties(){return{hass:{},config:{},active:{}}}setConfig(t){const e=["input_number","number","light","media_player","cover","fan","switch","lock"];if(!t.entity)throw new Error("You need to define entity");if(!e.includes(t.entity.split(".")[0]))throw new Error("Entity has to be one of the following: "+e.map(t=>" "+t));this.config=Object.assign({name:"MySlider",disabled_scroll:!1},t)}shouldUpdate(t){return!!this.config&&Lt(this,t,!1)}render(){var t,e=JSON.parse(JSON.stringify(this.config));const i=this.config.entity?this.config.entity:"ERROR: NO ENTITY ID",n=(null===(t=this.config.entity)||void 0===t||t.split(".")[1],this.hass.states[""+i]);var s=e.step?e.step:"1",o=e.minBar?e.minBar:0,r=e.maxBar?e.maxBar:100,a=e.minSet?e.minSet:0,l=e.maxSet?e.maxSet:100;(i.includes("input_number.")||i.includes("number."))&&(s=e.step?e.step:n.attributes.step,a=e.minSet?e.minSet:n.attributes.min,l=e.maxSet?e.maxSet:n.attributes.max);var c=e.width?e.width:"100%",d=e.height?e.height:"50px",h=e.radius?e.radius:"4px",u=e.top?e.top:"0px",g=e.bottom?e.bottom:"0px",p=e.right?e.right:"0px",m=e.left?e.left:"0px",f=e.rotate?e.rotate:"0",_=e.containerHeight?e.containerHeight:d;"0"!=f&&(f+="deg");var v=e.mainSliderColor?e.mainSliderColor:"var(--accent-color)",y=e.secondarySliderColor?e.secondarySliderColor:"#4d4d4d",b=e.mainSliderColorOff?e.mainSliderColorOff:"#636363",w=e.secondarySliderColorOff?e.secondarySliderColorOff:"#4d4d4d",x=e.border?e.border:"0",S=e.thumbWidth?e.thumbWidth:"25px",k=e.thumbHeight?e.thumbHeight:"80px",$=e.thumbColor?e.thumbColor:"#FFFFFF",T=e.thumbColorOff?e.thumbColorOff:"#969696",M=e.thumbHorizontalPadding?e.thumbHorizontalPadding:"10px",C=e.thumbVerticalPadding?e.thumbVerticalPadding:"20px",E=e.thumpTop?e.thumpTop:"calc((var(--slider-width) - var(--thumb-height)) / 2)",P=e.thumbBorderRight?e.thumbBorderRight:"var(--thumb-horizontal-padding) solid var(--slider-main-color)",N=e.thumbBorderLeft?e.thumbBorderLeft:"var(--thumb-horizontal-padding) solid var(--slider-main-color)",O=e.thumbBorderTop?e.thumbBorderTop:"var(--thumb-vertical-padding) solid var(--slider-main-color)",V=e.thumbBorderBotton?e.thumbBorderBotton:"var(--thumb-vertical-padding) solid var(--slider-main-color)",A=!!e.lockTrack&&e.lockTrack,H=`\n\t\t\t--slider-width: ${c};\n\t\t\t--slider-width-inverse: -${c};\n\t\t\t--slider-height: ${d};\n\t\t\t--slider-main-color: ${"off"===n.state||"locked"===n.state||null==n.state?"var(--slider-main-color-off)":"var(--slider-main-color-on)"};\n\t\t\t--slider-main-color-on: ${v};\n\t\t\t--slider-main-color-off: ${b};\n\t\t\t--slider-secondary-color: ${"off"===n.state||"locked"===n.state||null==n.state?"var(--slider-secondary-color-off)":"var(--slider-secondary-color-on)"};\n\t\t\t--slider-secondary-color-on: ${y};\n\t\t\t--slider-secondary-color-off: ${w};\n\t\t\t--slider-radius: ${h};\n\t\t\t--border: ${x};\n\n\t\t\t--thumb-width: ${S};\n\t\t\t--thumb-height: ${k};\n\t\t\t--thumb-color: ${"off"===n.state||null==n.state?"var(--thumb-color-off)":"var(--thumb-color-on)"};\n\t\t\t--thumb-color-on: ${$};\n\t\t\t--thumb-color-off: ${T};\n\t\t\t--thumb-horizontal-padding: ${M};\n\t\t\t--thumb-vertical-padding: ${C};\n\n\t\t\t--rotate: ${f};\n\t\t\t--top: ${u};\n\t\t\t--bottom: ${g};\n\t\t\t--right: ${p};\n\t\t\t--left: ${m};\n\t\t\t--container-height: ${_};\n\t\t\t--thumb-top: ${E};\n\t\t\t--thumb-border-right: ${P};\n\t\t\t--thumb-border-left: ${N};\n\t\t\t--thumb-border-top: ${O};\n\t\t\t--thumb-border-bottom: ${V};\n\t\t\t\n\t\t\t--lock-track-container: ${A?"none":"auto"};\n\t\t`;const L=t=>{i.includes("light.")?"Warmth"==e.function?this._setWarmth(n,t.target,a,l):this._setBrightness(n,t.target,a,l):i.includes("input_number.")||i.includes("number.")?this._setInputNumber(n,t.target,a,l):i.includes("media_player.")?this._setMediaVolume(n,t.target,a,l):i.includes("cover.")?this._setCover(n,t.target,a,l):i.includes("fan.")?this._setFan(n,t.target,a,l):i.includes("switch.")?this._setSwitch(n,t.target,a,l,o,r):i.includes("lock.")&&this._setLock(n,t.target,a,l,o,r)},D=t=>{e.intermediate&&L(t)},j=t=>{e.intermediate||L(t)},R=()=>{this.config.disabled_scroll=!this.config.disabled_scroll,this.config.disabled_scroll?Xt(window):Jt(window)};if(i.includes("light."))return"Warmth"==e.function?q` + +
+ +
+
+ `:q` + +
+ +
+
+ `;if(i.includes("input_number.")||i.includes("number."))return q` + +
+ +
+
+ `;if(i.includes("media_player.")){var Y=0;if(null!=n.attributes.volume_level)Y=Number(100*n.attributes.volume_level);return q` + +
+ +
+
+ `}return i.includes("cover.")?q` + +
+ +
+
+ `:i.includes("fan.")?q` + +
+ +
+
+ `:i.includes("switch.")||i.includes("lock.")?q` + +
+ +
+
+ `:void 0}_setBrightness(t,e,i,n){var s=e.value;s>n?s=n:sn?s=n:sn?s=n:sn?s=n:sn?s=n:sn?s=n:se=>{if(!(e instanceof A)||e instanceof j||"style"!==e.committer.name||e.committer.parts.length>1)throw new Error("The `styleMap` directive must be used in the style attribute and must be the only part in the attribute.");const{committer:i}=e,{style:n}=i.element;let s=Gt.get(e);void 0===s&&(n.cssText=i.strings.join(" "),Gt.set(e,s=new Set)),s.forEach(e=>{e in t||(s.delete(e),-1===e.indexOf("-")?n[e]=null:n.removeProperty(e))});for(const e in t)s.add(e),-1===e.indexOf("-")?n[e]=t[e]:n.setProperty(e,t[e])}),Qt=(t,e={})=>{const i=te[t];return i?Object.assign(Object.assign({},i),e):void console.log(t+": Not found in styles")},te={card:{height:"30px"},container:{width:"100%",height:"100%",position:"relative",overflow:"hidden","border-radius":"5px"},track:{width:"100%",height:"100%",position:"relative",background:"var(--card-background-color)"},progress:{height:"100%",background:"var(--paper-item-icon-active-color)",position:"absolute",width:"0.00%"},thumb:{height:"100%",background:"black",position:"absolute",right:"-5px",width:"10px"}},ee=function(t,e,i){var n,s;for(var o in e=void 0===e?[]:e,i=void 0===i?{}:i,t)t.hasOwnProperty(o)&&(n=o,s=t[o],e.push(n),"object"==typeof s&&null!==s?i=ee(s,e,i):i[e[e.length-1]]=s,e.pop());return i},ie=(t,e=100,i=0)=>t/(e-i)*100,ne=t=>Math.round(100*(t+Number.EPSILON))/100;function se(t,e){if(!oe(t))return oe(e)?e:{};if(!oe(e))return oe(t)?t:{};if(oe(t)&&oe(e)){const i=Object.assign({},t);return Object.keys(e).forEach(n=>{Array.isArray(e[n])?i[n]=e[n].map((e,i)=>t[n]&&oe(t[n][i])&&oe(e)?se(t[n][i],e):e):oe(e[n])?n in t?i[n]=se(t[n],e[n]):Object.assign(i,{[n]:e[n]}):Object.assign(i,{[n]:e[n]})}),i}return{}}function oe(t){return t&&"object"==typeof t&&!Array.isArray(t)}function re(e,i){if(void 0===e)return!1;const n=(s=e.entity_id).substring(0,s.indexOf("."));var s;const o=void 0!==i?i:null==e?void 0:e.state;if(["button","event","input_button","scene"].includes(n))return"unavailable"!==o;if(t(o))return!1;if("off"===o&&"alert"!==n)return!1;switch(n){case"alarm_control_panel":return"disarmed"!==o;case"alert":return"idle"!==o;case"cover":return"closed"!==o;case"device_tracker":case"person":return"not_home"!==o;case"lock":return"locked"!==o;case"media_player":return"standby"!==o;case"vacuum":return!["idle","docked","paused"].includes(o);case"plant":return"problem"===o;case"group":return["on","home","open","locked","problem"].includes(o);case"timer":return"active"===o;case"camera":return"streaming"===o}return!0}const ae=(t,e)=>{if(t===e)return!0;if(t&&e&&"object"==typeof t&&"object"==typeof e){if(t.constructor!==e.constructor)return!1;let i,n;if(Array.isArray(t)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(!ae(t[i],e[i]))return!1;return!0}if(t instanceof Map&&e instanceof Map){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;for(i of t.entries())if(!ae(i[1],e.get(i[0])))return!1;return!0}if(t instanceof Set&&e instanceof Set){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;return!0}if(ArrayBuffer.isView(t)&&ArrayBuffer.isView(e)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(t[i]!==e[i])return!1;return!0}if(t.constructor===RegExp)return t.source===e.source&&t.flags===e.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===e.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===e.toString();const s=Object.keys(t);if(n=s.length,n!==Object.keys(e).length)return!1;for(i=n;0!=i--;)if(!Object.prototype.hasOwnProperty.call(e,s[i]))return!1;for(i=n;0!=i--;){const n=s[i];if(!ae(t[n],e[n]))return!1}return!0}return t!=t&&e!=e},le=(t,e,i)=>{const n=JSON.parse(JSON.stringify(i));return ce(t,e,n)},ce=(t,e,i)=>{if(["number","boolean"].includes(typeof i))return i;if(!i)return i;if("object"==typeof i)return Object.keys(i).forEach(n=>{i[n]=ce(t,e,i[n])}),i;const n=i.trim();if("[[["===n.substring(0,3)&&"]]]"===n.slice(-3)){return de(t,e,n.slice(3,-3))}return i},de=(t,e,i)=>{try{return new Function("states","entity","user","hass","html","'use strict'; "+i).call(t,t.hass.states,e,t.hass.user,t.hass,q)}catch(t){if(t instanceof Error){const e=i.length<=100?i.trim():i.trim().substring(0,98)+"...";throw t.message=`${t.name}: ${t.message} in '${e}'`,t.name="MyCardJSTemplateError",t}console.log("Unexpected error (_evalTemplate)",t)}};console.info(`%c ---- MY-SLIDER-V2 ---- \n%c ${a("common.version")} 3.0.7 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: green"),window.customCards=window.customCards||[],window.customCards.push({type:"my-slider-v2",name:"Slider Card V2",description:"Custom Slider Card V2 for Lovelace."});let he=class extends gt{constructor(){super(...arguments),this.touchInput=!1,this.thumbTapped=!1,this.isSliding=!1,this.clientXLast=0,this.clientYLast=0,this.actionTaken=!1,this.zero=0,this.oldVal=0,this.sliderVal=0,this.sliderValPercent=0,this.initialTransition=""}setSliderValues(t,e,i=!1){this._config.inverse&&!i?(this.sliderVal=this._config.max-t,this.sliderValPercent=100-e):(this.sliderVal=t,this.sliderValPercent=e)}static getStubConfig(){return{}}static get properties(){return{hass:{},config:{},active:{}}}setConfig(t){const e=["light","input_number","number","media_player","cover","fan","switch","input_boolean","lock"];if(!t.entity)throw new Error("You need to define entity");if(!e.includes(t.entity.split(".")[0]))throw new Error("Entity has to be one of the following: "+e.map(t=>" "+t));this.config=Object.assign({name:"MySliderV2"},t)}shouldUpdate(t){return!!this.config&&(void 0!==this._config&&"seekbar"===this._config.mode&&this.entity.state,Lt(this,t,!1))}updated(t){super.updated(t),requestAnimationFrame(()=>{if(void 0===this.sliderEl&&null!==this.shadowRoot){this.sliderEl=this.shadowRoot.querySelector(".my-slider-custom-container");const t=this.sliderEl.querySelector(".my-slider-custom-progress");this.initialTransition=t.style.transition}})}render(){var t,e,i,n,s,o,r,a,l;const c=this.initializeConfig();if(null!==c)return c;const d=[{transition:this._config.vertical?"height 0.2s ease 0s":"width 0.2s ease 0s"}],h=(null===(t=this._config.styles)||void 0===t?void 0:t.progress)?Object.assign(Object.assign({},d),this._config.styles.progress):d,u=ee(null===(e=this._config.styles)||void 0===e?void 0:e.card)?ee(null===(i=this._config.styles)||void 0===i?void 0:i.card):{},g=ee(null===(n=this._config.styles)||void 0===n?void 0:n.container)?ee(null===(s=this._config.styles)||void 0===s?void 0:s.container):{},p=ee(null===(o=this._config.styles)||void 0===o?void 0:o.track)?ee(null===(r=this._config.styles)||void 0===r?void 0:r.track):{},m=ee(h),f=ee(null===(a=this._config.styles)||void 0===a?void 0:a.thumb)?ee(null===(l=this._config.styles)||void 0===l?void 0:l.thumb):{},_=Qt("card",u),v=Qt("container",g),y=Qt("track",p),b=Qt("progress",m),w=Qt("thumb",f);this._config.vertical?(b.height=this.sliderValPercent.toString()+"%",_.height=u.height?u.height:"100%",_.width=u.width?u.width:"30px",b.width=m.width?m.width:"100%",b.right=m.right?m.right:"auto",w.right=f.right?f:"auto",w.width=f.width?f.width:"100%",w.height=f.height?f.height:"10px",this._config.flipped?(b.top=m.top?m.top:"0",w.bottom=f.bottom?f.bottom:"-5px"):(b.bottom=m.bottom?m.bottom:"0",w.top=f.top?f.top:"-5px")):(b.width=this.sliderValPercent.toString()+"%",this._config.flipped&&(b.right=m.right?m.right:"0",w.right=f.right?f.right:"auto",w.left=f.left?f.left:"-5px"));const x=t=>{switch(t.type){case"mousedown":if(this.touchInput)return;S(t);break;case"touchstart":this.touchInput=!0,S(t);break;case"mousemove":if(this.touchInput)return;$(t);break;case"touchmove":this._config.disableScroll&&t.preventDefault(),$(t);break;case"mouseup":case"touchend":case"touchcancel":k(t)}},S=t=>{var e;if(this.actionTaken)return;const i=t.clientX||t.touches[0].clientX,n=t.clientY||t.touches[0].clientY;if(0===this.clientXLast&&(this.clientXLast=i),0===this.clientYLast&&(this.clientYLast=n),this._config.allowTapping)return this.actionTaken=!0,void this.calcProgress(t);{const s=t.composedPath()[0],o=null===(e=this.shadowRoot)||void 0===e?void 0:e.querySelector(".my-slider-custom-thumb");if(s.classList.contains("my-slider-custom-thumb"))return this.thumbTapped=!0,this.actionTaken=!0,void this.calcProgress(t);if(o){const e=o.getBoundingClientRect();if(i>=e.left-this._config.marginOfError&&i<=e.right+this._config.marginOfError&&n>=e.top-this._config.marginOfError&&n<=e.bottom+this._config.marginOfError)return this.thumbTapped=!0,this.actionTaken=!0,void this.calcProgress(t)}}this._config.allowSliding&&(this.actionTaken=!0),this.clientYLast=n,this.clientXLast=i},k=t=>{if(!this.actionTaken)return;this.sliderEl.querySelector(".my-slider-custom-progress").style.transition=this.initialTransition,(this._config.allowTapping||this.thumbTapped||this.isSliding)&&this.calcProgress(t),this.thumbTapped=!1,this.touchInput=!1,this.isSliding=!1,setTimeout(()=>{this.actionTaken=!1},50)},$=t=>{if(this.actionTaken){this.sliderEl.querySelector(".my-slider-custom-progress").style.transition="";const e=t.clientX||t.touches[0].clientX,i=t.clientY||t.touches[0].clientY;this._config.allowTapping||this.isSliding||!this._config.allowTapping&&this.thumbTapped?(this.calcProgress(t),this.clientXLast=e,this.clientYLast=i):this._config.allowSliding&&(this._config.vertical?Math.abs(i-this.clientYLast)>=this._config.slideDistance&&(this.isSliding=!0,this.clientXLast=e,this.clientYLast=i):Math.abs(e-this.clientXLast)>=this._config.slideDistance&&(this.isSliding=!0,this.clientXLast=e,this.clientYLast=i))}};return this.createAndCleanupEventListeners(x),q` + +
+
+
+
+
+
+
+
+ `}initializeConfig(){var t;if(this.actionTaken)return null;this.entity=this.hass.states[""+this.config.entity];try{this._config=le(this,this.entity,this.config)}catch(t){if(t instanceof Error){t.stack?console.error(t.stack):console.error(t);const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t.toString(),origConfig:this.config}),e}console.log("Unexpected error evaluating config on init:",t)}if(!this._config)return q`Error with evaluated _config`;const e=this._config.entity?null===(t=this._config.entity)||void 0===t?void 0:t.split(".")[0]:this._config.entity?this._config.entity.split(".")[0]:"none",i={sliderId:`slider-${this._config.entity.replace(".","-")}-${this._config.mode}`,type:this._config.type,disableScroll:void 0===this._config.disableScroll||this._config.disableScroll,allowTapping:void 0===this._config.allowTapping||this._config.allowTapping,allowSliding:void 0!==this._config.allowSliding&&this._config.allowSliding,marginOfError:void 0!==this._config.marginOfError?this._config.marginOfError:10,slideDistance:void 0!==this._config.slideDistance?this._config.slideDistance:10,showMin:void 0!==this._config.showMin&&this._config.showMin,minThreshold:0,maxThreshold:100,sliderMin:this._config.sliderMin?this._config.sliderMin:0,vertical:void 0!==this._config.vertical&&this._config.vertical,flipped:void 0!==this._config.flipped&&this._config.flipped,inverse:void 0!==this._config.inverse&&this._config.inverse,intermediate:void 0!==this._config.intermediate&&this._config.intermediate,min:this._config.min?this._config.min:0,max:this._config.max?this._config.max:100,step:this._config.step?this._config.step:1,mode:void 0!==this._config.mode?this._config.mode:void 0!==this._config.colorMode?this._config.colorMode:void 0!==this._config.coverMode?this._config.coverMode:"light"===e?"brightness":"cover"===e?"position":"media_player"===e?"volume":"brightness"};let n=0,s=0,o=0,r=!1;switch(e){case"light":if("brightness"===i.mode)this.oldVal=Math.ceil(ie(this.entity.attributes.brightness,256)),"on"===this.entity.state&&(n=Math.ceil(ie(this.entity.attributes.brightness,256)),!i.showMin&&i.min&&(n-=i.min)),n=n*(100-i.sliderMin)/100+i.sliderMin,n=nthis.updateSeekbar(),500);if("playing"!==this.entity.state)return;let t=0;this._config.max=this.entity.attributes.media_duration;const e=new Date,i=new Date(this.entity.attributes.media_position_updated_at);let n=this.entity.attributes.media_position+(e.getTime()-i.getTime())/1e3;n=Math.min(n,this._config.max),t=n,this.setSliderValues(t,ne(ie(t,this._config.max))),this.setProgress(this.sliderEl,Math.round(t),"updateSeekbar"),setTimeout(()=>this.updateSeekbar(),1e3)}calcProgress(t){if(void 0===this.sliderEl||null===this.sliderEl)return;const e=((t,e)=>{let i={x:0,y:0};if("touchstart"==t.type||"touchmove"==t.type||"touchend"==t.type||"touchcancel"==t.type){let e=void 0===t.originalEvent?t:t.originalEvent,n=e.touches[0]||e.changedTouches[0];i.x=n.clientX,i.y=n.clientY}else"mousedown"!=t.type&&"mouseup"!=t.type&&"mousemove"!=t.type&&"mouseover"!=t.type&&"mouseout"!=t.type&&"mouseenter"!=t.type&&"mouseleave"!=t.type||(i.x=t.clientX,i.y=t.clientY);let n=e.getBoundingClientRect(),s=i.x-n.left,o=i.y-n.top;return o=e.offsetHeight-o,{x:s,y:o}})(t,this.sliderEl),i=this.sliderEl.offsetWidth,n=this.sliderEl.offsetHeight,s=(this._config.vertical?ne(e.y/n*100):ne(e.x/i*100))/100*(this._config.max-0),o=this._config.max-s;let r=this._config.flipped?o:s;r=rthis._config.max?this._config.max:r{o.style.transition=this.initialTransition},200)}_setLock(t,e){var i=Math.min(this._config.max,this._config.maxThreshold);if(Number(i)<=e){var n="locked"===t.state?"unlock":"lock";this.hass.callService("lock",n,{entity_id:t.entity_id})}const s=Number(Math.max(this.zero,this._config.minThreshold)),o=ne(ie(s,this._config.max)),r=this.sliderEl.querySelector(".my-slider-custom-progress");this._config.vertical?r.style.transition="height 0.2s ease 0s":r.style.transition="width 0.2s ease 0s",this.setSliderValues(s,o),this.setProgress(this.sliderEl,s,"setLock"),setTimeout(()=>{r.style.transition=this.initialTransition},200)}createAndCleanupEventListeners(t){document.removeEventListener("mouseup",t),document.removeEventListener("touchend",t),document.removeEventListener("touchcancel",t),document.addEventListener("mouseup",t),document.addEventListener("touchend",t),document.addEventListener("touchcancel",t),document.addEventListener("mousemove",t)}static get styles(){return ht` + `}};l([ot()],he.prototype,"_config",void 0),l([ot({attribute:!1})],he.prototype,"hass",void 0),l([at()],he.prototype,"config",void 0),he=l([nt("my-slider-v2")],he);const ue=(t,e,i,n)=>{n=n||{},i=null==i?{}:i;const s=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return s.detail=i,t.dispatchEvent(s),s},ge=1,pe=2,me=3,fe=4,_e=5; +/** + * @license + * Copyright (c) 2021 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 ve{constructor(t){this.type=pe,this.options=t.options,this.legacyPart=t}get parentNode(){return this.legacyPart.startNode.parentNode}get startNode(){return this.legacyPart.startNode}get endNode(){return this.legacyPart.endNode}}class ye{constructor(t){this.legacyPart=t,this.type=t instanceof j?me:ge}get options(){}get name(){return this.legacyPart.committer.name}get element(){return this.legacyPart.committer.element}get strings(){return this.legacyPart.committer.strings}get tagName(){return this.element.tagName}}class be{constructor(t){this.type=fe,this.legacyPart=t}get options(){}get name(){return this.legacyPart.name}get element(){return this.legacyPart.element}get strings(){return this.legacyPart.strings}get tagName(){return this.element.tagName}}class we{constructor(t){this.type=_e,this.legacyPart=t}get options(){}get name(){return this.legacyPart.eventName}get element(){return this.legacyPart.element}get strings(){}get tagName(){return this.element.tagName}handleEvent(t){this.legacyPart.handleEvent(t)}}const xe="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0;class Se extends HTMLElement{constructor(){super(),this.holdTime=500,this.held=!1,this.cancelled=!1,this.isRepeating=!1,this.repeatCount=0}connectedCallback(){Object.assign(this.style,{position:"fixed",width:xe?"100px":"50px",height:xe?"100px":"50px",transform:"translate(-50%, -50%)",pointerEvents:"none",zIndex:"999"}),["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach(t=>{document.addEventListener(t,()=>{this.cancelled=!0,this.timer&&(this.stopAnimation(),clearTimeout(this.timer),this.timer=void 0,this.isRepeating&&this.repeatTimeout&&(clearInterval(this.repeatTimeout),this.isRepeating=!1))},{passive:!0})})}bind(t,e){t.actionHandler&&ae(e,t.actionHandler.options)||(t.actionHandler?(t.removeEventListener("touchstart",t.actionHandler.start),t.removeEventListener("touchend",t.actionHandler.end),t.removeEventListener("touchcancel",t.actionHandler.end),t.removeEventListener("mousedown",t.actionHandler.start),t.removeEventListener("click",t.actionHandler.end),t.removeEventListener("keyup",t.actionHandler.handleEnter)):t.addEventListener("contextmenu",t=>{const e=t||window.event;return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0,e.returnValue=!1,!1}),t.actionHandler={options:e},e.disabled||(t.actionHandler.start=i=>{let n,s;this.cancelled=!1,i.touches?(n=i.touches[0].clientX,s=i.touches[0].clientY):(n=i.clientX,s=i.clientY),e.hasHold&&(this.held=!1,this.timer=window.setTimeout(()=>{this.startAnimation(n,s),this.held=!0,e.repeat&&!this.isRepeating&&(this.repeatCount=0,this.isRepeating=!0,this.repeatTimeout=setInterval(()=>{ue(t,"action",{action:"hold"}),this.repeatCount++,this.repeatTimeout&&e.repeatLimit&&this.repeatCount>=e.repeatLimit&&(clearInterval(this.repeatTimeout),this.isRepeating=!1)},e.repeat))},this.holdTime))},t.actionHandler.end=t=>{if(["touchend","touchcancel"].includes(t.type)&&this.cancelled)return void(this.isRepeating&&this.repeatTimeout&&(clearInterval(this.repeatTimeout),this.isRepeating=!1));const i=t.target;t.cancelable&&t.preventDefault(),e.hasHold&&(clearTimeout(this.timer),this.isRepeating&&this.repeatTimeout&&clearInterval(this.repeatTimeout),this.isRepeating=!1,this.stopAnimation(),this.timer=void 0),e.hasHold&&this.held?e.repeat||ue(i,"action",{action:"hold"}):e.hasDoubleClick?"click"===t.type&&t.detail<2||!this.dblClickTimeout?this.dblClickTimeout=window.setTimeout(()=>{this.dblClickTimeout=void 0,ue(i,"action",{action:"tap"})},250):(clearTimeout(this.dblClickTimeout),this.dblClickTimeout=void 0,ue(i,"action",{action:"double_tap"})):ue(i,"action",{action:"tap"})},t.actionHandler.handleEnter=t=>{13===t.keyCode&&t.currentTarget.actionHandler.end(t)},t.addEventListener("touchstart",t.actionHandler.start,{passive:!0}),t.addEventListener("touchend",t.actionHandler.end),t.addEventListener("touchcancel",t.actionHandler.end),t.addEventListener("mousedown",t.actionHandler.start,{passive:!0}),t.addEventListener("click",t.actionHandler.end),t.addEventListener("keyup",t.actionHandler.handleEnter)))}startAnimation(t,e){Object.assign(this.style,{left:t+"px",top:e+"px",display:null})}stopAnimation(){}}customElements.define("my-card-action-handler",Se);const ke=(t,e)=>{const i=(()=>{const t=document.body;if(t.querySelector("my-card-action-handler"))return t.querySelector("my-card-action-handler");const e=document.createElement("my-card-action-handler");return t.appendChild(e),e})();i&&i.bind(t,e)},$e=function(t){const e=new WeakMap;return S((...i)=>n=>{const s=e.get(n);let o,r;void 0===s?(o=function(t){if(t instanceof H)return new ve(t);if(t instanceof Y)return new we(t);if(t instanceof L)return new be(t);if(t instanceof j||t instanceof A)return new ye(t);throw new Error("Unknown part type")}(n),r=new t(o),e.set(n,[o,r])):(o=s[0],r=s[1]),n.setValue(r.update(o,i)),n.commit()})}(class extends class{constructor(t){}update(t,e){return this.render(...e)}}{update(t,[e]){return ke(t.element,e),$}render(t){}}),Te=(t,e={})=>{const i=Me[t];return i?se(i,e):(console.log(t+": Not found in styles"),e)},Me={card:{height:"125px",width:"100%",background:"var(--card-background-color)",overflow:"hidden",cursor:"pointer",display:"flex","flex-direction":"column"},icon:{"--mdc-icon-size":"100%",height:"35px",width:"35px",display:"inline-block",color:"var(--paper-item-icon-color)","border-radius":"50%",margin:"7px 0px 0px 7px"},stats:{margin:"5px 2px 0px auto",color:"var(--primary-text-color)",display:"inline-block","font-family":'"Arial", sans-serif',"font-size":"11px","text-align":"center","text-shadow":"2px 2px 4px rgba(0, 0, 0, .9)",width:"50px",height:"50px",padding:"0px 3px"},camera:{"border-radius":"3px",overflow:"hidden"},labelContainer:{width:"100%",height:"100%",display:"flex","flex-direction":"column","align-items":"flex-start",overflow:"hidden"},label:{padding:"0",margin:"0 10px",color:"var(--primary-text-color)","font-weight":"bold",cursor:"pointer"},extraText:{margin:"0 10px",color:"var(--primary-text-color)","font-weight":"normal","font-size":"12px","white-space":"nowrap",overflow:"hidden"},row1:{display:"flex","justify-content":"space-between","min-height":"55px"},row2:{},buttonsContainer:{display:"flex","align-items":"flex-start","flex-direction":"row",padding:"0px 5px"},button:{padding:"3px 5px 3px 5px","aspect-ratio":"1 / 1",display:"flex","align-items":"center","justify-content":"center"},buttonText:{padding:"0px",margin:"0px"},buttonIcon:{padding:"0px",margin:"0px",position:"relative","--mdc-icon-size":"100%",display:"flex",height:"20px",width:"20px"},sliderCard:{"border-radius":"0px",background:"transparent","box-shadow":"none",cursor:"default"},sliderContainer:{"border-radius":"0px"},sliderTrack:{background:"transparent"},sliderThumbHor:{height:"20px",width:"3px",top:"6px",right:"2px","border-radius":"50px"},sliderThumbVer:{width:"20px",height:"3px",top:"2px",left:"7px","border-radius":"50px"},sliderProgressHor:{background:"linear-gradient(to top, var(--paper-item-icon-active-color), transparent)"},sliderProgressVer:{background:"linear-gradient(to left, var(--paper-item-icon-active-color), transparent)"},seekbarCard:{"border-radius":"0px",background:"transparent","box-shadow":"none",cursor:"default","margin-left":"30px"},seekbarContainer:{"border-radius":"0px"},seekbarTrack:{background:"transparent"},seekbarThumb:{height:"100%",width:"3px",right:"0px","border-radius":"50px",background:"linear-gradient(to top, var(--paper-item-icon-active-color) -20%, transparent 70%)"},seekbarProgress:{background:"transparent"}};console.info(`%c ---- MY-BUTTON ---- \n%c ${a("common.version")} 1.0.2 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: green"),window.customCards=window.customCards||[],window.customCards.push({type:"my-button",name:"My Button Card",description:"Custom Button Card for Lovelace."});let Ce=class extends gt{constructor(){super(...arguments),this.lastAction=0}static getStubConfig(){return{}}static get properties(){return{hass:{},config:{},active:{}}}setConfig(t){const e=["","light","cover","switch","input_boolean","button","lock","media_player"];if(t.entity&&!e.includes(t.entity.split(".")[0]))throw new Error("Entity has to be one of the following: "+e.map(t=>" "+t));this.config=Object.assign({name:"MyButton"},t)}shouldUpdate(t){return!!this.config&&Lt(this,t,!1)}updated(t){super.updated(t),requestAnimationFrame(()=>{var t;const e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector('[data-container="label-row"]'),i=null==e?void 0:e.querySelector("p");if(e&&i){const t=window.getComputedStyle(i),n=window.getComputedStyle(e),s=e.offsetWidth,o=i.offsetWidth+parseFloat(t.marginLeft)+parseFloat(t.marginRight),r=s-parseFloat(n.paddingLeft)-parseFloat(n.paddingRight);i.style.animation=o +
+
this._handleAction(t,this._config)} + .actionHandler=${$e({hasDoubleClick:"none"!==(null===(e=null===(t=this._config)||void 0===t?void 0:t.double_tap_action)||void 0===e?void 0:e.action),hasHold:"none"!==(null===(n=null===(i=this._config)||void 0===i?void 0:i.hold_action)||void 0===n?void 0:n.action),repeat:null===(o=null===(s=this._config)||void 0===s?void 0:s.hold_action)||void 0===o?void 0:o.repeat,repeatLimit:null===(a=null===(r=this._config)||void 0===r?void 0:r.hold_action)||void 0===a?void 0:a.repeat_limit})}> +
+ ${this.iconElement()} + ${this.statsElement()} +
+
+ ${this.labelElement()} +
+ +
+
+ ${!1===this._config.buttons.vertical?this.buttonsElement():""} +
+
+ ${!1===this._config.slider.vertical?this.sliderElement():""} + ${this._config.seekbar.show?this.seekbarElement():""} +
+
+ ${!0===this._config.buttons.vertical?this.buttonsElement():""} +
+
+ ${!0===this._config.slider.vertical?this.sliderElement():""} +
+
+ + `}iconElement(){var t,e,i,n;return this._config.icon.show?this._config.icon.tap_action||this._config.icon.double_tap_action||this._config.icon.hold_action?q` + this._handleAction(t,this._config.icon)} + .actionHandler=${$e({hasDoubleClick:"none"!==(null===(t=this._config.icon.double_tap_action)||void 0===t?void 0:t.action),hasHold:"none"!==(null===(e=this._config.icon.hold_action)||void 0===e?void 0:e.action),repeat:null===(i=this._config.icon.hold_action)||void 0===i?void 0:i.repeat,repeatLimit:null===(n=this._config.icon.hold_action)||void 0===n?void 0:n.repeat_limit})} /> + `:q` + + `:q``}statsElement(){var t,e,i,n,s,o,r,a;return this._config.stats.show?this._config.stats.tap_action||this._config.stats.double_tap_action||this._config.stats.hold_action?this._config.camera?q` +
this._handleAction(t,this._config.stats)} + .actionHandler=${$e({hasDoubleClick:"none"!==(null===(t=this._config.stats.double_tap_action)||void 0===t?void 0:t.action),hasHold:"none"!==(null===(e=this._config.stats.hold_action)||void 0===e?void 0:e.action),repeat:null===(i=this._config.stats.hold_action)||void 0===i?void 0:i.repeat,repeatLimit:null===(n=this._config.stats.hold_action)||void 0===n?void 0:n.repeat_limit})}> +
+ + ${this._config.stats.text} +
+
+ `:q` +
this._handleAction(t,this._config.stats)} + .actionHandler=${$e({hasDoubleClick:"none"!==(null===(s=this._config.stats.double_tap_action)||void 0===s?void 0:s.action),hasHold:"none"!==(null===(o=this._config.stats.hold_action)||void 0===o?void 0:o.action),repeat:null===(r=this._config.stats.hold_action)||void 0===r?void 0:r.repeat,repeatLimit:null===(a=this._config.stats.hold_action)||void 0===a?void 0:a.repeat_limit})}> + +
+ ${this._config.stats.text} +
+ +
+ `:this._config.camera?q` +
+
+ +
+
+ `:q` +
+
+ ${this._config.stats.text} +
+
+ `:q``}labelElement(){var t,e,i,n;return this._config.label.show?this._config.label.tap_action||this._config.label.double_tap_action||this._config.label.hold_action?q` +
+ + ${this._config.label.extra?q`

${this._config.label.extra}

`:""} + +
+ `:q` +
+ + ${this._config.label.extra?q`

${this._config.label.extra}

`:""} +
+ `:q``}sliderElement(){return this._config.slider.show?q` + + `:q``}buttonsElement(){if(!this._config.buttons.show)return q``;let t=Object.keys(this._config.buttons).filter(t=>t.startsWith("button")).map(t=>{var e,i,n,s;return this._config.buttons[t].show?this._config.buttons[t].tap_action||this._config.buttons[t].double_tap_action||this._config.buttons[t].hold_action?q` +
this._handleAction(e,this._config.buttons[t])} + .actionHandler=${$e({hasDoubleClick:"none"!==(null===(e=this._config.buttons[t].double_tap_action)||void 0===e?void 0:e.action),hasHold:"none"!==(null===(i=this._config.buttons[t].hold_action)||void 0===i?void 0:i.action),repeat:null===(n=this._config.buttons[t].hold_action)||void 0===n?void 0:n.repeat,repeatLimit:null===(s=this._config.buttons[t].hold_action)||void 0===s?void 0:s.repeat_limit})}> + ${this._config.buttons[t].text?q`

${this._config.buttons[t].text}

`:""} + ${this._config.buttons[t].icon?q``:""} +
`:q` +
+ ${this._config.buttons[t].text?q`

${this._config.buttons[t].text}

`:""} + ${this._config.buttons[t].icon?q``:""} +
`:q``});return q` +
+ ${t} + +
+ `}seekbarElement(){return this._config.slider.show?q` + + `:q``}initializeConfig(){var t,e;this.entity=this.hass.states[""+this.config.entity],0===this.lastAction&&(this.lastAction=(new Date).getTime());try{this._config=le(this,this.entity,this.config)}catch(t){if(t instanceof Error){t.stack?console.error(t.stack):console.error(t);const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t.toString(),origConfig:this.config}),e}console.log("Unexpected error evaluating config on init:",t)}if(!this._config)return q`Error with this._config...`;const i=this._config.entity?null===(t=this._config.entity)||void 0===t?void 0:t.split(".")[0]:"none",n={},s={show:!0,icon:"mdi:power"},o={show:!0},r={show:!1},a={show:!1,vertical:!0,styles:{}},l={show:!0,vertical:!1,entity:this._config.entity,allowTapping:!1,marginOfError:10},c={show:!1,entity:this._config.entity};if("none"!==i){const t="cover"===i,d="cover"===i,h=this.entity.state;o.text=this.entity.attributes.friendly_name,r.text=h.charAt(0).toUpperCase()+h.slice(1),l.entity=this.entity.entity_id,l.vertical=t,l.flipped=d,"light"===i?(n.tap_action={action:"toggle"},n.hold_action={action:"more-info"},this.entity.attributes.brightness&&(n.hold_action={action:"more-info"},r.text=Math.ceil(ie(this.entity.attributes.brightness,256))+"%"),r.show=!0,l.allowSliding=!0,l.slideDistance=15):"cover"===i?(n.hold_action={action:"more-info"},s.icon=(null===(e=this.entity.attributes)||void 0===e?void 0:e.current_position)>=50?"mdi:blinds-open":"mdi:blinds"):"switch"===i||"input_boolean"===i?(n.tap_action={action:"toggle"},n.hold_action={action:"more-info"},r.show=!0,l.show=!1,s.icon="on"===this.entity.state?"mdi:power-plug":"mdi:power-plug-off"):"button"===i?(n.tap_action={action:"call-service",service:"button.press",service_data:{entity_id:this.entity.entity_id}},n.hold_action={action:"more-info"},l.show=!1):"lock"===i?(n.hold_action={action:"more-info"},l.show=!1,r.show=!0,this._config.camera&&"string"==typeof this._config.camera&&(this._config.camera=this.hass.states[this._config.camera],r.entity=this._config.camera.entity_id,r.tap_action={action:"more-info"}),"locked"===this.entity.state?(s.icon="mdi:lock-outline",n.tap_action={action:"call-service",service:"lock.unlock",service_data:{entity_id:this.entity.entity_id}}):"unlocked"===this.entity.state&&(s.icon="mdi:lock-open-variant-outline",n.tap_action={action:"call-service",service:"lock.lock",service_data:{entity_id:this.entity.entity_id}})):"media_player"===i&&(n.tap_action={action:"more-info"},"speaker"===this.entity.attributes.device_class?(s.icon=re(this.entity,this.entity.state)?"mdi:speaker":"mdi:speaker-off","playing"===this.entity.state?s.icon="mdi:speaker-play":"paused"===this.entity.state&&(s.icon="mdi:speaker-pause")):"tv"===this.entity.attributes.device_class&&(s.icon=re(this.entity,this.entity.state)?"mdi:television":"mdi:television-off","playing"===this.entity.state?s.icon="mdi:television-play":"paused"===this.entity.state&&(s.icon="mdi:television-pause")),l.show="paused"!==this.entity.state&&re(this.entity,this.entity.state),l.vertical=!0,l.sliderMin=5,l.min=1,l.allowTapping=!1,l.marginOfError=10,c.show=re(this.entity,this.entity.state),c.vertical=!1,c.allowTapping=!1,c.marginOfError=5,c.mode="seekbar",this.entity.attributes.media_title&&(o.extra=this.entity.attributes.media_title+" - "+this.entity.attributes.media_artist),a.vertical=!1,a.show=!0,a.button0={show:!0,icon:"playing"===this.entity.state?"mdi:pause":"mdi:play",styles:{container:{position:"absolute"}},tap_action:{action:"call-service",service:"playing"===this.entity.state?"media_player.media_pause":"media_player.media_play",service_data:{entity_id:this.entity.entity_id}}})}else l.show=!1;const d=se(n,this._config);return this._config=void 0!==d?d:this._config,this._config.icon="string"==typeof this._config.icon?Object.assign(Object.assign({},s),{icon:this._config.icon}):"object"==typeof this._config.icon?se(s,this._config.icon):s,this._config.label="string"==typeof this._config.label?Object.assign(Object.assign({},o),{text:this._config.label}):"object"==typeof this._config.label?se(o,this._config.label):o,this._config.stats="string"==typeof this._config.stats?Object.assign(Object.assign({},r),{text:this._config.stats}):"object"==typeof this._config.stats?se(r,this._config.stats):r,this._config.buttons="object"==typeof this._config.buttons?se(a,this._config.buttons):a,this._config.slider=this._config.slider?se(l,this._config.slider):l,this._config.seekbar=this._config.seekbar?se(c,this._config.seekbar):c,void 0!==this._config.styles&&null!==this._config.styles||(this._config.styles={}),this.initializeStyles(),null}initializeStyles(){var t,e,i,n,s,o,r,a,l,c,d,h,u,g,p,m,f,_,v,y,b,w,x,S,k,$,T,M,C,E,P,N,O,V,A,H,L,D,j,R,Y,I,z,F,U,B,q,W,X,J,Z,G,K,Q,tt,et;if(!this._config)return;const it=this._config.entity?null===(t=this._config.entity)||void 0===t?void 0:t.split(".")[0]:"none",nt={background:"radial-gradient(circle at top left, rgba(230, 230, 230, 0.25), var(--card-background-color) 40%)"},st={filter:"drop-shadow(3px 3px 2px rgba(0,0,0,0.3)"},ot={card:Te("sliderCard",ee(null===(e=this._config.styles)||void 0===e?void 0:e.sliderCard)?ee(null===(i=this._config.styles)||void 0===i?void 0:i.sliderCard):{}),container:Te("sliderContainer",ee(null===(n=this._config.styles)||void 0===n?void 0:n.sliderContainer)?ee(null===(s=this._config.styles)||void 0===s?void 0:s.sliderContainer):{}),track:Te("sliderTrack",ee(null===(o=this._config.styles)||void 0===o?void 0:o.sliderTrack)?ee(null===(r=this._config.styles)||void 0===r?void 0:r.sliderTrack):{}),progress:this._config.slider.vertical?Te("sliderProgressVer",ee(null===(a=this._config.styles)||void 0===a?void 0:a.sliderProgressVer)?ee(null===(l=this._config.styles)||void 0===l?void 0:l.sliderProgressVer):{}):Te("sliderProgressHor",ee(null===(c=this._config.styles)||void 0===c?void 0:c.sliderProgressHor)?ee(null===(d=this._config.styles)||void 0===d?void 0:d.sliderProgressHor):{}),thumb:this._config.slider.vertical?Te("sliderThumbVer",ee(null===(h=this._config.styles)||void 0===h?void 0:h.sliderThumbVer)?ee(null===(u=this._config.styles)||void 0===u?void 0:u.sliderThumbVer):{}):Te("sliderThumbHor",ee(null===(g=this._config.styles)||void 0===g?void 0:g.sliderThumbHor)?ee(null===(p=this._config.styles)||void 0===p?void 0:p.sliderThumbHor):{})},rt={container:Te("stats",ee(null===(m=this._config.styles)||void 0===m?void 0:m.stats)?ee(null===(f=this._config.styles)||void 0===f?void 0:f.stats):{}),camera:Te("camera",ee(null===(_=this._config.styles)||void 0===_?void 0:_.camera)?ee(null===(v=this._config.styles)||void 0===v?void 0:v.camera):{})},at={container:Te("labelContainer",ee(null===(y=this._config.styles)||void 0===y?void 0:y.labelContainer)?ee(null===(b=this._config.styles)||void 0===b?void 0:b.labelContainer):{}),label:Te("label",ee(null===(w=this._config.styles)||void 0===w?void 0:w.label)?ee(null===(x=this._config.styles)||void 0===x?void 0:x.label):{}),extraText:Te("extraText",ee(null===(S=this._config.styles)||void 0===S?void 0:S.extraText)?ee(null===(k=this._config.styles)||void 0===k?void 0:k.extraText):{})},lt={container:Te("buttonsContainer",ee(null===($=this._config.styles)||void 0===$?void 0:$.buttonsContainer)?ee(null===(T=this._config.styles)||void 0===T?void 0:T.buttonsContainer):{}),button:Te("button",ee(null===(M=this._config.styles)||void 0===M?void 0:M.button)?ee(null===(C=this._config.styles)||void 0===C?void 0:C.button):{}),text:Te("buttonText",ee(null===(E=this._config.styles)||void 0===E?void 0:E.buttonText)?ee(null===(P=this._config.styles)||void 0===P?void 0:P.buttonText):{}),icon:Te("buttonIcon",ee(null===(N=this._config.styles)||void 0===N?void 0:N.buttonIcon)?ee(null===(O=this._config.styles)||void 0===O?void 0:O.buttonIcon):{})},ct={card:Te("seekbarCard",ee(null===(V=this._config.styles)||void 0===V?void 0:V.seekbarCard)?ee(null===(A=this._config.styles)||void 0===A?void 0:A.seekbarCard):{}),container:Te("seekbarContainer",ee(null===(H=this._config.styles)||void 0===H?void 0:H.seekbarContainer)?ee(null===(L=this._config.styles)||void 0===L?void 0:L.seekbarContainer):{}),track:Te("seekbarTrack",ee(null===(D=this._config.styles)||void 0===D?void 0:D.seekbarTrack)?ee(null===(j=this._config.styles)||void 0===j?void 0:j.seekbarTrack):{}),progress:Te("seekbarProgress",ee(null===(R=this._config.styles)||void 0===R?void 0:R.seekbarProgress)?ee(null===(Y=this._config.styles)||void 0===Y?void 0:Y.seekbarProgress):{}),thumb:Te("seekbarThumb",ee(null===(I=this._config.styles)||void 0===I?void 0:I.seekbarThumb)?ee(null===(z=this._config.styles)||void 0===z?void 0:z.seekbarThumb):{})};if(this._config.slider.vertical&&this._config.slider.flipped&&(ot.thumb.top="initial",ot.thumb.bottom="2px"),this._config.buttons.vertical&&(lt.container["flex-direction"]="column"),"none"!==it)if(re(this.entity,this.entity.state)&&(st.color="var(--paper-item-icon-active-color)",st.filter="drop-shadow(2px 2px 2px rgba(0,0,0,0.75)",nt.background="radial-gradient(circle at top left, rgba(230, 230, 230, 0.7), var(--card-background-color) 40%)"),"light"===it){if(this.entity.attributes.brightness){let t=1+this.entity.attributes.brightness/256;nt.background=`radial-gradient(circle at top left, rgba(230, 230, 230, 0.7), var(--card-background-color) ${Math.ceil(ie(this.entity.attributes.brightness,256))/t+"%"})`}}else"switch"===it||"input_boolean"===it||("lock"===it?"locked"===this.entity.state||"unlocked"===this.entity.state&&(st.color="var(--paper-item-icon-active-color)",nt.background="radial-gradient(circle at top left, rgba(230, 230, 230, 0.7), var(--card-background-color) 40%)"):"media_player"===it||"cover"===it&&(this.entity.attributes.current_position<=50?nt.background="radial-gradient(circle at top left, rgba(230, 230, 230, 0.25), var(--card-background-color) 40%)":st.color="var(--paper-item-icon-color)"));Object.keys(this._config.buttons).filter(t=>t.startsWith("button")).map(t=>(this._config.buttons[t].styles?(this._config.buttons[t].styles.container?this._config.buttons[t].styles.container=se(lt.button,this._config.buttons[t].styles.container?ee(this._config.buttons[t].styles.container):{}):this._config.buttons[t].styles.container=se(lt.button,this._config.buttons.styles.button?ee(this._config.buttons.styles.button):{}),this._config.buttons[t].styles.text?this._config.buttons[t].styles.text=se(lt.text,this._config.buttons[t].styles.text?ee(this._config.buttons[t].styles.text):{}):this._config.buttons[t].styles.text=se(lt.text,this._config.buttons.styles.text?ee(this._config.buttons.styles.text):{}),this._config.buttons[t].styles.icon?this._config.buttons[t].styles.icon=se(lt.text,this._config.buttons[t].styles.text?ee(this._config.buttons[t].styles.text):{}):this._config.buttons[t].styles.icon=se(lt.icon,this._config.buttons.styles.icon?ee(this._config.buttons.styles.icon):{})):this._config.buttons[t].styles={container:se(lt.button,this._config.buttons.styles.button?ee(this._config.buttons.styles.button):{}),text:se(lt.text,this._config.buttons.styles.text?ee(this._config.buttons.styles.text):{}),icon:se(lt.icon,this._config.buttons.styles.icon?ee(this._config.buttons.styles.icon):{})},null));const dt=(null===(F=this._config.styles)||void 0===F?void 0:F.card)?Object.assign(Object.assign({},nt),ee(this._config.styles.card)):nt,ht=(null===(U=this._config.styles)||void 0===U?void 0:U.icon)?Object.assign(Object.assign({},st),ee(this._config.styles.icon)):st;this._config.styles.card=Te("card",ee(dt)),this._config.styles.icon=Te("icon",ee(ht)),this._config.styles.row1=Te("row1",ee(null===(B=this._config.styles)||void 0===B?void 0:B.row1)?ee(null===(q=this._config.styles)||void 0===q?void 0:q.row1):{}),this._config.styles.row2=Te("row2",ee(null===(W=this._config.styles)||void 0===W?void 0:W.row2)?ee(null===(X=this._config.styles)||void 0===X?void 0:X.row2):{}),this._config.styles.button=Te("button",ee(null===(J=this._config.styles)||void 0===J?void 0:J.button)?ee(null===(Z=this._config.styles)||void 0===Z?void 0:Z.button):{}),this._config.slider.styles=(null===(G=this._config.slider)||void 0===G?void 0:G.styles)?se(ot,this._config.slider.styles):ot,this._config.stats.styles=(null===(K=this._config.stats)||void 0===K?void 0:K.styles)?se(rt,this._config.stats.styles):rt,this._config.label.styles=(null===(Q=this._config.label)||void 0===Q?void 0:Q.styles)?se(at,this._config.label.styles):at,this._config.buttons.styles=(null===(tt=this._config.buttons)||void 0===tt?void 0:tt.styles)?se(lt,this._config.buttons.styles):lt,this._config.seekbar.styles=(null===(et=this._config.seekbar)||void 0===et?void 0:et.styles)?se(ct,this._config.seekbar.styles):ct}_handleAction(t,e){var i;t.stopPropagation(),t.stopImmediatePropagation();if(!((new Date).getTime()-this.lastAction<25)&&(this.lastAction=(new Date).getTime(),e.entity||(e.entity=this._config.entity),null===(i=t.detail)||void 0===i?void 0:i.action))switch(t.detail.action){case"tap":case"hold":case"double_tap":if(!e)return;const i=t.detail.action,n=((t,e,i)=>{const n=JSON.parse(JSON.stringify(e)),s=e=>e?(Object.keys(e).forEach(i=>{"object"==typeof e[i]?e[i]=s(e[i]):e[i]=ce(t,t.entity,e[i])}),e):e;return n[i]=s(n[i]),!n[i].confirmation&&n.confirmation&&(n[i].confirmation=s(n.confirmation)),n})(this,e,i+"_action");(async(t,e,i,n)=>{ue(t,"hass-action",{config:i,action:n})})(this,this.hass,n,i)}}static get styles(){return ht` + @keyframes marquee { + 0% { text-indent: 100% } + 100% { text-indent: -100% } + } + `}};l([ot()],Ce.prototype,"_hass",void 0),l([ot()],Ce.prototype,"_config",void 0),l([ot({attribute:!1})],Ce.prototype,"hass",void 0),l([at()],Ce.prototype,"config",void 0),Ce=l([nt("my-button")],Ce),console.info(`%c ---- MY-CARDS ---- \n%c ${a("common.version")} 2.0.4 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: green");export{Ce as MyButton,Zt as MySlider,he as MySliderV2}; diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/simple-weather-card/simple-weather-card.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/simple-weather-card/simple-weather-card.js new file mode 100644 index 0000000..f19f7c4 --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/simple-weather-card/simple-weather-card.js @@ -0,0 +1,52 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";var r,i,o,s,A,a,c=f(n(1)),u=f(n(18)),l=n(19),d=n(20);function f(e){return e&&e.__esModule?e:{default:e}}function g(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}function M(e){var t,n=D(e.key);"method"===e.kind?t={value:e.value,writable:!0,configurable:!0,enumerable:!1}:"get"===e.kind?t={get:e.value,configurable:!0,enumerable:!1}:"set"===e.kind?t={set:e.value,configurable:!0,enumerable:!1}:"field"===e.kind&&(t={configurable:!0,writable:!0,enumerable:!0});var r={kind:"field"===e.kind?"field":"method",key:n,placement:e.static?"static":"field"===e.kind?"own":"prototype",descriptor:t};return e.decorators&&(r.decorators=e.decorators),"field"===e.kind&&(r.initializer=e.value),r}function h(e,t){void 0!==e.descriptor.get?t.descriptor.get=e.descriptor.get:t.descriptor.set=e.descriptor.set}function w(e){return e.decorators&&e.decorators.length}function y(e){return void 0!==e&&!(void 0===e.value&&void 0===e.writable)}function p(e,t){var n=e[t];if(void 0!==n&&"function"!=typeof n)throw new TypeError("Expected '"+t+"' to be a function");return n}function D(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}function E(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0;o--){var s=t[e.placement];s.splice(s.indexOf(e.key),1);var A=this.fromElementDescriptor(e),a=this.toElementFinisherExtras((0,i[o])(A)||A);e=a.element,this.addElementPlacement(e,t),a.finisher&&r.push(a.finisher);var c=a.extras;if(c){for(var u=0;u=0;r--){var i=this.fromClassDescriptor(e),o=this.toClassDescriptor((0,t[r])(i)||i);if(void 0!==o.finisher&&n.push(o.finisher),void 0!==o.elements){e=o.elements;for(var s=0;s(0,u.default)(T)},{kind:"get",static:!0,key:"properties",value:function(){return{_hass:{type:Object},config:{type:Object},entity:{type:Object},weather:{type:Object},custom:{type:Object}}}},{kind:"set",key:"hass",value:function(e){const{custom:t,entity:n}=this.config;this._hass=e;const r=e.states[n];r&&this.entity!==r&&(this.entity=r,this.weather=new c.default(e,r));const i={};t.forEach(t=>{const[n,r]=Object.entries(t)[0];if(e.states[r]){const t=e.states[r],{state:o}=this.custom[n]||{};o!==t.state&&(i[n]={state:t.state,unit:t.attributes.unit_of_measurement})}}),Object.entries(i).length>0&&(this.custom={...this.custom,...i})}},{kind:"get",key:"hass",value:function(){return this._hass}},{kind:"get",key:"name",value:function(){return this.config.name||this.weather.name}},{kind:"method",key:"setConfig",value:function(e){if(!e.entity)throw new Error("Specify an entity.");this.config={bg:!!e.backdrop,primary_info:["extrema"],secondary_info:["precipitation"],custom:[],tap_action:{action:"more-info"},...e,backdrop:{day:"#45aaf2",night:"#a55eea",text:"var(--text-dark-color)",fade:!1,...e.backdrop}},"string"==typeof e.primary_info&&(this.config.primary_info=[e.primary_info]),"string"==typeof e.secondary_info&&(this.config.secondary_info=[e.secondary_info]),this.config.primary_info||(this.config.primary_info=[]),this.config.secondary_info||(this.config.secondary_info=[])}},{kind:"method",key:"shouldUpdate",value:function(e){return["entity","custom"].some(t=>e.has(t))}},{kind:"method",key:"render",value:function(){return v(r||(r=g(["\n \n \n '," ",'\n \n \n ','\n \n \n
\n ',"\n ","\n
\n \n "])),this.config.bg,this.config.backdrop.fade,this.weather.isNight,this.config.backdrop.day,this.config.backdrop.night,this.config.backdrop.text,e=>this.handleTap(e),this.renderIcon(),this.renderAttr("temp"),this.name,this.renderAttr("state",!1),this.renderInfoRow(this.config.primary_info),this.renderInfoRow(this.config.secondary_info))}},{kind:"method",key:"renderIcon",value:function(){const e=this.custom["icon-state"]?this.weather.getIcon(this.custom["icon-state"].state):this.weather.icon;return this.weather.hasState&&e?v(i||(i=g(['\n \n '])),e):""}},{kind:"method",key:"renderExtrema",value:function(){const e=this.custom.high||this.weather.high,t=this.custom.low||this.weather.low;return e||t?v(o||(o=g(['\n \n '," ","\n ","\n \n "])),this.renderAttr("low"),e&&t?" / ":"",this.renderAttr("high")):""}},{kind:"method",key:"renderInfoRow",value:function(e){return v(s||(s=g(['\n
\n ',"\n
\n "])),e.map(e=>this.renderInfo(e)))}},{kind:"method",key:"renderInfo",value:function(e){return"extrema"===e?this.renderExtrema():v(A||(A=g(['\n \n \n ',"\n \n "])),this.weather.getIcon(j[e].icon),this.renderAttr(e))}},{kind:"method",key:"renderAttr",value:function(e){let t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];const n=this.custom[e]?this.custom[e].state:this.weather[e];if(!n&&0!==n)return;const{unit:r}=this.custom[e]&&this.custom[e].unit?this.custom[e]:j[e]||{};return v(a||(a=g([" "," "," "])),n,t?this.getUnit(r):"")}},{kind:"method",key:"handleTap",value:function(){(0,l.handleClick)(this,this._hass,this.config,this.config.tap_action)}},{kind:"method",key:"getUnit",value:function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"temperature";const t="speed"===e?"length":e,n=this._hass.config.unit_system[t];return"temperature"===e?n||N:"length"===e?"km"===n?"mm":"in":"speed"===e?n?"".concat(n,"/h"):"km/h":e}}]}}),m);window.customCards=window.customCards||[],window.customCards.push({type:"simple-weather-card",name:"Simple Weather Card",preview:!1,description:"A minimalistic weather card for Home Assistant"})},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=p(n(2)),i=p(n(3)),o=p(n(4)),s=p(n(5)),A=p(n(6)),a=p(n(7)),c=p(n(8)),u=p(n(9)),l=p(n(10)),d=p(n(11)),f=p(n(12)),g=p(n(13)),M=p(n(14)),h=p(n(15)),w=p(n(16)),y=p(n(17));function p(e){return e&&e.__esModule?e:{default:e}}const D={"clear-day":M.default,"clear-night":r.default,cloudy:i.default,overcast:i.default,fog:o.default,hail:g.default,lightning:s.default,"lightning-rainy":A.default,"partly-cloudy-day":c.default,"partly-cloudy-night":u.default,partlycloudy:c.default,pouring:l.default,rain:d.default,rainy:d.default,sleet:g.default,snow:f.default,snowy:f.default,"snowy-rainy":g.default,sunny:M.default,wind:h.default,windy:h.default,"windy-variant":h.default,humidity:w.default,pressure:y.default},E={...D,sunny:r.default,partlycloudy:u.default,"lightning-rainy":a.default},m=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];t.default=class{constructor(e,t){this.hass=e,this.entity=t,this.attr=t.attributes,this.forecast=t.attributes.forecast||[[]]}get state(){return this.useComponentEntityTranslations()?this.toLocale("component.weather.entity_component._.state."+this.entity.state,this.entity.state):this.toLocale("component.weather.state._."+this.entity.state,this.entity.state)}get hasState(){return this.entity.state&&"unknown"!==this.entity.state}get temp(){return this.attr.temperature}get name(){return this.attr.friendly_name}get high(){return this.forecast[0].temperature}get low(){return this.forecast[0].templow}get wind_speed(){return this.attr.wind_speed||0}get pressure(){return this.attr.pressure||0}get wind_bearing(){return"undefined"!==this.attr.wind_bearing?this.degToDirection(this.attr.wind_bearing):this.toLocale("state.default.unknown")}get precipitation(){return Math.round(100*(this.forecast[0].precipitation||0))/100}get precipitation_probability(){return this.forecast[0].precipitation_probability||0}get humidity(){return this.attr.humidity||0}get isNight(){return!!this.hass.states["sun.sun"]&&"below_horizon"===this.hass.states["sun.sun"].state}get icon(){const e=this.entity.state.toLowerCase();return this.isNight?E[e]:D[e]}getIcon(e){return D[e]}toLocale(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"unknown";const n=this.hass.selectedLanguage||this.hass.language,r=this.hass.resources[n];return r&&r[e]?r[e]:t}useComponentEntityTranslations(){return Number(this.hass.connection.haVersion.replaceAll(".",""))>=202340}degToDirection(e){const t=Math.floor(e/22.5+.5);return m[t%16]}}},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgBAMAAAAQtmoLAAAAHlBMVEUAAADL2/vK2/rH2fnI2vrG2PrJ2/vL3fzG2vzI3PwdWi7LAAAAB3RSTlMAFzNfjrze/OgtNAAAAdhJREFUeAHt17WC3EAMBuBZ3nIZOqcJVGGqwlSFqQpTFY6yVTiZrUJ3Z+tpj3ynBY3Hv6vDr5aGJMOYbetX49CtmsE1bxO9M7DcEVr02aDyt2nJvozxH+H1xPH01oAuUOyvwXRoxRUDKdCqAF9Qlj2XKAaX7TbF0EMqk3gNTiD+GECRBHaqR0lAnZSjMQG4ZVEDiybQFQm8ynjCrqwJzzIm5GkCvAU8oasSoL4QNXDPIgDLhjdfgSadBA9J3DcpKjTpFXiq4g3YSeIdWAbxAXx68ELcpkmDk9kSBvwnW2cw/zd+v6Ym4DkoQeIX7cOWJPGc0hx3SXBsJiWBQwmPhTXjc4kV/wv8uIoP/Wvaw0ro7Y4+a95ve5W1Od+2S6z4S1FgzdseOdYifuDJeMEOUQ0shHgLnCta7hI7RYnVy7PbbC25X12G0UwN6SYRWTsbZNmEXTS33126oXuCRb8eBUglJH5R+PiA3krZuSDBdD91TXaSmuOYP/6b/oXTGxh3MuXVYaf8SCmFnXbF2x6R9U2gd2G1hD6/qMNj341bYWjdEl/+HevkeRscs5qu2ZjcHav8qBmPvMr4Ic2NZeh4fbW0Yx7VkMvrRRl+P3w9vvn0yY0zO8y6tW0BcWLk3nu06KcAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgBAMAAAAQtmoLAAAAHlBMVEUAAADr6+vn5+fo6Ojo6Ojm5ubq6urp6enk5OTf399QNH53AAAACXRSTlMAIneu2P///z8+tLZ6AAACF0lEQVR4Ae2Wt5bUQBBFtfhwIgjxEOJdhHcRtigU4lohtvV2MiW4D1ijr11XpR76lKp3Y45uMq7uvPbq6j9mYmJi6fTD0Ly+PNtr/Zmg3Nvb3z8KiQ9eiFMf8HF343r4B+DDbvW/Qi5guVy/P2Q0QHuiKDwyAn6U6g8EK+BOOcAKH8sBVsDF4pBmYJv2vVd/JIwKCJ0jHHIEPPda5AnfnVUUPAEzZ4w8wZmKo8GOqvLVmTVPcDoRfKEdq99nBSRqakN4fbkzfXaFFd5JulmctgaJuQjAk/IgJeqeINxNwrlCixD7FShdYWFYIUZ8c6ehyYSe9R2GTf7GDRBhRRPSOdKWBPR9zVBFl6LfIkkgkvK0FP0ASVghloSobWrdAEmoiTThx2inAdMkYklANzKsMMIKEcWdBFy0E9cYYbueWBK+2KWBHMz7mraRhGWz+GAFIomIW8jqOOzUi7CyIsZOgggHvPbLBiKB4xbfsy2KMWoawGIiUGAlCbyFCjfgEhcCLU6dYwVhpy5F6B46AJc5cYIAFZYYHjLHKUGXq9+JSMh4rsJBV9CdoKSH3hF2hCHAnDTnSwHggdeL49URpB5Cdv14jDEYecKdqhwRkSfEWVWO0B6khLfZU4W9gJQQ8/vQX6c+xiHhZZVzDQ6sZlflLD3ECHFIoLv2QnCL/YT4ZPSiu2KrmcGM+qZ3lX7aj3D/VFdNTExYNgHAL10vmVlOfQAAAABJRU5ErkJggg=="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAOVBMVEUAAADHx8fKysrJycnIyMjHx8fGxsbDw8PBwcG/v7/CwsLCwsK+vr67u7u4uLi3t7e4uLi5ubm4uLhyXqCUAAAAE3RSTlMAFkaMtND6////CGT//7BAhezMkbzxVAAAAi9JREFUeAHtloeS2zAMRF0JrA60yv9/bBQagS2YpOj05PiS6zvYURk8Hv5vOp1Op9M5ns6XawjXy/l0bM80c7oGIr5D4XrKZ4ihgCzTwPARKMFsHR/DS4axxWdKHM+BFNZ/qeN83GTwFdZ/imWKDMfTJZDBilZcTsfhniFmgPEKp0yREHS6vwK7jLCivQDbv2eYqFjgxkNiFHAevQKJKyJ4RpoKcNMbMt5yHWm83MZ0Q9ZMfLNANo90EvYAiNMmE9sLeH59KWdfkMtISwFjGQ8ZxkXACqSUiVItmJfbNJYXwzjdlrmeGVLmoHS6D1gh+gU+MNn8Gh+kmelf2QdeB+0+SOPcFbD3AfCdPiAd76/A1JkiNl4voN0HOl0/Ia6g6AIGqw+i4IGI1ApsfMUHOt77oKGADZkqPgBQ9kFLQdYHcDeo5APZLZA9HwA1H1QLWnZ9uw863QfExq/wATt+gQ88OR94uN0HRskHWUT2fcBFsEKkPsgiiZoPeIOe/fFYcYaJRmJCtEBpKpDb+M0Hog5g+6fEjQ/knYLMrvdXAJ9pL0DWB+4Ykcs0FVR2PYz4q30wdB98Gh8M4w/5AErFB7aEwO/6AHf0a9YHbsk1+cCf/e0L4HxAMMRo9AFW/BWkTw8fMPx8BTs+sLO/vwLWDiKvA/GAyz6whfa07AUVzAdbagWMxObsn+9wPmgqKO96ZCj4IO4VFM/+G+S7fLC366FIOROrBaVd333Q6XQ6nc4Xua9QVFRZEEsAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAALe0lEQVR4nO2cW4wb1RnH/+fMjB1vdjfkssJbVtAmTTcNIJVeRFFVSF+qKg+tgsRbqcRDxRtVBVXSIiFBpUKkipa3XgKRiiLRUiWt2oaqElXghXBRVKhALJCFJJvdyTrJJnuxPfac8/XBnt1Zey5nbvYs9U8aOVmPZz5///N955zvnDEwYMCAAQMGDBjw/wjr5c0qlcpO27bvY4zdCWCvlHJS1/XV94kIQghwzqcAvEdEr+u6/uLY2Nh0L+3sJZkLMDc3d5Ax9gDnfDLJdaSUU0R0dHx8/HBatuWBTASoVCo7hRAnkzrdDynlCU3THvk0REaqAmTteKCVpgCAMQYp5ZSmafs3shA8rQuZpnmciM5m6Xw3RATG2CQRnTVN83gv7pkFiSNgZmbmnkKhcCq5KWo4EeBFs9ncNzEx8UqvbEmDRBFgmubxXjofaKUePwqFwqm5ubmDPTQnMbEjwDTN93uVbjoJioL2+1PlcnlPj8xJRKwI6KfzVWCMTZqm+X6/7VAhsgB5d74D53xDiBBJgI3ifGBtlJT3EZKyAKZpHs+D88PyvxvGGDjnB/LcMSt1wr0eagYRRQBgbdTUaDRyOUTVw09pDe+yNSM7HME456fQ4+KjCqEpKO85VBVN03L5XQJbRKVS2UlEZ3tljApRU1AnnPNdeaodBUaAEOJkrwzpFXn7Tr4R8Gls/Q55igLfCMhbS0kTIcQv+22Dg68AeRjzZwXn/EC/bXDwHIbmceKSVvpxrjU7O0uWZaFYLAJojZK8Kq22ba9boxZCnE5zPuHZB+Sx5JCmAAAghIBlWQCwTgQV2qtxJ2zbfiapGJ4CzM/Pp/ttE5K28x2q1SqA6AK4IaIp27YfjCtElwCf5tGPG3cEAMlEAFpCxFmf7uqEbdu+L5YFGZFV69c0bdXpANaJEQfG2KSU8uzMzMw9UT7XJUB709QABYQQ614BwDCMSMuiXsPQvYktS4msWr8flmWtc2YYTrpyXp3Pcs6fUhWhSwApZS5GP712vkMUETojwN1/cM6fUklHXQLE7YQ2Il7ftVgsKovgFwHOq2EYpyqVys6ga3j1AaE3zppetX4/Jzudc5gIfhHgFjaspJPazri0SOJ81lyGPv+G8vleEdA5GgoSISwChBBgjE0GpaIuAfqVe9O49+bZl6DXLiqf7+VcJwWFnef+u18EOK+6rv/Wz4YuAaKMAtIk8UJLvYKhCycgeTH85BDc8wMHL7+oRACAwCjoEqBdeOoZRJTY+UQSI9PPA7IBSrjsGzQh83NwWAQAgK7rP/K6plcf8F5Em2OTVrorXv0PigtnAF5Idp1i0bP1uxFC+DraTxgAYIx5lsC9+oDXI1seg9T6GruG0Y+fB5jSBo91dNaCVMsRcSIAaG3v6bxWlwC6rr+oZEVM0kg5boYu/gO8Pg8wDWBAkkuHtX43XpGgkKK+3nmdLgHGxsamsxgJpe14ANBqJoYv/g3QknW8UVq/G9UICJrces4DiCiVjthxeiZDWxIY+fAIQBJrVXX1+3SOaqK0/s7rqEaAV6HTT4CjsaxBxk53Ubz8OgqL77k6XnIdEa4T0/FuIkRAV6HTU4Dx8fHDbkdGOXpCcwWjHx9zOV8iquPd68FpENb6/fAtRRDRibSMS5uRC8fBmtcAxtByfjSymmwGDVHbdA3xfQXQNO2RdM1LB235HErmvwBuJLpOZ+vXNM3ziEpQBHgN8X0FaI+GejorDkXaGP3oWbTSTfwZb+eIR9O0QMeF5Xav4ajKCAgIqYZqmra/X7UhL0qXXoGx/CHA4rd+IYRn7vdyZlheD5qQeQkphDjded9AAcbGxqY557noC1jjOobPv5io3NDZqsPSjF9aCjucz7pfhRDw2roSuh5QLpfvzUMUjJz/E5i9BLAAkxVHoaq5PW4EeLV+v4astCAjpdzXTxH06x9g06VT/q1fwfFe67ZhRM39QSMg27af8bqHkgATExOvMMYOKVueJqKBLWePtIecLiLMu+I4P63WL4QAEU357ZxTXpJsT856Pioqmf+GVj3f6niVnO79ZtS0k1br1zQNtm0/6He/SGvC5XJ5Ty9F4NZVjJz7I8CLCk5vHZ0rYu4hoQqqdR3V16DWDyg+JemmXC7vMU3zfcZYtvuHiDA8fQyQVksA/xPX/sl0bKpOw756BpqoQhID5wHzBQKIcdR3rFWJHcHSaPlCCBiGsT/oayb6sY4sRTAW/out7z7Z7njdZoYlfdE632uBprNWRTYa+g5cu/NXXZHi/D/Jq5Qy9Nnk2NtSyuXyHinloUxGR3YdN5x9trXIAkC9xyWsfiWy1w7ZPqjjEBau726l587Uk7TlM8YOqWxZT7QvaHx8/LCUcp+zzz4thmZPgtVMlwBBeInD1o51mrn+Li2s7PgmxA1fWJe30+gDGGOHVH9cMLVtcKZpHpdSHohTwHJDi+dx47uPt1qop3mKZeeg0jhJEBjmvvwM9KFt696K2ml3flYl7bhJbWdcuVy+1zCMXUQ0FTctEUmMfvScj/MjLLaErUvIBi7f8gBQ2AIAiSPAGesbhrEr6pMyqW5NHBsbmy6Xy3vaQpyoVquRau/cPI1S85P2/wjquR8tpztHELIJa/Nu2ON3Q9f9C3CquZ5zPiWl3Fcul/fEefa4Zz/cWqvVJovFon94Nxax/cxPoMmaullxVuCkhUu3/RxydJfn6MUh6O+WZaFUKmX3kF5WdP50ca1WWx3Gjsz9HdvNv/iXmtNY7pQWrm37FhY+e7/yLvBSqbT6iCrQKiln+phqLzfnXvz9dwAGlAz27a2jdJIxaBGXdtVhBCnIPHfj9w8sbb/rtE6N1bf27r01o5t6mNEhfF+3p19Z1mBrQ6Wt24ynGCg754OAZg2V4TueWPzMV08zYwWi0Fw9+kn0/XwpUhi7GcOYfYjJxh3pZ0N3iQKo1vhr1+ylI2z2bUDU15/6+S+lfG91+puC3nzu1psuHHsTTC91lZv9CDWv4wQGkBDNTy7p91xvFl/TZPcOuNt//E+1e6dAZwrqawSMXHj5B8KSF8GkHXYutRyr60V9F9Y1nDBFJKpL9d/okr+2Vbdy95tl/U1BK3M/Jc4PEqnt7eEG2wFGF0DYFH42ASTQxOYL5me/94QEA8uuk4lNXwUgrktAhKd/ArjBoRU5V0pBRK1rSoEFy/jZ0t67LnPOEXX3XC/oqwBKEMB1Dq3A4f/4i/cMeHFFf/nSkjimvfprBKq8+w/p2BqDfAtAANcZtKLH7uewH/C2m9ZKlR7WySJeS7damyb5FcBxfsF56iJK+iAssvLTjdHi2yMs+t7RXpJPAQhgjvOjwgC7bn0w+8X7n2xs3Q0uwh+8mIhhYlrkTwACuBbT+Wg9rzG/tOlR8fEbS9z8EJAK1didt8e6VxrkSwBqLYKt5fyIMKC6LP56pT78Zzr3Frjvok4H+x+Od78UyI8AbefrxZjlKcZAtr20UhMHbzCWwHI35fImNwIkcj4A2HVc3nLn4crnvjGle5Qb8krfBSAAjDPocdMOADCgURfvLAwNP20P3wRh11KzL2v6K8Bq2knmfBJSzC6PHqpdf6fGP3kn+jVuuzv+/RPSVwEYT+h8ACCJ+krjBbLppc1MADJ/5YYg+ipAsRjzUaPVWTBB8KErlS23PcqGJWKOXPtKfyMAlKw+JptYYNsev/KVh85pFH9l6+YEJiSl751wbBihahlvXl5Z+Z3+xlHEeVx1lVt+kZpZUdm4AkiI5To90rBsS5t5q9/WxGZjCsCAap0fbUr26vZSHXms86uy8QRgpAvB5mcmf/hYs7gDjEJXM0O5KQWz4rLRBGAgpl9dxGP1WmMOrK6ynJxr+j4RiwTDZqsuTprV0SM48wKYs/SYlK99N4WLxKOvAiwsR6z9MCwIgUPDhi1oda//gAEDBgyIyf8A4n6spjurxDYAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAyVBMVEUAAADut0371Uv92kX920b+2kL92T/CyKz92j792Tvvs0jk1s3s4+Hl4d/r6ujp6Ojr6urn5eTk29ji19Po4tjq6urk2tb81jvp6en60z3r49P32Gnr6OHz1pbo6Ojr28Dj4uLm5ubn5+d/xLn8tl7d3d3/sk/k5OTa2tr3v3jh4eH/sErW1dTh4OD/r0f/rUXiwZr9sE/+slD0o0P9qkX1pEIP3/kL4f0q6P8H5f8A5f8n6P/6p0Mc6P8P5/8E4f0F5v8C2/sn6P8h4jSJAAAAQ3RSTlMAH1+bzey9Bf//SR1mpNHj8b5fNZP/T////////////////xH////////////l////0ayG+q0o7k/4/07PPNek3pNP7VonwQAAA8hJREFUeAHs0gWiwjAMgGFWDzBfmD/uf0rcgqV9bv8B8i3NJv/92CIhldZGiugjpluh3SFwWtj3nj/V7hwA6On7fr50jgLg5CuXmM3jJM2yJI+L2XGEVY4Cu1S4YMsqw8vi0pL5BAgXFhnelBV2Iuv7AMig8WWK92rSqm3b2tV3AAi4tM3xfk2DXX8WKKCt92lTfARsGs4CBUD4Pk+Gj4Ft41a4A2jP+YgMgESAc5HX+2Qs0Lz0FwKEvZFNkQe2d6hrAnj/qTn6AM1y+7feAMbnACxwPjQQwO/KqS8wbICNABAGLNj5ZIUaNtXnDH9hDiArbAmS5C/AAmQFWl0LDqhCACQN3XLsIw7ImPlMmK0pLa+FxUEgjLp7Y++9TRjSg255/5f7vwS7IOO55xyqZtD+/Ig/qkMBQDRZfQrMvHYCG4EfTNvfHgEGRopZkyxAo9gbmHr8hyRFgUkUQKH91TPekD5maZrmrIlEARqKL5H1R1lWFEUa3CW6sfoqkJcIgEoemLTFW7QhPmY2kCpxgAbSQ8YG7RIbSNMqJ6GfxsJrilFcwm/qQFUplgaoL3to8Ee135zqgFI5SwMz0U8FNkgl0DeBqg6gIPJ7bmp74jhhYwNpE8AaZIGp4BDsEzAPARS8ayAQPuX52xMwT4G8hqWBwF8mxuijeQl4C0SCLQKrx0F4Ao4A0OHAwhNonx/GcGkeAwoBX4FemXn/9u/z15HxBVAIBPotH8PrCDyB5wAKwFkg8pyxf5NI89G8BPxrkO8Q6E+sP8qeAk0BCYst+BfQ/vj1xXUAT+CB7E6pVPVcoDdWge+7Cv6jeeVPgznd3rT2BYatAP0z7zK3/+8/bFadQIDJ7Z+2WyHaw9KlB0kBbICZyeHfhP3gl1MPotN1BVxDb34ehv3gv0sPylMTUKoJAE1P6POqJeG3yw6yUx24LsBCd/LzrC3yxw65PWEEmgYqL5yni1n/p726QK4YBmIAqgwFrvEHC/kc5vvfqejOZjregrXlvgsoYFl4o0Pud4SN7DL3OsUwEZ9yr20GG8eTyFfOUMQxgq2e/7SBX1GWBR2w3aofqKqbpm6pgO29Dn5986APDtg6kVKZunlQbxBk+0SrwNA4AxdwivUP5PRUQAa/UQJGJkCtwCQBExGgVgC1BNQIoXwg40/UQVVIQBEeEEE3N86MIFIBRewS5hhBpAKqYizLsUAg5Q/bUSpgR6mAnW2m7JeVTtkvMxGeWWS/OMR+Mdj9EsR+Mbj9EsTlTKL3SzD79fWfiNgvGrFfgtgvBrFfX+/fLZ2VqOfdJyPAAAAAAElFTkSuQmCC"},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAllBMVEUAAAC8y87X3OnY3+zU2OXN3/3J3Py5usLW3+7O3ffF2Pnd3eLl5efp6ern5+fl5ebq6urH2vvp6enj5ObC1Pfo6OjU3vLt0rDk5OT7uWfb29vh4eHxw4n/sk/p3c7W1tb/sEr/r0f/rET6qkfqokj3p0b0o0T9rEYR4PoL4f0i6P8H5f8A5f8K5v8J5v8A4v0C2/sC2fne7Ib2AAAAMnRSTlMAF0uOL77/B6Po/2HG6v/f////tf///////////////////9BMrIbrKu5E+f/bnqaTqTOwt04AAAN7SURBVHgB5JiHdqswDEBZwWQgUxM1zNL5/7/4HB+F+hAbqxlnvftG9721lB3918RJmj1Rn23yPI+fphdFfkY8bTrb3PA0/y5/akDQ73/3iLJ9cSgB5CHdx5FFQfq7liyytKwsoMjm7sscSG72J2S3KUm3yedCcateVi6UTMyGX6hw6xJEXblRqipElOysQnqDP5aVL6CRcXqkwo1rzqBaCyjcHu3C5s/+yo8yVK924eWPe45hLUDsTEEH/l4QsmIEcGcCdIR8E/MDdcUJKDhSgUgFewG8gHqlwoW8SGJORHIDaAUIztUu4fjtI1iNnSa0blHyA+qo2Rn0G3ovY2yAG0B9hCVpFCBl+YnlF+F1G9wye0JuqnK/nshW1eGAQiyTtcDea0eNYvg1tfjrCpTCUwMAyAug9BcOHn/Tdj2ngKGCdAYQhrHr+kYXkBXA2hcAt/9t1AHGGXDGt+nGGWhaHTAFdqAU7BEphNNIgZ4dwD1vyTQgzWQCDTL9WDIvpspseBzfKQDcAMa8KxoN6P0SaIAbcM8ou/Y3rdabQK8DVAj7sXAGROO4CrxbASowAofwEmjD77+BxgAcv2/L2dVV4N0OUIEVCN8hqPOG3x0BZwEX1J5AoixoQFaAgHAg9QSEtH4GhndHwFnAJfvIQ6YuAA3IMSIqrAXiyEdx+QmgDS8upgSs+yXZ/ENCoAHZAeeU+BMi4t74zYaXAYIKfn8ZeIKj1DwgYvxluCTA68ckWi/02n96X/JheH/rrYI7UISfBNKAln7N0M37BnT7D5yXaga3/rOdOo0JAAA6/SIKkzn1mtM0UQDO4JUfiojDl1v/OUznQE+BMwu9TFj+76WdGCfrANeFRu4jFsJhpwMQnYU+j/5f1vSKD4efpZn4115dHEYQA0EULZ0Ux7AY8g/ObLep56Aye18A/RendnyMVbl/WHwIe0zvW/Ex9llMz2xQWIthk5i1M857xwf0DyjElGLgA4t2P90JbGA2eJeJ6U40XGDe8b6cHmQiMM/zYfUP6EEYDMx3VryvSKBgxHxvgyJLIDMBC0WUQCQCKzRVAnU8sEDlJODGAwa6lh40DAd2nGnUfah/AeG6991hPLDiM8lf4NMC9pMD6+l+8Zbz/eIZZb94xH4x2P0SxH4x2P0SxH5RyP0SxH5xqP0SxH7RiP0SxH7RZL9+l4tr2oowXbK2IEgAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAsVBMVEUAAAD0z2L93U/93lL93Ev81D/93Ef+3Uz72k75zEP93Ej93Ej920X920P920L92kH70z/92j/92kP92j381EP92Tv92D7+2Tv92Tn70jf810L44Yv15afx6c3x7uTv7+/43G371Dzu7u792Dfp49zcxLbt7Ovm29Xs6+vp4t/x8fH70jXq5OL5zzT4yjP4yzPq05fyvzP4zTP2xjPvyWD0wjHo3L/2xjD4yjLm5eTkxIAPtip5AAAAO3RSTlMAF1V5o7zT5foJl///////R/9w/yT//+L/8v/////////////bL8tJ6Ib//6P/nP///9P//1f/deP/x8TGymgAAAPmSURBVHgB7JSFduswEESfGSSHE7Nkx4///wPrVVm0ktvDuaWDczOzOv3x4MEDZ4IwipM0y1eyNImjMPjG8CIsU0JoVe12u/0LFUnLsPiW+EOckwr4KDgCVR4fvvzho4zQFdlwfKHKTsXX480CIIs2K0KIf0G7EXA+H7NwU/wlIYQ6CFb2yWXDx88JEQZko7Mg9y1RlIT4CK7H0usSQUokAUUE11saeMyfEj8BGK7pxTk/I1sE1/rinr9JAAb3/X1vIHC5Q/Ga7/eKXg0FKijJZgFQYvkhIfaFEME1RA6cY4K9XdC09kMnUj66kCJoki0D+QhsIxWZnI+fQBE0dWEURJTK8WoBVNCczAWohPdCIDBXOJmy993685qPC4wV3guQfhgZ45yN49Dn3cpegC8kqPX5B/pM3o+Mf2QUjv07R0TQHLSCmAJkYFyBDa8KfCEg1p44h/yecS2sh6GwAq+CVnfmENYZuZFxAkOHLwSEGkFJu4lxC2yaRYsjshBQagRp1d+5nX4GBbYQkKr5Ae05gBpuN3yhZgnURzrdOc40zzcwIAWaRX2oEeMOsGcDJliWkyL4yZ0Y5xkUWIFFufIv7kgPAjCcrYJEFvx2FbBcGJACi/yM/nBnBhA8A/kGQS0J/roLWPdqMBUAQYtcALsCKK4A/FUKAJ8F/7gP/wHGxmGCMk2jFAA2LyT/GxcKuYA8UcA3cx9moZAL1E/tmYVuI0EMQKfcFa5OWG4nrInHy3D//2Fn3ST1MjnivID4PXkZGtuQgI9nQ4lV1c+bKa8CAZ9UMFxwgQdV5SgK6M9nMIakzu+wjd1YgNb6A6lQD/wRb0TsJwI0tJAoQX6HLw6wn/h8QbeQVmdu5IuI/cR76ApnIlXjKPXrzzAGDrROBwehn0iosOJE2jxYS/20jGJkf/PC60bs11kcIxesarCX+vUHBbjgqwYHqV9/xlQAMHykq7GR+V0AEU6FXLVYC/y8iMAVum6hdiI9kZ0nMMYNIBlBdxBwoPBUBxst0BMJBYAwBlLVyUGi1yX5T4EH1cN2oZ6XEIIB0/8w5Ga/QM/HOgp0LyBmU87X8wCngFUDbPaz7Ez2O0DkqSFu1vPtxEfoBiD/jRrGO+rJcvYnZ38x4bHmZq0nm3n5uAU06Gd2az2DMvj1R9P8xOawn2h3evJTgJf/pMbfICuHybIgYT1aT80jDYchN4ExEgAv6ZJXLORogW0AcNYrFiYtTk73h51yokjVQry8qGoBoYWBIuelvyiBVRthKgj0jG9j/C83TeDF+uoSeKmNECnAFQCMbOqpy3Hj5/YhKl4QX4roweb+jZrKlStX/gHY0DfPBrPHXQAAAABJRU5ErkJggg=="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAS1BMVEUAAADO1fHO1ejZ4fLR2ezd5vbB0vXDzufS4vzP4PzN3vvV3fDk5+/Ayerc5vfu7u7v7+/t7e7k5eavwOrT2OO4y/DAzOi2ye+/0vYf7YBFAAAAGHRSTlMAByNNhqbMO////2bRFP///+v/////9qxLSnpnAAACKklEQVR4Ae3U2YKzKgzA8QRBXECgdTnv/6RH+2V2LWLgzl/n+h8NOHDe7XZDUUlVY6G4Vk276voi9cps9U2HkJ1QW51UpfKkg8ws5Uu9QP+ZL3IC1rS/KMhJN+1vAjKS1C91xKr5O0BCNqianQE65/PvDcC8/abcEcjmpdglrf72s37Gdk1TvcxXYNoDFrLo2yOYa0FHIAvVlh0g/mS7f3INqFuyMyPnC3R7cp0A5YfB+Y1zw9C9ILAhxVuKf3JDngFV220TBr9j6CywmW43T3rgst3K+UMKgUd3XePfCQJY1PF6iNPAEe2vOBOw8XGOsSXt/AkB4SrjT1FwkfYnVSVfgLGkyvuyrxD8aQEuED6BhnTSJwtG9YK/oYjQ8zcUEap8d4jxb1x6DhOfoDyDc/EJhjfAKYgIrP6qKjfAvQQstSJH+kKH7D6EMtfUfbElPjRH4juyzH78pgZu35nsh+DIuVMWvH58ABhWP74i0Ix85JCJ4fVdDxHaMfIrATGSk/ePZy0rYRHhEJpLeTI+N9NqPpxhw6U8vcDnAAuHxCM9T+aP/KThDREu1GlBNEHCW9ak1cnjcz8LRKB0iXHqr2J9oo07Wybjn+ePj0jwmFOen2gZ0vLTpocUQo7j471xpPr6o/uZBJdnFMVXtYV0ejqtgktwmU5ZEK6yy/Tfron+1t8igGQdQabFAhfq+ihfa4QsUMvp77NTPRe0ul+Wep7rRfbaJsRvt9v/h4jJKdZPEXkAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAATlBMVEUAAADj4+Pp6enn5+fp6enp6enp6enq6urm5ubm5ubg4ODg4ODm5ubo6Ojc3Nww6v8Y6P8T5/8m6P8H5v8A5f8L5v8N4PoF4f0C4P0D2/uDNJb/AAAAGnRSTlMAE2SS0e7//7smT/////0a6ehI///bMaX0k6g43vIAAAKLSURBVHgB7JjpmuQgCEVjiBdTgu//trMPX5uuoLSzT5/6f09xyb6984eTdjoymBm57OmHpxO4A+VHOs6DXwHOjx/17ws/A+D8Q6Ygfg4+wWU9/2BHAOTVejL7AvC5tF2+Bz/AkNgBxtsNeU7AaXG/jmBp08SzAqYfvgDLXyipjAVLJZ0cEeCMbzgmyD94A4wrtYooM+dC+8w4FBXIF4PiM6w0dOSogJsJPsOUflBDRm0iYgJV7J7gERdIq51AlX7AWdwLai/QsngZ6tH2ydALnBlyXIBPAjPoN273oMF8E0gvQLoRhPNNIJ1AKS7AQCCdQM9oRRgKxPKdEXI8X7/ni7wUSOwwxT3SqhlURx1ROB9i8b1hn79UwKW+FMhoCQnBeMDyL4YytWUM0YvADPm5gBAItxtOhy9ICFJNYLgVbQeM6ACGe7FIigBcvwf2Bu+CWhDA4lRE7SfSn2g9STCN2u1Yr+TtFtLQADcC2u45fkC+pM0hBwpSyx8NEF+Dky9pczklkv9aII9tQGqT/YQLMg55e37ZZqDq1uP0Q9MfQ8T5/zrfv6M4quKCheJpvlAKfpDK1VvGJb1ZfMTxoNJaHdEmX6He+dheXR04DAQxGO7ANFrqv9DD1eyBSWH6Hv9wDLpB/TD0m2UcWWSTfZi2CsCimuzLtF4AgEXTW9WvFXxhkQSrwp8CtIKKRRDNxZ8Fn1hALIJkLrWCbywgFkEwF1hALCAWQTaXa4GbL0f/RXDz5eiDDDdfRMW+oXhBNV9kBcbXesFmUcSUc4pqebkfXCuxCLhWWhHMrNWEL/NF1tZKKYK2VkIRRLi4vwgSXNpfBAEu7C+CDJf3lNv7i85+kH2tzrloeC3ayx7vgDWcARqOBsEAAAAASUVORK5CYII="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAARVBMVEUAAACy6fHl5eXq6uro6Ojm5ubp6enq6urm5ubi4uLj4+Pf39/o6Ojh4eES3/kP5/8k6f8M5/8G5v8A5f8G4f0C4P0E2/uRTESCAAAAF3RSTlMAEFuV5P///8EbLv//jCngQ/z//6X0kyzJZw0AAAJ0SURBVHgB7NYHlqQ6DIVhOejKLsv7X27ndj0KrELAm/zNSZPuTz5Nv7h/QkyZISLIJQa6WE2QBZR65XyWFUi+0TVCkS2A5EoXSLINAKTQaVmMAMCBTqksdgByoxNuMocLClUMGCodxfsCEs7fXzMAPv982gEkOiDs34dU8iuOAPL5J8gO4EZe2RfIl5/Ao9aYVURySX3P6SRvgD8KineiqdIT7A3ICHyQFMgSXPvjGvEIqCKSIfoD3NoioJpOvsXrQFsGtBx9y7BFVwHrHPhYYBT0S6QJde8D7R0vAwi07cA+f+wzLwKa/AHMfAV4EdDqvUSY4XYv6F068sPK/Cn6BL1j32MK7DgDZv2Pm+dFgx3gQe/i/k8FTK2tCsZNCHDOA2P+oVB2vcp4StukkGlTx2rcxo0f2IEKH2lfBeXBvERU4LKaf2d+LKrCQcfgqhDpilMYc8qs49fHH1WaCOw7AcU7fcQ01RV78TzQaa5csM+VDNlxgXS1bz5Drttg7HMg0409++sAR3oitP3XB+q5w0Ph4/uF9uhWgY197rRPKGwcv/qv/1otrJv777C5zz2QR+iNGXMP661Xcguxl9yeyqXH+tpeXWQxDMNAAG0OYBL4/jct+ikrOS+acvvXEzDO4SP95VIynvHVRtTqVoa51fj7t75Qic8o9IWl0VVbZhm+sswehYYyyfBQAhNEproZNvsnSciIm2EjgRkyZTJD8TlSMupm2Oj7TVEmk90MGzfj6zT0SYaHkYl8gfs0Y++PyKIqGc/8fV/HIay/YEB/AaD+WoH9BYD6ywNczsAM4XOE99d7ThHWXyu4vwDWX4Av7K+/E4vvci3LGOUMAAAAAElFTkSuQmCC"},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAPFBMVEUAAADZ2dnl5eXn5+fo6Ojm5ubp6enq6urg4ODm5ubQ0NDBwcHo6OjExMS9vb2/v7/AwMC8vLy9vb23t7ftsDFCAAAAFHRSTlMAEVuR2P////67J0z///+q+4HR+IsFiaoAAAJsSURBVHgB7ZWHYusgDEUlY4lhsv//Xx+NGtrEhliJ3vbpXvdUAwwbfzg4uJHYe88hDmie7tjfwdHSkUa/QJjABox+CWY/mlTh/DJc8BHeZvRtQYHeLAKz7wvYp7em69uwgQF9B668bqB1Ao+m850LmGz3cy5gZzyAKnhr0HFdvjDabmgVvLNJo04Q9BPQCTgEouy9D9G5NeU4ZT7T1ZBl5vm5I2sF/iYQvEODHZ31iKqgqAboMOgF0qMqKDiDU3wvCPeCHPVL2hHkmaBXA2nzRVAN+ZPhpSXiZcIHdC9gbAheyKdrPlEVdJukz5cKKIghV5K2Rcz9CgqS3y8hKPPrkK+K7wLSrSm3oWu8kIVOj5w6Xw4azQ3D+quCu9zyhWdDQFbFi4AWDXHVUeanZKlgZmg9rh3z2nCBJH5uaAiQldwalKnSbRGMrOIhXuheFolYQa6BM8MADSIrIApEWQT59iKGBA2QdAVk+ewRgiYuKwpoChy0GQ3yCaFDUDQoS76mgNVj6OQTQpdEmvy5gAZ4Aob1/VE3SBjp9fwIa3A9A/X642AdGKnz/2d9/+fgSJnnSCgv5pND0IAuEHGbh/Qg8TpwcjE8Z4xuSLCx8YewK8DPZF/YBL9DgGlJkBCsOBwP+CjA8k0wIpXU3elecDqWzxLYcN5/cJy+BFOJL5zBiJMYzkkE6Sz5J8MpH0Rx/XCQeBmLGUn6VCnVWDNdpIp94TKBPTKK2vy/TlBXs66s8LcMGQ+fmy/vZGXRsPkS/3XQRGE2Csm7fL8qLqZXRaqbI4K6UWZzOOyXruv94U984Pz5z+RNcCnAxsYv5gcBJmV5WFk6DwAAAABJRU5ErkJggg=="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAYFBMVEUAAADT3t/h4eHq6urp6enn5+fp6enq6urm5ubG0NLLy8uds7bo6Ojj4+Pn5+fh4eHe3t4U5/8Y5P0B5f/ExMS+vr4K5v8D3vy+vr4A3/28vLy9vb29vb29vb3BwcG4uLjMVSTuAAAAH3RSTlMAD1uV0e7//7ohOgX///+M/+g6//3/252t9H3R8/+TcABA0AAAAzBJREFUeAHsmAe6rCAMhUWGBO8kwf1v9jbsSonM6+9fwPm/k1jpfnP+Y3r7cICI4HxvXp0+WMAd4IdXxj/wBKB7e9VsPF4BgO4lLSxeA5+gb45/PjAjAHDPxvEQ5gWATWN6wzQwGxp2bbAgiNzu8HR1AjQ3BQ+sE4Brvz7zArCtC0jnN1xKvihoG9KAGgG86TesE7gfXACAmYUQ0XkbaupYrUDGLwPFnZMtbl20AlwEEWtarlGEM+PIIouACPqcoNcLeGTeCD6xrXfxcQnjQUC++TG0hcZPw1aQ7eBuCmYDTST3QOp8gPHbsBeASQhu5Mt3PssiyA5JnR8b8GSglUF7nwFkG0QDUamCU+cDzfksW4HoLlOAXIMpn4WKM7LaeACO+ZGNoK9/VECVIDpKSzCgio8X0VyAZTslX7VlKELzBqJjNbhrQYDq8AgvG5gleYEBJXP+xpEdUfcAFbLJXzaRfVgMoi0QA2VL9oHqlQViHMnRMKQERlGBlrclHeEuSSBFAYkCOhFiWNueRZIFZMj+gCgGlChguxyG6wqk89kUfgK5LV/6roBxUIBivn7Dy4+4lOefyvddDYGz148k8yVUH4Zwej5CqXzuNcc5TJf5X8BlPgfTaTDBsUCaQ/oYhjtHasG7sYjz4aO9OtlyEASiMHwrO6xNBolYiL7/WzYcbKtP5tBmzpdxVb8zC4Ovh2HGTZnVyuB2eL2J1vinOsJBcf4cBRvhENqM6EaB7W9ge5vAejNZ3yTQaKDB9cgcChjaOULlx8jVjnYD5KzDpNVAi6tVcWot/DfAUsd/1UyHyNukqzRQdTbxM51klpDnmRwwuRiED5zlBiXI5USfv5LgCAo8FhpGGeNjQgVvsGPdtG2zRiE97no+ZsdiR8K4Be7tqL9JYFHrGagXmFvlp+mJ9QfOAhFKkcuz+xD1OeF2xxFzYYFlujRDlC7ZZOdcSxcJSuR56dLMgXTJhsRD9TZHUcCEEAZhaAAsQ/xnMJE4v7zgBkeABhJywWFCXRh1VL7gaGB3wSHWABOKaWAHQwPgGwTAGmDcIgAJI8FtApBZ5mOIoEputHKc3jfFjOf29QPQe5BuJum+4AAAAABJRU5ErkJggg=="},function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgBAMAAAAQtmoLAAAAKlBMVEUAAAD81kP71UL71kD81kD81z/91z793Eb4yz/920L92kD92j792Tv92TcDye7yAAAACXRSTlMAH1Bzm8vn/wvXXbMnAAAC1klEQVR4Ae2XNdPbQBiEzeMujE2YmZmZmZm5CmhUhfMTojnXQbkNO12YmaP/El8u1r7S+uSvC31b76Pd907SzaWa9ccq3WPW6j3Hdq0c3rJJ9mLPNY6qyvfV0eknGvszc1z1UyXfL98+0rWR//QexwFQqZwf29gfASqXEomM9jtuDfArmkholV7jAFAa0LpoX6w+DgFa223+M0594OnchEIgADy/Xr9UO6cmFQOeL6sbsMcOPEQE1N4J5cWB59uSJkACgAf8Up11khKeryVgjgAUA/fppRB+l4Cq4i9Ih2SAxxYje64bvnwAHtgbidcbQLzTOVHIBBCwNwKMEY0sCe+kv7hHJGBmA9w1wEe5d1kR4AGoSCAYKoCCXFSvLvAi2Co/NQTUEkoEvJXvhRzBo5kN8F7MLAPQKAZ8w9SZqtHY9S7YgABbl3O1WUNhQIkWKQimhkBeua7ZYxeNGFiKV1V5nn44AtBIAG9CoJ9SntLtwwnQCCOIdR3n1aQQwADepnnVBBOBkdEoBLAR69QveRyAEYIPAjAR2o8ANCJgH54dBpRphOCjBKRK3CgGsJ8baUlA+hFgA/aRv0yNaAb2I4CA9T7WB4Wo0QcBQPCjEQHz4UUhaiRejfHsRwAAvHz9yY8ANBKvd2fuzwHyA8rf4cdzgPhEczBxAAD8BDJkRgAaid9MsWz3I+C7+H0vsBdCwPsUNCAxIFxVqMB+Coj87rPkR0BIyAOleID8FKCPLGhC3I+AGvIueuyyHwFG0WM384zXEwHYNmgD2xGArwdqD7f0I2BbDMjARksqG0EL2Q873gvobB3/dwBrCShuJL8cGbuGse0DyJGh9MGoPxDEx5bwQe2ifhmwDC6agualCaAz0i81N2VRb2M3daDt9ivNJmOP+j/QxFDmUCDaGH3smngtO0T+Rhe5Q8l+Vma29O/s2pTL66ZwXPvlla7Hhz7ietysP1A/ALZC9b117sbZAAAAAElFTkSuQmCC"},function(e,t){e.exports="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOSIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDE5IDIwIj4KICA8cGF0aCBmaWxsPSIjRDhEOEQ4IiBkPSJNMSw4IEMwLjQ0NzcxNTI1LDggMCw3LjU1MjI4NDcgMCw3IEMwLDYuNDQ3NzE1MyAwLjQ0NzcxNTI1LDYgMSw2IEw5LDYgQzkuOTA1MzAxNjcsNi4wMDAxOTMzIDEwLjY5Nzg0MTMsNS4zOTIyNDg2IDEwLjkzMjIzMTEsNC41MTc4MTU5IEMxMS4xNjY2MjA4LDMuNjQzMzgzMiAxMC43ODQ0MDM2LDIuNzIwNTQ3NSAxMC4wMDAzNzYsMi4yNjc5MTk4IEM5LjIxNjM0ODQsMS44MTUyOTIgOC4yMjYwNDgzMSwxLjk0NTc1ODQgNy41ODYsMi41ODYgQzcuMTkzNjIxMTEsMi45NjQ5NzIyIDYuNTY5OTE1MjIsMi45NTk1NTI0IDYuMTg0MTgxNDIsMi41NzM4MTg2IEM1Ljc5ODQ0NzYzLDIuMTg4MDg0OCA1Ljc5MzAyNzc5LDEuNTY0Mzc4OSA2LjE3MiwxLjE3MiBDNy40NTIwOTY2MywtMC4xMDg0ODMyIDkuNDMyNjk2OCwtMC4zNjk0MTU5IDExLjAwMDc1MiwwLjUzNTgzOTUgQzEyLjU2ODgwNzIsMS40NDEwOTUgMTMuMzMzMjQxNywzLjI4Njc2NjQgMTIuODY0NDYyMSw1LjAzNTYzMTggQzEyLjM5NTY4MjUsNi43ODQ0OTczIDEwLjgxMDYwMzMsOC4wMDAzODY2IDksOCBMMSw4IFogTTE2LDEwIEMxNi40NTI2NTA4LDEwLjAwMDA5NjcgMTYuODQ4OTIwNiw5LjY5NjEyNDMgMTYuOTY2MTE1NSw5LjI1ODkwOCBDMTcuMDgzMzEwNCw4LjgyMTY5MTYgMTYuODkyMjAxOCw4LjM2MDI3MzggMTYuNTAwMTg4LDguMTMzOTU5OSBDMTYuMTA4MTc0Miw3LjkwNzY0NiAxNS42MTMwMjQyLDcuOTcyODc5MiAxNS4yOTMsOC4yOTMgQzE0LjkwMDYyMTEsOC42NzE5NzIyIDE0LjI3NjkxNTIsOC42NjY1NTI0IDEzLjg5MTE4MTQsOC4yODA4MTg2IEMxMy41MDU0NDc2LDcuODk1MDg0OCAxMy41MDAwMjc4LDcuMjcxMzc4OSAxMy44NzksNi44NzkgQzE0LjgzOTA3MjUsNS45MTg2Mzc2IDE2LjMyNDUyMjYsNS43MjI5MzggMTcuNTAwNTY0LDYuNDAxODc5NyBDMTguNjc2NjA1NCw3LjA4MDgyMTMgMTkuMjQ5OTMxMyw4LjQ2NTA3NDggMTguODk4MzQ2Niw5Ljc3NjcyMzkgQzE4LjU0Njc2MTksMTEuMDg4MzcyOSAxNy4zNTc5NTI1LDEyLjAwMDI5IDE2LDEyIEwyLDEyIEMxLjQ0NzcxNTI1LDEyIDEsMTEuNTUyMjg0NyAxLDExIEMxLDEwLjQ0NzcxNTMgMS40NDc3MTUyNSwxMCAyLDEwIEwxNiwxMCBaIE0xNSwxNiBMMSwxNiBDMC40NDc3MTUyNSwxNiAwLDE1LjU1MjI4NDcgMCwxNSBDMCwxNC40NDc3MTUzIDAuNDQ3NzE1MjUsMTQgMSwxNCBMMTUsMTQgQzE2LjM1Nzk1MjUsMTMuOTk5NzEgMTcuNTQ2NzYxOSwxNC45MTE2MjcxIDE3Ljg5ODM0NjYsMTYuMjIzMjc2MSBDMTguMjQ5OTMxMywxNy41MzQ5MjUyIDE3LjY3NjYwNTQsMTguOTE5MTc4NyAxNi41MDA1NjQsMTkuNTk4MTIwMyBDMTUuMzI0NTIyNiwyMC4yNzcwNjIgMTMuODM5MDcyNSwyMC4wODEzNjI0IDEyLjg3OSwxOS4xMjEgQzEyLjYxOTA0MzQsMTguODY5OTI1NSAxMi41MTQ3ODc0LDE4LjQ5ODEyMjEgMTIuNjA2MzAyOCwxOC4xNDg0OTI3IEMxMi42OTc4MTgzLDE3Ljc5ODg2MzIgMTIuOTcwODYzMiwxNy41MjU4MTgzIDEzLjMyMDQ5MjcsMTcuNDM0MzAyOCBDMTMuNjcwMTIyMSwxNy4zNDI3ODc0IDE0LjA0MTkyNTUsMTcuNDQ3MDQzNCAxNC4yOTMsMTcuNzA3IEMxNC42MTMwMjQyLDE4LjAyNzEyMDggMTUuMTA4MTc0MiwxOC4wOTIzNTQgMTUuNTAwMTg4LDE3Ljg2NjA0MDEgQzE1Ljg5MjIwMTgsMTcuNjM5NzI2MiAxNi4wODMzMTA0LDE3LjE3ODMwODQgMTUuOTY2MTE1NSwxNi43NDEwOTIgQzE1Ljg0ODkyMDYsMTYuMzAzODc1NyAxNS40NTI2NTA4LDE1Ljk5OTkwMzMgMTUsMTYgWiIvPgo8L3N2Zz4K"},function(e,t){e.exports="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBmaWxsPSIjMDBFNUZGIiBkPSJNMTIsMSBDNy43Mzg3NSw3LjI5NzM3NSA1LDExLjIzNjYyNSA1LDE1LjA2NTYyNSBDNSwxOC44OTYzNzUgOC4xMzE2MjUsMjIgMTIsMjIgQzE1Ljg2ODM3NSwyMiAxOSwxOC44OTYzNzUgMTksMTUuMDY1NjI1IEMxOSwxMS4yMzY2MjUgMTYuMjYxMjUsNy4yOTczNzUgMTIsMSBaIi8+Cjwvc3ZnPgo="},function(e,t){e.exports="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBmaWxsPSIjRDhEOEQ4IiBkPSJNMTIsMkExMCwxMCAwIDAsMCAyLDEyQTEwLDEwIDAgMCwwIDEyLDIyQTEwLDEwIDAgMCwwIDIyLDEyQTEwLDEwIDAgMCwwIDEyLDJNMTIsNEE4LDggMCAwLDEgMjAsMTJDMjAsMTQuNCAxOSwxNi41IDE3LjMsMThDMTUuOSwxNi43IDE0LDE2IDEyLDE2QzEwLDE2IDguMiwxNi43IDYuNywxOEM1LDE2LjUgNCwxNC40IDQsMTJBOCw4IDAgMCwxIDEyLDRNMTQsNS44OUMxMy42Miw1LjkgMTMuMjYsNi4xNSAxMy4xLDYuNTRMMTEuODEsOS43N0wxMS43MSwxMEMxMSwxMC4xMyAxMC40MSwxMC42IDEwLjE0LDExLjI2QzkuNzMsMTIuMjkgMTAuMjMsMTMuNDUgMTEuMjYsMTMuODZDMTIuMjksMTQuMjcgMTMuNDUsMTMuNzcgMTMuODYsMTIuNzRDMTQuMTIsMTIuMDggMTQsMTEuMzIgMTMuNTcsMTAuNzZMMTMuNjcsMTAuNUwxNC45Niw3LjI5TDE0Ljk3LDcuMjZDMTUuMTcsNi43NSAxNC45Miw2LjE3IDE0LjQxLDUuOTZDMTQuMjgsNS45MSAxNC4xNSw1Ljg5IDE0LDUuODlNMTAsNkExLDEgMCAwLDAgOSw3QTEsMSAwIDAsMCAxMCw4QTEsMSAwIDAsMCAxMSw3QTEsMSAwIDAsMCAxMCw2TTcsOUExLDEgMCAwLDAgNiwxMEExLDEgMCAwLDAgNywxMUExLDEgMCAwLDAgOCwxMEExLDEgMCAwLDAgNyw5TTE3LDlBMSwxIDAgMCwwIDE2LDEwQTEsMSAwIDAsMCAxNywxMUExLDEgMCAwLDAgMTgsMTBBMSwxIDAgMCwwIDE3LDlaIj48L3BhdGg+Cjwvc3ZnPgo="},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=function(e){return e(r||(t=["\n ha-card {\n display: flex;\n flex-flow: row;\n align-items: center;\n padding: 16px;\n color: var(--primary-text-color, #000);\n font-weight: var(--swc-font-weight, 400);\n transition: background 1s;\n cursor: pointer;\n }\n ha-card[bg] {\n font-weight: var(--swc-font-weight, 500);\n background: var(--day-color);\n color: var(--text-color);\n }\n ha-card[bg][night] {\n background: var(--night-color);\n }\n ha-card[bg][fade] {\n background: linear-gradient(var(--day-color), transparent 250%);\n }\n ha-card[bg][fade][night] {\n background: linear-gradient(var(--night-color) 0%, transparent 300%);\n }\n .weather__icon {\n height: 40px;\n width: 40px;\n background-size: contain;\n background-repeat: no-repeat;\n flex: 0 0 40px;\n color: white;\n margin-right: 16px;\n }\n .weather__icon--small {\n display: inline-block;\n height: 1em;\n width: 1em;\n min-width: 1em;\n flex: initial;\n margin: 0 .2em;\n }\n .weather__info {\n display: flex;\n flex-flow: column;\n justify-content: space-between;\n min-height: 42px;\n min-width: 0;\n }\n .weather__info__row {\n display: flex;\n align-items: center;\n max-width: 100%;\n }\n .weather__info__item {\n padding-left: 8px;\n display: flex;\n align-items: center;\n }\n .weather__info--add {\n padding-left: 8px;\n margin-left: auto;\n align-items: flex-end;\n }\n .weather__info__state,\n .weather__info__title,\n .weather__info__row {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n "],n||(n=t.slice(0)),r=Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(n)}}))));var t,n};t.default=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.handleClick=void 0;t.handleClick=(e,t,n,r)=>{let i;switch(r.action){case"more-info":i=new Event("hass-more-info",{composed:!0}),i.detail={entityId:r.entity||n.entity},e.dispatchEvent(i);break;case"navigate":if(!r.navigation_path)return;history.pushState(null,"",r.navigation_path),i=new Event("location-changed",{composed:!0}),i.detail={replace:!1},window.dispatchEvent(i);break;case"call-service":if(!r.service)return;const[o,s]=r.service.split(".",2),A={...r.service_data};t.callService(o,s,A)}}},function(e,t,n){"use strict";n.r(t),n.d(t,"customElement",(function(){return r})),n.d(t,"property",(function(){return o})),n.d(t,"state",(function(){return s})),n.d(t,"eventOptions",(function(){return a})),n.d(t,"query",(function(){return c})),n.d(t,"queryAll",(function(){return u})),n.d(t,"queryAsync",(function(){return l})),n.d(t,"queryAssignedElements",(function(){return g})),n.d(t,"queryAssignedNodes",(function(){return M})); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const r=e=>t=>"function"==typeof t?((e,t)=>(customElements.define(e,t),t))(e,t):((e,t)=>{const{kind:n,elements:r}=t;return{kind:n,elements:r,finisher(t){customElements.define(e,t)}}})(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,i=(e,t)=>"method"===t.kind&&t.descriptor&&!("value"in t.descriptor)?{...t,finisher(n){n.createProperty(t.key,e)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:t.key,initializer(){"function"==typeof t.initializer&&(this[t.key]=t.initializer.call(this))},finisher(n){n.createProperty(t.key,e)}};function o(e){return(t,n)=>void 0!==n?((e,t,n)=>{t.constructor.createProperty(n,e)})(e,t,n):i(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function s(e){return o({...e,state:!0})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const A=({finisher:e,descriptor:t})=>(n,r)=>{var i;if(void 0===r){const r=null!==(i=n.originalKey)&&void 0!==i?i:n.key,o=null!=t?{kind:"method",placement:"prototype",key:r,descriptor:t(n.key)}:{...n,key:r};return null!=e&&(o.finisher=function(t){e(t,r)}),o}{const i=n.constructor;void 0!==t&&Object.defineProperty(n,r,t(r)),null==e||e(i,r)}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;function a(e){return A({finisher:(t,n)=>{Object.assign(t.prototype[n],e)}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function c(e,t){return A({descriptor:n=>{const r={get(){var t,n;return null!==(n=null===(t=this.renderRoot)||void 0===t?void 0:t.querySelector(e))&&void 0!==n?n:null},enumerable:!0,configurable:!0};if(t){const t="symbol"==typeof n?Symbol():"__"+n;r.get=function(){var n,r;return void 0===this[t]&&(this[t]=null!==(r=null===(n=this.renderRoot)||void 0===n?void 0:n.querySelector(e))&&void 0!==r?r:null),this[t]}}return r}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function u(e){return A({descriptor:t=>({get(){var t,n;return null!==(n=null===(t=this.renderRoot)||void 0===t?void 0:t.querySelectorAll(e))&&void 0!==n?n:[]},enumerable:!0,configurable:!0})})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function l(e){return A({descriptor:t=>({async get(){var t;return await this.updateComplete,null===(t=this.renderRoot)||void 0===t?void 0:t.querySelector(e)},enumerable:!0,configurable:!0})})} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var d;const f=null!=(null===(d=window.HTMLSlotElement)||void 0===d?void 0:d.prototype.assignedElements)?(e,t)=>e.assignedElements(t):(e,t)=>e.assignedNodes(t).filter(e=>e.nodeType===Node.ELEMENT_NODE);function g(e){const{slot:t,selector:n}=null!=e?e:{};return A({descriptor:r=>({get(){var r;const i="slot"+(t?`[name=${t}]`:":not([name])"),o=null===(r=this.renderRoot)||void 0===r?void 0:r.querySelector(i),s=null!=o?f(o,e):[];return n?s.filter(e=>e.matches(n)):s},enumerable:!0,configurable:!0})})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function M(e,t,n){let r,i=e;return"object"==typeof e?(i=e.slot,r=e):r={flatten:t},n?g({slot:i,flatten:t,selector:n}):A({descriptor:e=>({get(){var e,t;const n="slot"+(i?`[name=${i}]`:":not([name])"),o=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(n);return null!==(t=null==o?void 0:o.assignedNodes(r))&&void 0!==t?t:[]},enumerable:!0,configurable:!0})})}}]); diff --git a/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/weather-radar-card/weather-radar-card.js b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/weather-radar-card/weather-radar-card.js new file mode 100644 index 0000000..5c0e0fb --- /dev/null +++ b/homeassistant/config/custom_components/ui_lovelace_minimalist/cards/weather-radar-card/weather-radar-card.js @@ -0,0 +1,1334 @@ +var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;d--)(r=e[d])&&(o=(a<3?r(o):a>3?r(t,i,o):r(t,i))||o);return a>3&&o&&Object.defineProperty(t,i,o),o}function r(e){var t="function"==typeof Symbol&&Symbol.iterator,i=t&&e[t],n=0;if(i)return i.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")} +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const a=window,o=a.ShadowRoot&&(void 0===a.ShadyCSS||a.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,d=Symbol(),l=new WeakMap;class s{constructor(e,t,i){if(this._$cssResult$=!0,i!==d)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(o&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=l.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&l.set(t,e))}return e}toString(){return this.cssText}}const c=(e,...t)=>{const i=1===e.length?e[0]:t.reduce(((t,i,n)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[n+1]),e[0]);return new s(i,e,d)},m=(e,t)=>{o?e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):t.forEach((t=>{const i=document.createElement("style"),n=a.litNonce;void 0!==n&&i.setAttribute("nonce",n),i.textContent=t.cssText,e.appendChild(i)}))},p=o?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new s("string"==typeof e?e:e+"",void 0,d))(t)})(e):e +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var h;const u=window,f=u.trustedTypes,g=f?f.emptyScript:"",b=u.reactiveElementPolyfillSupport,v={toAttribute(e,t){switch(t){case Boolean:e=e?g:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},x=(e,t)=>t!==e&&(t==t||e==e),_={attribute:!0,type:String,converter:v,reflect:!1,hasChanged:x};class y extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(e){var t;this.finalize(),(null!==(t=this.h)&&void 0!==t?t:this.h=[]).push(e)}static get observedAttributes(){this.finalize();const e=[];return this.elementProperties.forEach(((t,i)=>{const n=this._$Ep(i,t);void 0!==n&&(this._$Ev.set(n,i),e.push(n))})),e}static createProperty(e,t=_){if(t.state&&(t.attribute=!1),this.finalize(),this.elementProperties.set(e,t),!t.noAccessor&&!this.prototype.hasOwnProperty(e)){const i="symbol"==typeof e?Symbol():"__"+e,n=this.getPropertyDescriptor(e,i,t);void 0!==n&&Object.defineProperty(this.prototype,e,n)}}static getPropertyDescriptor(e,t,i){return{get(){return this[t]},set(n){const r=this[e];this[t]=n,this.requestUpdate(e,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)||_}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const e=Object.getPrototypeOf(this);if(e.finalize(),void 0!==e.h&&(this.h=[...e.h]),this.elementProperties=new Map(e.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const e=this.properties,t=[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)];for(const i of t)this.createProperty(i,e[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(p(e))}else void 0!==e&&t.push(p(e));return t}static _$Ep(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}u(){var e;this._$E_=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(e=this.constructor.h)||void 0===e||e.forEach((e=>e(this)))}addController(e){var t,i;(null!==(t=this._$ES)&&void 0!==t?t:this._$ES=[]).push(e),void 0!==this.renderRoot&&this.isConnected&&(null===(i=e.hostConnected)||void 0===i||i.call(e))}removeController(e){var t;null===(t=this._$ES)||void 0===t||t.splice(this._$ES.indexOf(e)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((e,t)=>{this.hasOwnProperty(t)&&(this._$Ei.set(t,this[t]),delete this[t])}))}createRenderRoot(){var e;const t=null!==(e=this.shadowRoot)&&void 0!==e?e:this.attachShadow(this.constructor.shadowRootOptions);return m(t,this.constructor.elementStyles),t}connectedCallback(){var e;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostConnected)||void 0===t?void 0:t.call(e)}))}enableUpdating(e){}disconnectedCallback(){var e;null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostDisconnected)||void 0===t?void 0:t.call(e)}))}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$EO(e,t,i=_){var n;const r=this.constructor._$Ep(e,i);if(void 0!==r&&!0===i.reflect){const a=(void 0!==(null===(n=i.converter)||void 0===n?void 0:n.toAttribute)?i.converter:v).toAttribute(t,i.type);this._$El=e,null==a?this.removeAttribute(r):this.setAttribute(r,a),this._$El=null}}_$AK(e,t){var i;const n=this.constructor,r=n._$Ev.get(e);if(void 0!==r&&this._$El!==r){const e=n.getPropertyOptions(r),a="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==(null===(i=e.converter)||void 0===i?void 0:i.fromAttribute)?e.converter:v;this._$El=r,this[r]=a.fromAttribute(t,e.type),this._$El=null}}requestUpdate(e,t,i){let n=!0;void 0!==e&&(((i=i||this.constructor.getPropertyOptions(e)).hasChanged||x)(this[e],t)?(this._$AL.has(e)||this._$AL.set(e,t),!0===i.reflect&&this._$El!==e&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(e,i))):n=!1),!this.isUpdatePending&&n&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var e;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((e,t)=>this[t]=e)),this._$Ei=void 0);let t=!1;const i=this._$AL;try{t=this.shouldUpdate(i),t?(this.willUpdate(i),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostUpdate)||void 0===t?void 0:t.call(e)})),this.update(i)):this._$Ek()}catch(e){throw t=!1,this._$Ek(),e}t&&this._$AE(i)}willUpdate(e){}_$AE(e){var t;null===(t=this._$ES)||void 0===t||t.forEach((e=>{var t;return null===(t=e.hostUpdated)||void 0===t?void 0:t.call(e)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(e){return!0}update(e){void 0!==this._$EC&&(this._$EC.forEach(((e,t)=>this._$EO(t,this[t],e))),this._$EC=void 0),this._$Ek()}updated(e){}firstUpdated(e){}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var w;y.finalized=!0,y.elementProperties=new Map,y.elementStyles=[],y.shadowRootOptions={mode:"open"},null==b||b({ReactiveElement:y}),(null!==(h=u.reactiveElementVersions)&&void 0!==h?h:u.reactiveElementVersions=[]).push("1.4.2");const E=window,T=E.trustedTypes,I=T?T.createPolicy("lit-html",{createHTML:e=>e}):void 0,A=`lit$${(Math.random()+"").slice(9)}$`,C="?"+A,S=`<${C}>`,R=document,k=(e="")=>R.createComment(e),O=e=>null===e||"object"!=typeof e&&"function"!=typeof e,L=Array.isArray,F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,D=/-->/g,$=/>/g,M=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),N=/'/g,H=/"/g,B=/^(?:script|style|textarea|title)$/i,P=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),z=Symbol.for("lit-noChange"),V=Symbol.for("lit-nothing"),U=new WeakMap,X=R.createTreeWalker(R,129,null,!1),j=(e,t)=>{const i=e.length-1,n=[];let r,a=2===t?"":"",o=F;for(let t=0;t"===l[0]?(o=null!=r?r:F,s=-1):void 0===l[1]?s=-2:(s=o.lastIndex-l[2].length,d=l[1],o=void 0===l[3]?M:'"'===l[3]?H:N):o===H||o===N?o=M:o===D||o===$?o=F:(o=M,r=void 0);const m=o===M&&e[t+1].startsWith("/>")?" ":"";a+=o===F?i+S:s>=0?(n.push(d),i.slice(0,s)+"$lit$"+i.slice(s)+A+m):i+A+(-2===s?(n.push(void 0),t):m)}const d=a+(e[i]||"")+(2===t?"":"");if(!Array.isArray(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==I?I.createHTML(d):d,n]};class W{constructor({strings:e,_$litType$:t},i){let n;this.parts=[];let r=0,a=0;const o=e.length-1,d=this.parts,[l,s]=j(e,t);if(this.el=W.createElement(l,i),X.currentNode=this.el.content,2===t){const e=this.el.content,t=e.firstChild;t.remove(),e.append(...t.childNodes)}for(;null!==(n=X.nextNode())&&d.length0){n.textContent=T?T.emptyScript:"";for(let i=0;iL(e)||"function"==typeof(null==e?void 0:e[Symbol.iterator]))(e)?this.k(e):this.g(e)}O(e,t=this._$AB){return this._$AA.parentNode.insertBefore(e,t)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}g(e){this._$AH!==V&&O(this._$AH)?this._$AA.nextSibling.data=e:this.T(R.createTextNode(e)),this._$AH=e}$(e){var t;const{values:i,_$litType$:n}=e,r="number"==typeof n?this._$AC(e):(void 0===n.el&&(n.el=W.createElement(n.h,this.options)),n);if((null===(t=this._$AH)||void 0===t?void 0:t._$AD)===r)this._$AH.p(i);else{const e=new q(r,this),t=e.v(this.options);e.p(i),this.T(t),this._$AH=e}}_$AC(e){let t=U.get(e.strings);return void 0===t&&U.set(e.strings,t=new W(e)),t}k(e){L(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,n=0;for(const r of e)n===t.length?t.push(i=new Y(this.O(k()),this.O(k()),this,this.options)):i=t[n],i._$AI(r),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=V}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(e,t=this,i,n){const r=this.strings;let a=!1;if(void 0===r)e=G(this,e,t,0),a=!O(e)||e!==this._$AH&&e!==z,a&&(this._$AH=e);else{const n=e;let o,d;for(e=r[0],o=0;o{var n,r;const a=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:t;let o=a._$litPart$;if(void 0===o){const e=null!==(r=null==i?void 0:i.renderBefore)&&void 0!==r?r:null;a._$litPart$=o=new Y(t.insertBefore(k(),e),e,void 0,null!=i?i:{})}return o._$AI(e),o})(t,this.renderRoot,this.renderOptions)}connectedCallback(){var e;super.connectedCallback(),null===(e=this._$Do)||void 0===e||e.setConnected(!0)}disconnectedCallback(){var e;super.disconnectedCallback(),null===(e=this._$Do)||void 0===e||e.setConnected(!1)}render(){return z}}ae.finalized=!0,ae._$litElement$=!0,null===(ne=globalThis.litElementHydrateSupport)||void 0===ne||ne.call(globalThis,{LitElement:ae});const oe=globalThis.litElementPolyfillSupport;null==oe||oe({LitElement:ae}),(null!==(re=globalThis.litElementVersions)&&void 0!==re?re:globalThis.litElementVersions=[]).push("3.2.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const de=e=>t=>"function"==typeof t?((e,t)=>(customElements.define(e,t),t))(e,t):((e,t)=>{const{kind:i,elements:n}=t;return{kind:i,elements:n,finisher(t){customElements.define(e,t)}}})(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,le=(e,t)=>"method"===t.kind&&t.descriptor&&!("value"in t.descriptor)?{...t,finisher(i){i.createProperty(t.key,e)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:t.key,initializer(){"function"==typeof t.initializer&&(this[t.key]=t.initializer.call(this))},finisher(i){i.createProperty(t.key,e)}};function se(e){return(t,i)=>void 0!==i?((e,t,i)=>{t.constructor.createProperty(i,e)})(e,t,i):le(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function ce(e){return se({...e,state:!0})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const me=({finisher:e,descriptor:t})=>(i,n)=>{var r;if(void 0===n){const n=null!==(r=i.originalKey)&&void 0!==r?r:i.key,a=null!=t?{kind:"method",placement:"prototype",key:n,descriptor:t(i.key)}:{...i,key:n};return null!=e&&(a.finisher=function(t){e(t,n)}),a}{const r=i.constructor;void 0!==t&&Object.defineProperty(i,n,t(n)),null==e||e(r,n)}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;function pe(e){return me({finisher:(t,i)=>{Object.assign(t.prototype[i],e)}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function he(e,t){return me({descriptor:i=>{const n={get(){var t,i;return null!==(i=null===(t=this.renderRoot)||void 0===t?void 0:t.querySelector(e))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(t){const t="symbol"==typeof i?Symbol():"__"+i;n.get=function(){var i,n;return void 0===this[t]&&(this[t]=null!==(n=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(e))&&void 0!==n?n:null),this[t]}}return n}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function ue(e){return me({descriptor:t=>({async get(){var t;return await this.updateComplete,null===(t=this.renderRoot)||void 0===t?void 0:t.querySelector(e)},enumerable:!0,configurable:!0})})} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var fe;const ge=null!=(null===(fe=window.HTMLSlotElement)||void 0===fe?void 0:fe.prototype.assignedElements)?(e,t)=>e.assignedElements(t):(e,t)=>e.assignedNodes(t).filter((e=>e.nodeType===Node.ELEMENT_NODE)); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +function be(e,t,i){let n,r=e;return"object"==typeof e?(r=e.slot,n=e):n={flatten:t},i?function(e){const{slot:t,selector:i}=null!=e?e:{};return me({descriptor:n=>({get(){var n;const r="slot"+(t?`[name=${t}]`:":not([name])"),a=null===(n=this.renderRoot)||void 0===n?void 0:n.querySelector(r),o=null!=a?ge(a,e):[];return i?o.filter((e=>e.matches(i))):o},enumerable:!0,configurable:!0})})}({slot:r,flatten:t,selector:i}):me({descriptor:e=>({get(){var e,t;const i="slot"+(r?`[name=${r}]`:":not([name])"),a=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(i);return null!==(t=null==a?void 0:a.assignedNodes(n))&&void 0!==t?t:[]},enumerable:!0,configurable:!0})})}var ve,xe;!function(e){e.language="language",e.system="system",e.comma_decimal="comma_decimal",e.decimal_comma="decimal_comma",e.space_comma="space_comma",e.none="none"}(ve||(ve={})),function(e){e.language="language",e.system="system",e.am_pm="12",e.twenty_four="24"}(xe||(xe={}));var _e=function(e,t,i,n){n=n||{},i=null==i?{}:i;var r=new Event(t,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,e.dispatchEvent(r),r +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var ye=function(){function e(e){void 0===e&&(e={}),this.adapter=e}return Object.defineProperty(e,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),e.prototype.init=function(){},e.prototype.destroy=function(){},e}(),we={ROOT:"mdc-form-field"},Ee={LABEL_SELECTOR:".mdc-form-field > label"},Te=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.click=function(){r.handleClick()},r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return we},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return Ee},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{activateInputRipple:function(){},deactivateInputRipple:function(){},deregisterInteractionHandler:function(){},registerInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){this.adapter.registerInteractionHandler("click",this.click)},n.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("click",this.click)},n.prototype.handleClick=function(){var e=this;this.adapter.activateInputRipple(),requestAnimationFrame((function(){e.adapter.deactivateInputRipple()}))},n}(ye); +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const Ie=e=>e.nodeType===Node.ELEMENT_NODE;function Ae(e){return{addClass:t=>{e.classList.add(t)},removeClass:t=>{e.classList.remove(t)},hasClass:t=>e.classList.contains(t)}}const Ce=()=>{},Se={get passive(){return!1}};document.addEventListener("x",Ce,Se),document.removeEventListener("x",Ce);const Re=(e=window.document)=>{let t=e.activeElement;const i=[];if(!t)return i;for(;t&&(i.push(t),t.shadowRoot);)t=t.shadowRoot.activeElement;return i},ke=e=>{const t=Re();if(!t.length)return!1;const i=t[t.length-1],n=new Event("check-if-focused",{bubbles:!0,composed:!0});let r=[];const a=e=>{r=e.composedPath()};return document.body.addEventListener("check-if-focused",a),i.dispatchEvent(n),document.body.removeEventListener("check-if-focused",a),-1!==r.indexOf(e)}; +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class Oe extends ae{click(){if(this.mdcRoot)return this.mdcRoot.focus(),void this.mdcRoot.click();super.click()}createFoundation(){void 0!==this.mdcFoundation&&this.mdcFoundation.destroy(),this.mdcFoundationClass&&(this.mdcFoundation=new this.mdcFoundationClass(this.createAdapter()),this.mdcFoundation.init())}firstUpdated(){this.createFoundation()}} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */var Le,Fe;const De=null!==(Fe=null===(Le=window.ShadyDOM)||void 0===Le?void 0:Le.inUse)&&void 0!==Fe&&Fe;class $e extends Oe{constructor(){super(...arguments),this.disabled=!1,this.containingForm=null,this.formDataListener=e=>{this.disabled||this.setFormData(e.formData)}}findFormElement(){if(!this.shadowRoot||De)return null;const e=this.getRootNode().querySelectorAll("form");for(const t of Array.from(e))if(t.contains(this))return t;return null}connectedCallback(){var e;super.connectedCallback(),this.containingForm=this.findFormElement(),null===(e=this.containingForm)||void 0===e||e.addEventListener("formdata",this.formDataListener)}disconnectedCallback(){var e;super.disconnectedCallback(),null===(e=this.containingForm)||void 0===e||e.removeEventListener("formdata",this.formDataListener),this.containingForm=null}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(e=>{this.dispatchEvent(new Event("change",e))}))}}$e.shadowRootOptions={mode:"open",delegatesFocus:!0},n([se({type:Boolean})],$e.prototype,"disabled",void 0); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const Me=e=>(t,i)=>{if(t.constructor._observers){if(!t.constructor.hasOwnProperty("_observers")){const e=t.constructor._observers;t.constructor._observers=new Map,e.forEach(((e,i)=>t.constructor._observers.set(i,e)))}}else{t.constructor._observers=new Map;const e=t.updated;t.updated=function(t){e.call(this,t),t.forEach(((e,t)=>{const i=this.constructor._observers.get(t);void 0!==i&&i.call(this,this[t],e)}))}}t.constructor._observers.set(i,e)} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,Ne=1,He=3,Be=4,Pe=e=>(...t)=>({_$litDirective$:e,values:t});class ze{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const Ve=Pe(class extends ze{constructor(e){var t;if(super(e),e.type!==Ne||"class"!==e.name||(null===(t=e.strings)||void 0===t?void 0:t.length)>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(e){return" "+Object.keys(e).filter((t=>e[t])).join(" ")+" "}update(e,[t]){var i,n;if(void 0===this.nt){this.nt=new Set,void 0!==e.strings&&(this.st=new Set(e.strings.join(" ").split(/\s/).filter((e=>""!==e))));for(const e in t)t[e]&&!(null===(i=this.st)||void 0===i?void 0:i.has(e))&&this.nt.add(e);return this.render(t)}const r=e.element.classList;this.nt.forEach((e=>{e in t||(r.remove(e),this.nt.delete(e))}));for(const e in t){const i=!!t[e];i===this.nt.has(e)||(null===(n=this.st)||void 0===n?void 0:n.has(e))||(i?(r.add(e),this.nt.add(e)):(r.remove(e),this.nt.delete(e)))}return z}}); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class Ue extends Oe{constructor(){super(...arguments),this.alignEnd=!1,this.spaceBetween=!1,this.nowrap=!1,this.label="",this.mdcFoundationClass=Te}createAdapter(){return{registerInteractionHandler:(e,t)=>{this.labelEl.addEventListener(e,t)},deregisterInteractionHandler:(e,t)=>{this.labelEl.removeEventListener(e,t)},activateInputRipple:async()=>{const e=this.input;if(e instanceof $e){const t=await e.ripple;t&&t.startPress()}},deactivateInputRipple:async()=>{const e=this.input;if(e instanceof $e){const t=await e.ripple;t&&t.endPress()}}}}get input(){var e,t;return null!==(t=null===(e=this.slottedInputs)||void 0===e?void 0:e[0])&&void 0!==t?t:null}render(){const e={"mdc-form-field--align-end":this.alignEnd,"mdc-form-field--space-between":this.spaceBetween,"mdc-form-field--nowrap":this.nowrap};return P` +
+ + +
`}click(){this._labelClick()}_labelClick(){const e=this.input;e&&(e.focus(),e.click())}}n([se({type:Boolean})],Ue.prototype,"alignEnd",void 0),n([se({type:Boolean})],Ue.prototype,"spaceBetween",void 0),n([se({type:Boolean})],Ue.prototype,"nowrap",void 0),n([se({type:String}),Me((async function(e){var t;null===(t=this.input)||void 0===t||t.setAttribute("aria-label",e)}))],Ue.prototype,"label",void 0),n([he(".mdc-form-field")],Ue.prototype,"mdcRoot",void 0),n([be("",!0,"*")],Ue.prototype,"slottedInputs",void 0),n([he("label")],Ue.prototype,"labelEl",void 0); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const Xe=c`.mdc-form-field{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));display:inline-flex;align-items:center;vertical-align:middle}.mdc-form-field>label{margin-left:0;margin-right:auto;padding-left:4px;padding-right:0;order:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{margin-left:auto;margin-right:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{padding-left:0;padding-right:4px}.mdc-form-field--nowrap>label{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.mdc-form-field--align-end>label{margin-left:auto;margin-right:0;padding-left:0;padding-right:4px;order:-1}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{padding-left:4px;padding-right:0}.mdc-form-field--space-between{justify-content:space-between}.mdc-form-field--space-between>label{margin:0}[dir=rtl] .mdc-form-field--space-between>label,.mdc-form-field--space-between>label[dir=rtl]{margin:0}:host{display:inline-flex}.mdc-form-field{width:100%}::slotted(*){-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}::slotted(mwc-switch){margin-right:10px}[dir=rtl] ::slotted(mwc-switch),::slotted(mwc-switch[dir=rtl]){margin-left:10px}`,je={"mwc-formfield":class extends Ue{static get styles(){return Xe}}}; +/** + * @license + * Copyright 2020 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var We="Unknown",Ge="Backspace",qe="Enter",Ye="Spacebar",Ke="PageUp",Ze="PageDown",Qe="End",Je="Home",et="ArrowLeft",tt="ArrowUp",it="ArrowRight",nt="ArrowDown",rt="Delete",at="Escape",ot="Tab",dt=new Set;dt.add(Ge),dt.add(qe),dt.add(Ye),dt.add(Ke),dt.add(Ze),dt.add(Qe),dt.add(Je),dt.add(et),dt.add(tt),dt.add(it),dt.add(nt),dt.add(rt),dt.add(at),dt.add(ot);var lt=8,st=13,ct=32,mt=33,pt=34,ht=35,ut=36,ft=37,gt=38,bt=39,vt=40,xt=46,_t=27,yt=9,wt=new Map;wt.set(lt,Ge),wt.set(st,qe),wt.set(ct,Ye),wt.set(mt,Ke),wt.set(pt,Ze),wt.set(ht,Qe),wt.set(ut,Je),wt.set(ft,et),wt.set(gt,tt),wt.set(bt,it),wt.set(vt,nt),wt.set(xt,rt),wt.set(_t,at),wt.set(yt,ot);var Et,Tt,It=new Set;function At(e){var t=e.key;if(dt.has(t))return t;var i=wt.get(e.keyCode);return i||We} +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */It.add(Ke),It.add(Ze),It.add(Qe),It.add(Je),It.add(et),It.add(tt),It.add(it),It.add(nt);var Ct="mdc-list-item--activated",St="mdc-list-item",Rt="mdc-list-item--disabled",kt="mdc-list-item--selected",Ot="mdc-list-item__text",Lt="mdc-list-item__primary-text",Ft="mdc-list";(Et={})[""+Ct]="mdc-list-item--activated",Et[""+St]="mdc-list-item",Et[""+Rt]="mdc-list-item--disabled",Et[""+kt]="mdc-list-item--selected",Et[""+Lt]="mdc-list-item__primary-text",Et[""+Ft]="mdc-list";var Dt=((Tt={})[""+Ct]="mdc-deprecated-list-item--activated",Tt[""+St]="mdc-deprecated-list-item",Tt[""+Rt]="mdc-deprecated-list-item--disabled",Tt[""+kt]="mdc-deprecated-list-item--selected",Tt[""+Ot]="mdc-deprecated-list-item__text",Tt[""+Lt]="mdc-deprecated-list-item__primary-text",Tt[""+Ft]="mdc-deprecated-list",Tt),$t={ACTION_EVENT:"MDCList:action",ARIA_CHECKED:"aria-checked",ARIA_CHECKED_CHECKBOX_SELECTOR:'[role="checkbox"][aria-checked="true"]',ARIA_CHECKED_RADIO_SELECTOR:'[role="radio"][aria-checked="true"]',ARIA_CURRENT:"aria-current",ARIA_DISABLED:"aria-disabled",ARIA_ORIENTATION:"aria-orientation",ARIA_ORIENTATION_HORIZONTAL:"horizontal",ARIA_ROLE_CHECKBOX_SELECTOR:'[role="checkbox"]',ARIA_SELECTED:"aria-selected",ARIA_INTERACTIVE_ROLES_SELECTOR:'[role="listbox"], [role="menu"]',ARIA_MULTI_SELECTABLE_SELECTOR:'[aria-multiselectable="true"]',CHECKBOX_RADIO_SELECTOR:'input[type="checkbox"], input[type="radio"]',CHECKBOX_SELECTOR:'input[type="checkbox"]',CHILD_ELEMENTS_TO_TOGGLE_TABINDEX:"\n ."+St+" button:not(:disabled),\n ."+St+" a,\n ."+Dt[St]+" button:not(:disabled),\n ."+Dt[St]+" a\n ",DEPRECATED_SELECTOR:".mdc-deprecated-list",FOCUSABLE_CHILD_ELEMENTS:"\n ."+St+" button:not(:disabled),\n ."+St+" a,\n ."+St+' input[type="radio"]:not(:disabled),\n .'+St+' input[type="checkbox"]:not(:disabled),\n .'+Dt[St]+" button:not(:disabled),\n ."+Dt[St]+" a,\n ."+Dt[St]+' input[type="radio"]:not(:disabled),\n .'+Dt[St]+' input[type="checkbox"]:not(:disabled)\n ',RADIO_SELECTOR:'input[type="radio"]',SELECTED_ITEM_SELECTOR:'[aria-selected="true"], [aria-current="true"]'},Mt={UNSET_INDEX:-1,TYPEAHEAD_BUFFER_CLEAR_TIMEOUT_MS:300},Nt=["input","button","textarea","select"],Ht=function(e){var t=e.target;if(t){var i=(""+t.tagName).toLowerCase();-1===Nt.indexOf(i)&&e.preventDefault()}};function Bt(e,t){for(var i=new Map,n=0;nt&&!i(a[d].index)){l=d;break}if(-1!==l)return n.sortedIndexCursor=l,a[n.sortedIndexCursor].index;return-1}(a,o,l,t):function(e,t,i){var n=i.typeaheadBuffer[0],r=e.get(n);if(!r)return-1;var a=r[i.sortedIndexCursor];if(0===a.text.lastIndexOf(i.typeaheadBuffer,0)&&!t(a.index))return a.index;var o=(i.sortedIndexCursor+1)%r.length,d=-1;for(;o!==i.sortedIndexCursor;){var l=r[o],s=0===l.text.lastIndexOf(i.typeaheadBuffer,0),c=!t(l.index);if(s&&c){d=o;break}o=(o+1)%r.length}if(-1!==d)return i.sortedIndexCursor=d,r[i.sortedIndexCursor].index;return-1}(a,l,t),-1===i||d||r(i),i}function zt(e){return e.typeaheadBuffer.length>0} +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Vt={LABEL_FLOAT_ABOVE:"mdc-floating-label--float-above",LABEL_REQUIRED:"mdc-floating-label--required",LABEL_SHAKE:"mdc-floating-label--shake",ROOT:"mdc-floating-label"},Ut=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.shakeAnimationEndHandler=function(){r.handleShakeAnimationEnd()},r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return Vt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},getWidth:function(){return 0},registerInteractionHandler:function(){},deregisterInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){this.adapter.registerInteractionHandler("animationend",this.shakeAnimationEndHandler)},n.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("animationend",this.shakeAnimationEndHandler)},n.prototype.getWidth=function(){return this.adapter.getWidth()},n.prototype.shake=function(e){var t=n.cssClasses.LABEL_SHAKE;e?this.adapter.addClass(t):this.adapter.removeClass(t)},n.prototype.float=function(e){var t=n.cssClasses,i=t.LABEL_FLOAT_ABOVE,r=t.LABEL_SHAKE;e?this.adapter.addClass(i):(this.adapter.removeClass(i),this.adapter.removeClass(r))},n.prototype.setRequired=function(e){var t=n.cssClasses.LABEL_REQUIRED;e?this.adapter.addClass(t):this.adapter.removeClass(t)},n.prototype.handleShakeAnimationEnd=function(){var e=n.cssClasses.LABEL_SHAKE;this.adapter.removeClass(e)},n}(ye); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */const Xt=Pe(class extends ze{constructor(e){switch(super(e),this.foundation=null,this.previousPart=null,e.type){case Ne:case He:break;default:throw new Error("FloatingLabel directive only support attribute and property parts")}}update(e,[t]){if(e!==this.previousPart){this.foundation&&this.foundation.destroy(),this.previousPart=e;const t=e.element;t.classList.add("mdc-floating-label");const i=(e=>({addClass:t=>e.classList.add(t),removeClass:t=>e.classList.remove(t),getWidth:()=>e.scrollWidth,registerInteractionHandler:(t,i)=>{e.addEventListener(t,i)},deregisterInteractionHandler:(t,i)=>{e.removeEventListener(t,i)}}))(t);this.foundation=new Ut(i),this.foundation.init()}return this.render(t)}render(e){return this.foundation}}); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var jt={LINE_RIPPLE_ACTIVE:"mdc-line-ripple--active",LINE_RIPPLE_DEACTIVATING:"mdc-line-ripple--deactivating"},Wt=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.transitionEndHandler=function(e){r.handleTransitionEnd(e)},r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return jt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},setStyle:function(){},registerEventHandler:function(){},deregisterEventHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){this.adapter.registerEventHandler("transitionend",this.transitionEndHandler)},n.prototype.destroy=function(){this.adapter.deregisterEventHandler("transitionend",this.transitionEndHandler)},n.prototype.activate=function(){this.adapter.removeClass(jt.LINE_RIPPLE_DEACTIVATING),this.adapter.addClass(jt.LINE_RIPPLE_ACTIVE)},n.prototype.setRippleCenter=function(e){this.adapter.setStyle("transform-origin",e+"px center")},n.prototype.deactivate=function(){this.adapter.addClass(jt.LINE_RIPPLE_DEACTIVATING)},n.prototype.handleTransitionEnd=function(e){var t=this.adapter.hasClass(jt.LINE_RIPPLE_DEACTIVATING);"opacity"===e.propertyName&&t&&(this.adapter.removeClass(jt.LINE_RIPPLE_ACTIVE),this.adapter.removeClass(jt.LINE_RIPPLE_DEACTIVATING))},n}(ye); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */const Gt=Pe(class extends ze{constructor(e){switch(super(e),this.previousPart=null,this.foundation=null,e.type){case Ne:case He:return;default:throw new Error("LineRipple only support attribute and property parts.")}}update(e,t){if(this.previousPart!==e){this.foundation&&this.foundation.destroy(),this.previousPart=e;const t=e.element;t.classList.add("mdc-line-ripple");const i=(e=>({addClass:t=>e.classList.add(t),removeClass:t=>e.classList.remove(t),hasClass:t=>e.classList.contains(t),setStyle:(t,i)=>e.style.setProperty(t,i),registerEventHandler:(t,i)=>{e.addEventListener(t,i)},deregisterEventHandler:(t,i)=>{e.removeEventListener(t,i)}}))(t);this.foundation=new Wt(i),this.foundation.init()}return this.render()}render(){return this.foundation}}); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var qt,Yt,Kt={ANCHOR:"mdc-menu-surface--anchor",ANIMATING_CLOSED:"mdc-menu-surface--animating-closed",ANIMATING_OPEN:"mdc-menu-surface--animating-open",FIXED:"mdc-menu-surface--fixed",IS_OPEN_BELOW:"mdc-menu-surface--is-open-below",OPEN:"mdc-menu-surface--open",ROOT:"mdc-menu-surface"},Zt={CLOSED_EVENT:"MDCMenuSurface:closed",CLOSING_EVENT:"MDCMenuSurface:closing",OPENED_EVENT:"MDCMenuSurface:opened",FOCUSABLE_ELEMENTS:["button:not(:disabled)",'[href]:not([aria-disabled="true"])',"input:not(:disabled)","select:not(:disabled)","textarea:not(:disabled)",'[tabindex]:not([tabindex="-1"]):not([aria-disabled="true"])'].join(", ")},Qt={TRANSITION_OPEN_DURATION:120,TRANSITION_CLOSE_DURATION:75,MARGIN_TO_EDGE:32,ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO:.67,TOUCH_EVENT_WAIT_MS:30};!function(e){e[e.BOTTOM=1]="BOTTOM",e[e.CENTER=2]="CENTER",e[e.RIGHT=4]="RIGHT",e[e.FLIP_RTL=8]="FLIP_RTL"}(qt||(qt={})),function(e){e[e.TOP_LEFT=0]="TOP_LEFT",e[e.TOP_RIGHT=4]="TOP_RIGHT",e[e.BOTTOM_LEFT=1]="BOTTOM_LEFT",e[e.BOTTOM_RIGHT=5]="BOTTOM_RIGHT",e[e.TOP_START=8]="TOP_START",e[e.TOP_END=12]="TOP_END",e[e.BOTTOM_START=9]="BOTTOM_START",e[e.BOTTOM_END=13]="BOTTOM_END"}(Yt||(Yt={})); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Jt={ACTIVATED:"mdc-select--activated",DISABLED:"mdc-select--disabled",FOCUSED:"mdc-select--focused",INVALID:"mdc-select--invalid",MENU_INVALID:"mdc-select__menu--invalid",OUTLINED:"mdc-select--outlined",REQUIRED:"mdc-select--required",ROOT:"mdc-select",WITH_LEADING_ICON:"mdc-select--with-leading-icon"},ei={ARIA_CONTROLS:"aria-controls",ARIA_DESCRIBEDBY:"aria-describedby",ARIA_SELECTED_ATTR:"aria-selected",CHANGE_EVENT:"MDCSelect:change",HIDDEN_INPUT_SELECTOR:'input[type="hidden"]',LABEL_SELECTOR:".mdc-floating-label",LEADING_ICON_SELECTOR:".mdc-select__icon",LINE_RIPPLE_SELECTOR:".mdc-line-ripple",MENU_SELECTOR:".mdc-select__menu",OUTLINE_SELECTOR:".mdc-notched-outline",SELECTED_TEXT_SELECTOR:".mdc-select__selected-text",SELECT_ANCHOR_SELECTOR:".mdc-select__anchor",VALUE_ATTR:"data-value"},ti={LABEL_SCALE:.75,UNSET_INDEX:-1,CLICK_DEBOUNCE_TIMEOUT_MS:330},ii=function(e){function n(t,r){void 0===r&&(r={});var a=e.call(this,i(i({},n.defaultAdapter),t))||this;return a.disabled=!1,a.isMenuOpen=!1,a.useDefaultValidation=!0,a.customValidity=!0,a.lastSelectedIndex=ti.UNSET_INDEX,a.clickDebounceTimeout=0,a.recentlyClicked=!1,a.leadingIcon=r.leadingIcon,a.helperText=r.helperText,a}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return Jt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return ti},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return ei},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},activateBottomLine:function(){},deactivateBottomLine:function(){},getSelectedIndex:function(){return-1},setSelectedIndex:function(){},hasLabel:function(){return!1},floatLabel:function(){},getLabelWidth:function(){return 0},setLabelRequired:function(){},hasOutline:function(){return!1},notchOutline:function(){},closeOutline:function(){},setRippleCenter:function(){},notifyChange:function(){},setSelectedText:function(){},isSelectAnchorFocused:function(){return!1},getSelectAnchorAttr:function(){return""},setSelectAnchorAttr:function(){},removeSelectAnchorAttr:function(){},addMenuClass:function(){},removeMenuClass:function(){},openMenu:function(){},closeMenu:function(){},getAnchorElement:function(){return null},setMenuAnchorElement:function(){},setMenuAnchorCorner:function(){},setMenuWrapFocus:function(){},focusMenuItemAtIndex:function(){},getMenuItemCount:function(){return 0},getMenuItemValues:function(){return[]},getMenuItemTextAtIndex:function(){return""},isTypeaheadInProgress:function(){return!1},typeaheadMatchItem:function(){return-1}}},enumerable:!1,configurable:!0}),n.prototype.getSelectedIndex=function(){return this.adapter.getSelectedIndex()},n.prototype.setSelectedIndex=function(e,t,i){void 0===t&&(t=!1),void 0===i&&(i=!1),e>=this.adapter.getMenuItemCount()||(e===ti.UNSET_INDEX?this.adapter.setSelectedText(""):this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(e).trim()),this.adapter.setSelectedIndex(e),t&&this.adapter.closeMenu(),i||this.lastSelectedIndex===e||this.handleChange(),this.lastSelectedIndex=e)},n.prototype.setValue=function(e,t){void 0===t&&(t=!1);var i=this.adapter.getMenuItemValues().indexOf(e);this.setSelectedIndex(i,!1,t)},n.prototype.getValue=function(){var e=this.adapter.getSelectedIndex(),t=this.adapter.getMenuItemValues();return e!==ti.UNSET_INDEX?t[e]:""},n.prototype.getDisabled=function(){return this.disabled},n.prototype.setDisabled=function(e){this.disabled=e,this.disabled?(this.adapter.addClass(Jt.DISABLED),this.adapter.closeMenu()):this.adapter.removeClass(Jt.DISABLED),this.leadingIcon&&this.leadingIcon.setDisabled(this.disabled),this.disabled?this.adapter.removeSelectAnchorAttr("tabindex"):this.adapter.setSelectAnchorAttr("tabindex","0"),this.adapter.setSelectAnchorAttr("aria-disabled",this.disabled.toString())},n.prototype.openMenu=function(){this.adapter.addClass(Jt.ACTIVATED),this.adapter.openMenu(),this.isMenuOpen=!0,this.adapter.setSelectAnchorAttr("aria-expanded","true")},n.prototype.setHelperTextContent=function(e){this.helperText&&this.helperText.setContent(e)},n.prototype.layout=function(){if(this.adapter.hasLabel()){var e=this.getValue().length>0,t=this.adapter.hasClass(Jt.FOCUSED),i=e||t,n=this.adapter.hasClass(Jt.REQUIRED);this.notchOutline(i),this.adapter.floatLabel(i),this.adapter.setLabelRequired(n)}},n.prototype.layoutOptions=function(){var e=this.adapter.getMenuItemValues().indexOf(this.getValue());this.setSelectedIndex(e,!1,!0)},n.prototype.handleMenuOpened=function(){if(0!==this.adapter.getMenuItemValues().length){var e=this.getSelectedIndex(),t=e>=0?e:0;this.adapter.focusMenuItemAtIndex(t)}},n.prototype.handleMenuClosing=function(){this.adapter.setSelectAnchorAttr("aria-expanded","false")},n.prototype.handleMenuClosed=function(){this.adapter.removeClass(Jt.ACTIVATED),this.isMenuOpen=!1,this.adapter.isSelectAnchorFocused()||this.blur()},n.prototype.handleChange=function(){this.layout(),this.adapter.notifyChange(this.getValue()),this.adapter.hasClass(Jt.REQUIRED)&&this.useDefaultValidation&&this.setValid(this.isValid())},n.prototype.handleMenuItemAction=function(e){this.setSelectedIndex(e,!0)},n.prototype.handleFocus=function(){this.adapter.addClass(Jt.FOCUSED),this.layout(),this.adapter.activateBottomLine()},n.prototype.handleBlur=function(){this.isMenuOpen||this.blur()},n.prototype.handleClick=function(e){this.disabled||this.recentlyClicked||(this.setClickDebounceTimeout(),this.isMenuOpen?this.adapter.closeMenu():(this.adapter.setRippleCenter(e),this.openMenu()))},n.prototype.handleKeydown=function(e){if(!this.isMenuOpen&&this.adapter.hasClass(Jt.FOCUSED)){var t=At(e)===qe,i=At(e)===Ye,n=At(e)===tt,r=At(e)===nt;if(!(e.ctrlKey||e.metaKey)&&(!i&&e.key&&1===e.key.length||i&&this.adapter.isTypeaheadInProgress())){var a=i?" ":e.key,o=this.adapter.typeaheadMatchItem(a,this.getSelectedIndex());return o>=0&&this.setSelectedIndex(o),void e.preventDefault()}(t||i||n||r)&&(n&&this.getSelectedIndex()>0?this.setSelectedIndex(this.getSelectedIndex()-1):r&&this.getSelectedIndex()null!=e?e:V +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */,ri=(e={})=>{const t={};for(const i in e)t[i]=e[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},t)};class ai extends $e{constructor(){super(...arguments),this.mdcFoundationClass=ii,this.disabled=!1,this.outlined=!1,this.label="",this.outlineOpen=!1,this.outlineWidth=0,this.value="",this.name="",this.selectedText="",this.icon="",this.menuOpen=!1,this.helper="",this.validateOnInitialRender=!1,this.validationMessage="",this.required=!1,this.naturalMenuWidth=!1,this.isUiValid=!0,this.fixedMenuPosition=!1,this.typeaheadState={bufferClearTimeout:0,currentFirstChar:"",sortedIndexCursor:0,typeaheadBuffer:""},this.sortedIndexByFirstChar=new Map,this.menuElement_=null,this.listeners=[],this.onBodyClickBound=()=>{},this._menuUpdateComplete=null,this.valueSetDirectly=!1,this.validityTransform=null,this._validity=ri()}get items(){return this.menuElement_||(this.menuElement_=this.menuElement),this.menuElement_?this.menuElement_.items:[]}get selected(){const e=this.menuElement;return e?e.selected:null}get index(){const e=this.menuElement;return e?e.index:-1}get shouldRenderHelperText(){return!!this.helper||!!this.validationMessage}get validity(){return this._checkValidity(this.value),this._validity}render(){const e={"mdc-select--disabled":this.disabled,"mdc-select--no-label":!this.label,"mdc-select--filled":!this.outlined,"mdc-select--outlined":this.outlined,"mdc-select--with-leading-icon":!!this.icon,"mdc-select--required":this.required,"mdc-select--invalid":!this.isUiValid},t={"mdc-select__menu--invalid":!this.isUiValid},i=this.label?"label":void 0,n=this.shouldRenderHelperText?"helper-text":void 0;return P` +
+ + +
+ ${this.renderRipple()} + ${this.outlined?this.renderOutline():this.renderLabel()} + ${this.renderLeadingIcon()} + + ${this.selectedText} + + + + + + + + + + ${this.renderLineRipple()} +
+ + + +
+ ${this.renderHelperText()}`}renderRipple(){return this.outlined?V:P` + + `}renderOutline(){return this.outlined?P` + + ${this.renderLabel()} + `:V}renderLabel(){return this.label?P` + ${this.label} + `:V}renderLeadingIcon(){return this.icon?P`
${this.icon}
`:V}renderLineRipple(){return this.outlined?V:P` + + `}renderHelperText(){if(!this.shouldRenderHelperText)return V;const e=this.validationMessage&&!this.isUiValid;return P` +

${e?this.validationMessage:this.helper}

`}createAdapter(){return Object.assign(Object.assign({},Ae(this.mdcRoot)),{activateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},hasLabel:()=>!!this.label,floatLabel:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.float(e)},getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,setLabelRequired:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(e)},hasOutline:()=>this.outlined,notchOutline:e=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=e,this.outlineOpen=!0)},closeOutline:()=>{this.outlineElement&&(this.outlineOpen=!1)},setRippleCenter:e=>{if(this.lineRippleElement){this.lineRippleElement.lineRippleFoundation.setRippleCenter(e)}},notifyChange:async e=>{if(!this.valueSetDirectly&&e===this.value)return;this.valueSetDirectly=!1,this.value=e,await this.updateComplete;const t=new Event("change",{bubbles:!0});this.dispatchEvent(t)},setSelectedText:e=>this.selectedText=e,isSelectAnchorFocused:()=>{const e=this.anchorElement;if(!e)return!1;return e.getRootNode().activeElement===e},getSelectAnchorAttr:e=>{const t=this.anchorElement;return t?t.getAttribute(e):null},setSelectAnchorAttr:(e,t)=>{const i=this.anchorElement;i&&i.setAttribute(e,t)},removeSelectAnchorAttr:e=>{const t=this.anchorElement;t&&t.removeAttribute(e)},openMenu:()=>{this.menuOpen=!0},closeMenu:()=>{this.menuOpen=!1},addMenuClass:()=>{},removeMenuClass:()=>{},getAnchorElement:()=>this.anchorElement,setMenuAnchorElement:()=>{},setMenuAnchorCorner:()=>{const e=this.menuElement;e&&(e.corner="BOTTOM_START")},setMenuWrapFocus:e=>{const t=this.menuElement;t&&(t.wrapFocus=e)},focusMenuItemAtIndex:e=>{const t=this.menuElement;if(!t)return;const i=t.items[e];i&&i.focus()},getMenuItemCount:()=>{const e=this.menuElement;return e?e.items.length:0},getMenuItemValues:()=>{const e=this.menuElement;if(!e)return[];return e.items.map((e=>e.value))},getMenuItemTextAtIndex:e=>{const t=this.menuElement;if(!t)return"";const i=t.items[e];return i?i.text:""},getSelectedIndex:()=>this.index,setSelectedIndex:()=>{},isTypeaheadInProgress:()=>zt(this.typeaheadState),typeaheadMatchItem:(e,t)=>{if(!this.menuElement)return-1;const i={focusItemAtIndex:e=>{this.menuElement.focusItemAtIndex(e)},focusedItemIndex:t||this.menuElement.getFocusedItemIndex(),nextChar:e,sortedIndexByFirstChar:this.sortedIndexByFirstChar,skipFocus:!1,isItemAtIndexDisabled:e=>this.items[e].disabled},n=Pt(i,this.typeaheadState);return-1!==n&&this.select(n),n}})}checkValidity(){const e=this._checkValidity(this.value);if(!e){const e=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(e)}return e}reportValidity(){const e=this.checkValidity();return this.isUiValid=e,e}_checkValidity(e){const t=this.formElement.validity;let i=ri(t);if(this.validityTransform){const t=this.validityTransform(e,i);i=Object.assign(Object.assign({},i),t)}return this._validity=i,this._validity.valid}setCustomValidity(e){this.validationMessage=e,this.formElement.setCustomValidity(e)}async getUpdateComplete(){await this._menuUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){const e=this.menuElement;if(e&&(this._menuUpdateComplete=e.updateComplete,await this._menuUpdateComplete),super.firstUpdated(),this.mdcFoundation.isValid=()=>!0,this.mdcFoundation.setValid=()=>{},this.mdcFoundation.setDisabled(this.disabled),this.validateOnInitialRender&&this.reportValidity(),!this.selected){!this.items.length&&this.slotElement&&this.slotElement.assignedNodes({flatten:!0}).length&&(await new Promise((e=>requestAnimationFrame(e))),await this.layout());const e=this.items.length&&""===this.items[0].value;if(!this.value&&e)return void this.select(0);this.selectByValue(this.value)}this.sortedIndexByFirstChar=Bt(this.items.length,(e=>this.items[e].text))}onItemsUpdated(){this.sortedIndexByFirstChar=Bt(this.items.length,(e=>this.items[e].text))}select(e){const t=this.menuElement;t&&t.select(e)}selectByValue(e){let t=-1;for(let i=0;i0,r=i&&this.index{this.menuElement.focusItemAtIndex(e)},focusedItemIndex:t,isTargetListItem:!!i&&i.hasAttribute("mwc-list-item"),sortedIndexByFirstChar:this.sortedIndexByFirstChar,isItemAtIndexDisabled:e=>this.items[e].disabled},this.typeaheadState)}async onSelected(e){this.mdcFoundation||await this.updateComplete,this.mdcFoundation.handleMenuItemAction(e.detail.index);const t=this.items[e.detail.index];t&&(this.value=t.value)}onOpened(){this.mdcFoundation&&(this.menuOpen=!0,this.mdcFoundation.handleMenuOpened())}onClosed(){this.mdcFoundation&&(this.menuOpen=!1,this.mdcFoundation.handleMenuClosed())}setFormData(e){this.name&&null!==this.selected&&e.append(this.name,this.value)}async layout(e=!0){this.mdcFoundation&&this.mdcFoundation.layout(),await this.updateComplete;const t=this.menuElement;t&&t.layout(e);const i=this.labelElement;if(!i)return void(this.outlineOpen=!1);const n=!!this.label&&!!this.value;if(i.floatingLabelFoundation.float(n),!this.outlined)return;this.outlineOpen=n,await this.updateComplete;const r=i.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=r)}async layoutOptions(){this.mdcFoundation&&this.mdcFoundation.layoutOptions()}}n([he(".mdc-select")],ai.prototype,"mdcRoot",void 0),n([he(".formElement")],ai.prototype,"formElement",void 0),n([he("slot")],ai.prototype,"slotElement",void 0),n([he("select")],ai.prototype,"nativeSelectElement",void 0),n([he("input")],ai.prototype,"nativeInputElement",void 0),n([he(".mdc-line-ripple")],ai.prototype,"lineRippleElement",void 0),n([he(".mdc-floating-label")],ai.prototype,"labelElement",void 0),n([he("mwc-notched-outline")],ai.prototype,"outlineElement",void 0),n([he(".mdc-menu")],ai.prototype,"menuElement",void 0),n([he(".mdc-select__anchor")],ai.prototype,"anchorElement",void 0),n([se({type:Boolean,attribute:"disabled",reflect:!0}),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setDisabled(e)}))],ai.prototype,"disabled",void 0),n([se({type:Boolean}),Me((function(e,t){void 0!==t&&this.outlined!==t&&this.layout(!1)}))],ai.prototype,"outlined",void 0),n([se({type:String}),Me((function(e,t){void 0!==t&&this.label!==t&&this.layout(!1)}))],ai.prototype,"label",void 0),n([ce()],ai.prototype,"outlineOpen",void 0),n([ce()],ai.prototype,"outlineWidth",void 0),n([se({type:String}),Me((function(e){if(this.mdcFoundation){const t=null===this.selected&&!!e,i=this.selected&&this.selected.value!==e;(t||i)&&this.selectByValue(e),this.reportValidity()}}))],ai.prototype,"value",void 0),n([se()],ai.prototype,"name",void 0),n([ce()],ai.prototype,"selectedText",void 0),n([se({type:String})],ai.prototype,"icon",void 0),n([ce()],ai.prototype,"menuOpen",void 0),n([se({type:String})],ai.prototype,"helper",void 0),n([se({type:Boolean})],ai.prototype,"validateOnInitialRender",void 0),n([se({type:String})],ai.prototype,"validationMessage",void 0),n([se({type:Boolean})],ai.prototype,"required",void 0),n([se({type:Boolean})],ai.prototype,"naturalMenuWidth",void 0),n([ce()],ai.prototype,"isUiValid",void 0),n([se({type:Boolean})],ai.prototype,"fixedMenuPosition",void 0),n([pe({capture:!0})],ai.prototype,"handleTypeahead",null); +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const oi=(e,t)=>e-t,di=["input","button","textarea","select"];function li(e){return e instanceof Set}const si=e=>{const t=e===Mt.UNSET_INDEX?new Set:e;return li(t)?new Set(t):new Set([t])};class ci extends ye{constructor(e){super(Object.assign(Object.assign({},ci.defaultAdapter),e)),this.isMulti_=!1,this.wrapFocus_=!1,this.isVertical_=!0,this.selectedIndex_=Mt.UNSET_INDEX,this.focusedItemIndex_=Mt.UNSET_INDEX,this.useActivatedClass_=!1,this.ariaCurrentAttrValue_=null}static get strings(){return $t}static get numbers(){return Mt}static get defaultAdapter(){return{focusItemAtIndex:()=>{},getFocusedElementIndex:()=>0,getListItemCount:()=>0,isFocusInsideList:()=>!1,isRootFocused:()=>!1,notifyAction:()=>{},notifySelected:()=>{},getSelectedStateForElementIndex:()=>!1,setDisabledStateForElementIndex:()=>{},getDisabledStateForElementIndex:()=>!1,setSelectedStateForElementIndex:()=>{},setActivatedStateForElementIndex:()=>{},setTabIndexForElementIndex:()=>{},setAttributeForElementIndex:()=>{},getAttributeForElementIndex:()=>null}}setWrapFocus(e){this.wrapFocus_=e}setMulti(e){this.isMulti_=e;const t=this.selectedIndex_;if(e){if(!li(t)){const e=t===Mt.UNSET_INDEX;this.selectedIndex_=e?new Set:new Set([t])}}else if(li(t))if(t.size){const e=Array.from(t).sort(oi);this.selectedIndex_=e[0]}else this.selectedIndex_=Mt.UNSET_INDEX}setVerticalOrientation(e){this.isVertical_=e}setUseActivatedClass(e){this.useActivatedClass_=e}getSelectedIndex(){return this.selectedIndex_}setSelectedIndex(e){this.isIndexValid_(e)&&(this.isMulti_?this.setMultiSelectionAtIndex_(si(e)):this.setSingleSelectionAtIndex_(e))}handleFocusIn(e,t){t>=0&&this.adapter.setTabIndexForElementIndex(t,0)}handleFocusOut(e,t){t>=0&&this.adapter.setTabIndexForElementIndex(t,-1),setTimeout((()=>{this.adapter.isFocusInsideList()||this.setTabindexToFirstSelectedItem_()}),0)}handleKeydown(e,t,i){const n="ArrowLeft"===At(e),r="ArrowUp"===At(e),a="ArrowRight"===At(e),o="ArrowDown"===At(e),d="Home"===At(e),l="End"===At(e),s="Enter"===At(e),c="Spacebar"===At(e);if(this.adapter.isRootFocused())return void(r||l?(e.preventDefault(),this.focusLastElement()):(o||d)&&(e.preventDefault(),this.focusFirstElement()));let m,p=this.adapter.getFocusedElementIndex();if(!(-1===p&&(p=i,p<0))){if(this.isVertical_&&o||!this.isVertical_&&a)this.preventDefaultEvent(e),m=this.focusNextElement(p);else if(this.isVertical_&&r||!this.isVertical_&&n)this.preventDefaultEvent(e),m=this.focusPrevElement(p);else if(d)this.preventDefaultEvent(e),m=this.focusFirstElement();else if(l)this.preventDefaultEvent(e),m=this.focusLastElement();else if((s||c)&&t){const t=e.target;if(t&&"A"===t.tagName&&s)return;this.preventDefaultEvent(e),this.setSelectedIndexOnAction_(p,!0)}this.focusedItemIndex_=p,void 0!==m&&(this.setTabindexAtIndex_(m),this.focusedItemIndex_=m)}}handleSingleSelection(e,t,i){e!==Mt.UNSET_INDEX&&(this.setSelectedIndexOnAction_(e,t,i),this.setTabindexAtIndex_(e),this.focusedItemIndex_=e)}focusNextElement(e){let t=e+1;if(t>=this.adapter.getListItemCount()){if(!this.wrapFocus_)return e;t=0}return this.adapter.focusItemAtIndex(t),t}focusPrevElement(e){let t=e-1;if(t<0){if(!this.wrapFocus_)return e;t=this.adapter.getListItemCount()-1}return this.adapter.focusItemAtIndex(t),t}focusFirstElement(){return this.adapter.focusItemAtIndex(0),0}focusLastElement(){const e=this.adapter.getListItemCount()-1;return this.adapter.focusItemAtIndex(e),e}setEnabled(e,t){this.isIndexValid_(e)&&this.adapter.setDisabledStateForElementIndex(e,!t)}preventDefaultEvent(e){const t=`${e.target.tagName}`.toLowerCase();-1===di.indexOf(t)&&e.preventDefault()}setSingleSelectionAtIndex_(e,t=!0){this.selectedIndex_!==e&&(this.selectedIndex_!==Mt.UNSET_INDEX&&(this.adapter.setSelectedStateForElementIndex(this.selectedIndex_,!1),this.useActivatedClass_&&this.adapter.setActivatedStateForElementIndex(this.selectedIndex_,!1)),t&&this.adapter.setSelectedStateForElementIndex(e,!0),this.useActivatedClass_&&this.adapter.setActivatedStateForElementIndex(e,!0),this.setAriaForSingleSelectionAtIndex_(e),this.selectedIndex_=e,this.adapter.notifySelected(e))}setMultiSelectionAtIndex_(e,t=!0){const i=((e,t)=>{const i=Array.from(e),n=Array.from(t),r={added:[],removed:[]},a=i.sort(oi),o=n.sort(oi);let d=0,l=0;for(;d=0&&this.focusedItemIndex_!==e&&this.adapter.setTabIndexForElementIndex(this.focusedItemIndex_,-1),this.adapter.setTabIndexForElementIndex(e,0)}setTabindexToFirstSelectedItem_(){let e=0;"number"==typeof this.selectedIndex_&&this.selectedIndex_!==Mt.UNSET_INDEX?e=this.selectedIndex_:li(this.selectedIndex_)&&this.selectedIndex_.size>0&&(e=Math.min(...this.selectedIndex_)),this.setTabindexAtIndex_(e)}isIndexValid_(e){if(e instanceof Set){if(!this.isMulti_)throw new Error("MDCListFoundation: Array of index is only supported for checkbox based list");if(0===e.size)return!0;{let t=!1;for(const i of e)if(t=this.isIndexInRange_(i),t)break;return t}}if("number"==typeof e){if(this.isMulti_)throw new Error("MDCListFoundation: Expected array of index for checkbox based list but got number: "+e);return e===Mt.UNSET_INDEX||this.isIndexInRange_(e)}return!1}isIndexInRange_(e){const t=this.adapter.getListItemCount();return e>=0&&ee.hasAttribute("mwc-list-item");function pi(){const e=this.itemsReadyResolver;this.itemsReady=new Promise((e=>this.itemsReadyResolver=e)),e()}class hi extends Oe{constructor(){super(),this.mdcAdapter=null,this.mdcFoundationClass=ci,this.activatable=!1,this.multi=!1,this.wrapFocus=!1,this.itemRoles=null,this.innerRole=null,this.innerAriaLabel=null,this.rootTabbable=!1,this.previousTabindex=null,this.noninteractive=!1,this.itemsReadyResolver=()=>{},this.itemsReady=Promise.resolve([]),this.items_=[];const e=function(e,t=50){let i;return function(n=!0){clearTimeout(i),i=setTimeout((()=>{e(n)}),t)}}(this.layout.bind(this));this.debouncedLayout=(t=!0)=>{pi.call(this),e(t)}}async getUpdateComplete(){const e=await super.getUpdateComplete();return await this.itemsReady,e}get items(){return this.items_}updateItems(){var e;const t=null!==(e=this.assignedElements)&&void 0!==e?e:[],i=[];for(const e of t)mi(e)&&(i.push(e),e._managingList=this),e.hasAttribute("divider")&&!e.hasAttribute("role")&&e.setAttribute("role","separator");this.items_=i;const n=new Set;if(this.items_.forEach(((e,t)=>{this.itemRoles?e.setAttribute("role",this.itemRoles):e.removeAttribute("role"),e.selected&&n.add(t)})),this.multi)this.select(n);else{const e=n.size?n.entries().next().value[1]:-1;this.select(e)}const r=new Event("items-updated",{bubbles:!0,composed:!0});this.dispatchEvent(r)}get selected(){const e=this.index;if(!li(e))return-1===e?null:this.items[e];const t=[];for(const i of e)t.push(this.items[i]);return t}get index(){return this.mdcFoundation?this.mdcFoundation.getSelectedIndex():-1}render(){const e=null===this.innerRole?void 0:this.innerRole,t=null===this.innerAriaLabel?void 0:this.innerAriaLabel,i=this.rootTabbable?"0":"-1";return P` + +
    + + ${this.renderPlaceholder()} +
+ `}renderPlaceholder(){var e;const t=null!==(e=this.assignedElements)&&void 0!==e?e:[];return void 0!==this.emptyMessage&&0===t.length?P` + ${this.emptyMessage} + `:null}firstUpdated(){super.firstUpdated(),this.items.length||(this.mdcFoundation.setMulti(this.multi),this.layout())}onFocusIn(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e);this.mdcFoundation.handleFocusIn(e,t)}}onFocusOut(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e);this.mdcFoundation.handleFocusOut(e,t)}}onKeydown(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e),i=e.target,n=mi(i);this.mdcFoundation.handleKeydown(e,n,t)}}onRequestSelected(e){if(this.mdcFoundation){let t=this.getIndexOfTarget(e);if(-1===t&&(this.layout(),t=this.getIndexOfTarget(e),-1===t))return;if(this.items[t].disabled)return;const i=e.detail.selected,n=e.detail.source;this.mdcFoundation.handleSingleSelection(t,"interaction"===n,i),e.stopPropagation()}}getIndexOfTarget(e){const t=this.items,i=e.composedPath();for(const e of i){let i=-1;if(Ie(e)&&mi(e)&&(i=t.indexOf(e)),-1!==i)return i}return-1}createAdapter(){return this.mdcAdapter={getListItemCount:()=>this.mdcRoot?this.items.length:0,getFocusedElementIndex:this.getFocusedItemIndex,getAttributeForElementIndex:(e,t)=>{if(!this.mdcRoot)return"";const i=this.items[e];return i?i.getAttribute(t):""},setAttributeForElementIndex:(e,t,i)=>{if(!this.mdcRoot)return;const n=this.items[e];n&&n.setAttribute(t,i)},focusItemAtIndex:e=>{const t=this.items[e];t&&t.focus()},setTabIndexForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.tabindex=t)},notifyAction:e=>{const t={bubbles:!0,composed:!0};t.detail={index:e};const i=new CustomEvent("action",t);this.dispatchEvent(i)},notifySelected:(e,t)=>{const i={bubbles:!0,composed:!0};i.detail={index:e,diff:t};const n=new CustomEvent("selected",i);this.dispatchEvent(n)},isFocusInsideList:()=>ke(this),isRootFocused:()=>{const e=this.mdcRoot;return e.getRootNode().activeElement===e},setDisabledStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.disabled=t)},getDisabledStateForElementIndex:e=>{const t=this.items[e];return!!t&&t.disabled},setSelectedStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.selected=t)},getSelectedStateForElementIndex:e=>{const t=this.items[e];return!!t&&t.selected},setActivatedStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.activated=t)}},this.mdcAdapter}selectUi(e,t=!1){const i=this.items[e];i&&(i.selected=!0,i.activated=t)}deselectUi(e){const t=this.items[e];t&&(t.selected=!1,t.activated=!1)}select(e){this.mdcFoundation&&this.mdcFoundation.setSelectedIndex(e)}toggle(e,t){this.multi&&this.mdcFoundation.toggleMultiAtIndex(e,t)}onListItemConnected(e){const t=e.target;this.layout(-1===this.items.indexOf(t))}layout(e=!0){e&&this.updateItems();const t=this.items[0];for(const e of this.items)e.tabindex=-1;t&&(this.noninteractive?this.previousTabindex||(this.previousTabindex=t):t.tabindex=0),this.itemsReadyResolver()}getFocusedItemIndex(){if(!this.mdcRoot)return-1;if(!this.items.length)return-1;const e=Re();if(!e.length)return-1;for(let t=e.length-1;t>=0;t--){const i=e[t];if(mi(i))return this.items.indexOf(i)}return-1}focusItemAtIndex(e){for(const e of this.items)if(0===e.tabindex){e.tabindex=-1;break}this.items[e].tabindex=0,this.items[e].focus()}focus(){const e=this.mdcRoot;e&&e.focus()}blur(){const e=this.mdcRoot;e&&e.blur()}}n([se({type:String})],hi.prototype,"emptyMessage",void 0),n([he(".mdc-deprecated-list")],hi.prototype,"mdcRoot",void 0),n([be("",!0,"*")],hi.prototype,"assignedElements",void 0),n([be("",!0,'[tabindex="0"]')],hi.prototype,"tabbableElements",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setUseActivatedClass(e)}))],hi.prototype,"activatable",void 0),n([se({type:Boolean}),Me((function(e,t){this.mdcFoundation&&this.mdcFoundation.setMulti(e),void 0!==t&&this.layout()}))],hi.prototype,"multi",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setWrapFocus(e)}))],hi.prototype,"wrapFocus",void 0),n([se({type:String}),Me((function(e,t){void 0!==t&&this.updateItems()}))],hi.prototype,"itemRoles",void 0),n([se({type:String})],hi.prototype,"innerRole",void 0),n([se({type:String})],hi.prototype,"innerAriaLabel",void 0),n([se({type:Boolean})],hi.prototype,"rootTabbable",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e){var t,i;if(e){const e=null!==(i=null===(t=this.tabbableElements)||void 0===t?void 0:t[0])&&void 0!==i?i:null;this.previousTabindex=e,e&&e.setAttribute("tabindex","-1")}else!e&&this.previousTabindex&&(this.previousTabindex.setAttribute("tabindex","0"),this.previousTabindex=null)}))],hi.prototype,"noninteractive",void 0); +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class ui{constructor(e){this.startPress=t=>{e().then((e=>{e&&e.startPress(t)}))},this.endPress=()=>{e().then((e=>{e&&e.endPress()}))},this.startFocus=()=>{e().then((e=>{e&&e.startFocus()}))},this.endFocus=()=>{e().then((e=>{e&&e.endFocus()}))},this.startHover=()=>{e().then((e=>{e&&e.startHover()}))},this.endHover=()=>{e().then((e=>{e&&e.endHover()}))}}} +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class fi extends ae{constructor(){super(...arguments),this.value="",this.group=null,this.tabindex=-1,this.disabled=!1,this.twoline=!1,this.activated=!1,this.graphic=null,this.multipleGraphics=!1,this.hasMeta=!1,this.noninteractive=!1,this.selected=!1,this.shouldRenderRipple=!1,this._managingList=null,this.boundOnClick=this.onClick.bind(this),this._firstChanged=!0,this._skipPropRequest=!1,this.rippleHandlers=new ui((()=>(this.shouldRenderRipple=!0,this.ripple))),this.listeners=[{target:this,eventNames:["click"],cb:()=>{this.onClick()}},{target:this,eventNames:["mouseenter"],cb:this.rippleHandlers.startHover},{target:this,eventNames:["mouseleave"],cb:this.rippleHandlers.endHover},{target:this,eventNames:["focus"],cb:this.rippleHandlers.startFocus},{target:this,eventNames:["blur"],cb:this.rippleHandlers.endFocus},{target:this,eventNames:["mousedown","touchstart"],cb:e=>{const t=e.type;this.onDown("mousedown"===t?"mouseup":"touchend",e)}}]}get text(){const e=this.textContent;return e?e.trim():""}render(){const e=this.renderText(),t=this.graphic?this.renderGraphic():P``,i=this.hasMeta?this.renderMeta():P``;return P` + ${this.renderRipple()} + ${t} + ${e} + ${i}`}renderRipple(){return this.shouldRenderRipple?P` + + `:this.activated?P`
`:""}renderGraphic(){const e={multi:this.multipleGraphics};return P` + + + `}renderMeta(){return P` + + + `}renderText(){const e=this.twoline?this.renderTwoline():this.renderSingleLine();return P` + + ${e} + `}renderSingleLine(){return P``}renderTwoline(){return P` + + + + + + + `}onClick(){this.fireRequestSelected(!this.selected,"interaction")}onDown(e,t){const i=()=>{window.removeEventListener(e,i),this.rippleHandlers.endPress()};window.addEventListener(e,i),this.rippleHandlers.startPress(t)}fireRequestSelected(e,t){if(this.noninteractive)return;const i=new CustomEvent("request-selected",{bubbles:!0,composed:!0,detail:{source:t,selected:e}});this.dispatchEvent(i)}connectedCallback(){super.connectedCallback(),this.noninteractive||this.setAttribute("mwc-list-item","");for(const e of this.listeners)for(const t of e.eventNames)e.target.addEventListener(t,e.cb,{passive:!0})}disconnectedCallback(){super.disconnectedCallback();for(const e of this.listeners)for(const t of e.eventNames)e.target.removeEventListener(t,e.cb);this._managingList&&(this._managingList.debouncedLayout?this._managingList.debouncedLayout(!0):this._managingList.layout(!0))}firstUpdated(){const e=new Event("list-item-rendered",{bubbles:!0,composed:!0});this.dispatchEvent(e)}}n([he("slot")],fi.prototype,"slotElement",void 0),n([ue("mwc-ripple")],fi.prototype,"ripple",void 0),n([se({type:String})],fi.prototype,"value",void 0),n([se({type:String,reflect:!0})],fi.prototype,"group",void 0),n([se({type:Number,reflect:!0})],fi.prototype,"tabindex",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e){e?this.setAttribute("aria-disabled","true"):this.setAttribute("aria-disabled","false")}))],fi.prototype,"disabled",void 0),n([se({type:Boolean,reflect:!0})],fi.prototype,"twoline",void 0),n([se({type:Boolean,reflect:!0})],fi.prototype,"activated",void 0),n([se({type:String,reflect:!0})],fi.prototype,"graphic",void 0),n([se({type:Boolean})],fi.prototype,"multipleGraphics",void 0),n([se({type:Boolean})],fi.prototype,"hasMeta",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e){e?(this.removeAttribute("aria-checked"),this.removeAttribute("mwc-list-item"),this.selected=!1,this.activated=!1,this.tabIndex=-1):this.setAttribute("mwc-list-item","")}))],fi.prototype,"noninteractive",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e){const t=this.getAttribute("role"),i="gridcell"===t||"option"===t||"row"===t||"tab"===t;i&&e?this.setAttribute("aria-selected","true"):i&&this.setAttribute("aria-selected","false"),this._firstChanged?this._firstChanged=!1:this._skipPropRequest||this.fireRequestSelected(e,"property")}))],fi.prototype,"selected",void 0),n([ce()],fi.prototype,"shouldRenderRipple",void 0),n([ce()],fi.prototype,"_managingList",void 0); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var gi,bi={MENU_SELECTED_LIST_ITEM:"mdc-menu-item--selected",MENU_SELECTION_GROUP:"mdc-menu__selection-group",ROOT:"mdc-menu"},vi={ARIA_CHECKED_ATTR:"aria-checked",ARIA_DISABLED_ATTR:"aria-disabled",CHECKBOX_SELECTOR:'input[type="checkbox"]',LIST_SELECTOR:".mdc-list,.mdc-deprecated-list",SELECTED_EVENT:"MDCMenu:selected",SKIP_RESTORE_FOCUS:"data-menu-item-skip-restore-focus"},xi={FOCUS_ROOT_INDEX:-1};!function(e){e[e.NONE=0]="NONE",e[e.LIST_ROOT=1]="LIST_ROOT",e[e.FIRST_ITEM=2]="FIRST_ITEM",e[e.LAST_ITEM=3]="LAST_ITEM"}(gi||(gi={})); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var _i=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.isSurfaceOpen=!1,r.isQuickOpen=!1,r.isHoistedElement=!1,r.isFixedPosition=!1,r.isHorizontallyCenteredOnViewport=!1,r.maxHeight=0,r.openBottomBias=0,r.openAnimationEndTimerId=0,r.closeAnimationEndTimerId=0,r.animationRequestId=0,r.anchorCorner=Yt.TOP_START,r.originCorner=Yt.TOP_START,r.anchorMargin={top:0,right:0,bottom:0,left:0},r.position={x:0,y:0},r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return Kt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return Zt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return Qt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"Corner",{get:function(){return Yt},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},hasAnchor:function(){return!1},isElementInContainer:function(){return!1},isFocused:function(){return!1},isRtl:function(){return!1},getInnerDimensions:function(){return{height:0,width:0}},getAnchorDimensions:function(){return null},getWindowDimensions:function(){return{height:0,width:0}},getBodyDimensions:function(){return{height:0,width:0}},getWindowScroll:function(){return{x:0,y:0}},setPosition:function(){},setMaxHeight:function(){},setTransformOrigin:function(){},saveFocus:function(){},restoreFocus:function(){},notifyClose:function(){},notifyOpen:function(){},notifyClosing:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){var e=n.cssClasses,t=e.ROOT,i=e.OPEN;if(!this.adapter.hasClass(t))throw new Error(t+" class required in root element.");this.adapter.hasClass(i)&&(this.isSurfaceOpen=!0)},n.prototype.destroy=function(){clearTimeout(this.openAnimationEndTimerId),clearTimeout(this.closeAnimationEndTimerId),cancelAnimationFrame(this.animationRequestId)},n.prototype.setAnchorCorner=function(e){this.anchorCorner=e},n.prototype.flipCornerHorizontally=function(){this.originCorner=this.originCorner^qt.RIGHT},n.prototype.setAnchorMargin=function(e){this.anchorMargin.top=e.top||0,this.anchorMargin.right=e.right||0,this.anchorMargin.bottom=e.bottom||0,this.anchorMargin.left=e.left||0},n.prototype.setIsHoisted=function(e){this.isHoistedElement=e},n.prototype.setFixedPosition=function(e){this.isFixedPosition=e},n.prototype.isFixed=function(){return this.isFixedPosition},n.prototype.setAbsolutePosition=function(e,t){this.position.x=this.isFinite(e)?e:0,this.position.y=this.isFinite(t)?t:0},n.prototype.setIsHorizontallyCenteredOnViewport=function(e){this.isHorizontallyCenteredOnViewport=e},n.prototype.setQuickOpen=function(e){this.isQuickOpen=e},n.prototype.setMaxHeight=function(e){this.maxHeight=e},n.prototype.setOpenBottomBias=function(e){this.openBottomBias=e},n.prototype.isOpen=function(){return this.isSurfaceOpen},n.prototype.open=function(){var e=this;this.isSurfaceOpen||(this.adapter.saveFocus(),this.isQuickOpen?(this.isSurfaceOpen=!0,this.adapter.addClass(n.cssClasses.OPEN),this.dimensions=this.adapter.getInnerDimensions(),this.autoposition(),this.adapter.notifyOpen()):(this.adapter.addClass(n.cssClasses.ANIMATING_OPEN),this.animationRequestId=requestAnimationFrame((function(){e.dimensions=e.adapter.getInnerDimensions(),e.autoposition(),e.adapter.addClass(n.cssClasses.OPEN),e.openAnimationEndTimerId=setTimeout((function(){e.openAnimationEndTimerId=0,e.adapter.removeClass(n.cssClasses.ANIMATING_OPEN),e.adapter.notifyOpen()}),Qt.TRANSITION_OPEN_DURATION)})),this.isSurfaceOpen=!0))},n.prototype.close=function(e){var t=this;if(void 0===e&&(e=!1),this.isSurfaceOpen){if(this.adapter.notifyClosing(),this.isQuickOpen)return this.isSurfaceOpen=!1,e||this.maybeRestoreFocus(),this.adapter.removeClass(n.cssClasses.OPEN),this.adapter.removeClass(n.cssClasses.IS_OPEN_BELOW),void this.adapter.notifyClose();this.adapter.addClass(n.cssClasses.ANIMATING_CLOSED),requestAnimationFrame((function(){t.adapter.removeClass(n.cssClasses.OPEN),t.adapter.removeClass(n.cssClasses.IS_OPEN_BELOW),t.closeAnimationEndTimerId=setTimeout((function(){t.closeAnimationEndTimerId=0,t.adapter.removeClass(n.cssClasses.ANIMATING_CLOSED),t.adapter.notifyClose()}),Qt.TRANSITION_CLOSE_DURATION)})),this.isSurfaceOpen=!1,e||this.maybeRestoreFocus()}},n.prototype.handleBodyClick=function(e){var t=e.target;this.adapter.isElementInContainer(t)||this.close()},n.prototype.handleKeydown=function(e){var t=e.keyCode;("Escape"===e.key||27===t)&&this.close()},n.prototype.autoposition=function(){var e;this.measurements=this.getAutoLayoutmeasurements();var t=this.getoriginCorner(),i=this.getMenuSurfaceMaxHeight(t),r=this.hasBit(t,qt.BOTTOM)?"bottom":"top",a=this.hasBit(t,qt.RIGHT)?"right":"left",o=this.getHorizontalOriginOffset(t),d=this.getVerticalOriginOffset(t),l=this.measurements,s=l.anchorSize,c=l.surfaceSize,m=((e={})[a]=o,e[r]=d,e);s.width/c.width>Qt.ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO&&(a="center"),(this.isHoistedElement||this.isFixedPosition)&&this.adjustPositionForHoistedElement(m),this.adapter.setTransformOrigin(a+" "+r),this.adapter.setPosition(m),this.adapter.setMaxHeight(i?i+"px":""),this.hasBit(t,qt.BOTTOM)||this.adapter.addClass(n.cssClasses.IS_OPEN_BELOW)},n.prototype.getAutoLayoutmeasurements=function(){var e=this.adapter.getAnchorDimensions(),t=this.adapter.getBodyDimensions(),i=this.adapter.getWindowDimensions(),n=this.adapter.getWindowScroll();return e||(e={top:this.position.y,right:this.position.x,bottom:this.position.y,left:this.position.x,width:0,height:0}),{anchorSize:e,bodySize:t,surfaceSize:this.dimensions,viewportDistance:{top:e.top,right:i.width-e.right,bottom:i.height-e.bottom,left:e.left},viewportSize:i,windowScroll:n}},n.prototype.getoriginCorner=function(){var e,t,i=this.originCorner,r=this.measurements,a=r.viewportDistance,o=r.anchorSize,d=r.surfaceSize,l=n.numbers.MARGIN_TO_EDGE;this.hasBit(this.anchorCorner,qt.BOTTOM)?(e=a.top-l+this.anchorMargin.bottom,t=a.bottom-l-this.anchorMargin.bottom):(e=a.top-l+this.anchorMargin.top,t=a.bottom-l+o.height-this.anchorMargin.top),!(t-d.height>0)&&e>t+this.openBottomBias&&(i=this.setBit(i,qt.BOTTOM));var s,c,m=this.adapter.isRtl(),p=this.hasBit(this.anchorCorner,qt.FLIP_RTL),h=this.hasBit(this.anchorCorner,qt.RIGHT)||this.hasBit(i,qt.RIGHT),u=!1;(u=m&&p?!h:h)?(s=a.left+o.width+this.anchorMargin.right,c=a.right-this.anchorMargin.right):(s=a.left+this.anchorMargin.left,c=a.right+o.width-this.anchorMargin.left);var f=s-d.width>0,g=c-d.width>0,b=this.hasBit(i,qt.FLIP_RTL)&&this.hasBit(i,qt.RIGHT);return g&&b&&m||!f&&b?i=this.unsetBit(i,qt.RIGHT):(f&&u&&m||f&&!u&&h||!g&&s>=c)&&(i=this.setBit(i,qt.RIGHT)),i},n.prototype.getMenuSurfaceMaxHeight=function(e){if(this.maxHeight>0)return this.maxHeight;var t=this.measurements.viewportDistance,i=0,r=this.hasBit(e,qt.BOTTOM),a=this.hasBit(this.anchorCorner,qt.BOTTOM),o=n.numbers.MARGIN_TO_EDGE;return r?(i=t.top+this.anchorMargin.top-o,a||(i+=this.measurements.anchorSize.height)):(i=t.bottom-this.anchorMargin.bottom+this.measurements.anchorSize.height-o,a&&(i-=this.measurements.anchorSize.height)),i},n.prototype.getHorizontalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,qt.RIGHT),n=this.hasBit(this.anchorCorner,qt.RIGHT);if(i){var r=n?t.width-this.anchorMargin.left:this.anchorMargin.right;return this.isHoistedElement||this.isFixedPosition?r-(this.measurements.viewportSize.width-this.measurements.bodySize.width):r}return n?t.width-this.anchorMargin.right:this.anchorMargin.left},n.prototype.getVerticalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,qt.BOTTOM),n=this.hasBit(this.anchorCorner,qt.BOTTOM);return i?n?t.height-this.anchorMargin.top:-this.anchorMargin.bottom:n?t.height+this.anchorMargin.bottom:this.anchorMargin.top},n.prototype.adjustPositionForHoistedElement=function(e){var t,i,n=this.measurements,a=n.windowScroll,o=n.viewportDistance,d=n.surfaceSize,l=n.viewportSize,s=Object.keys(e);try{for(var c=r(s),m=c.next();!m.done;m=c.next()){var p=m.value,h=e[p]||0;!this.isHorizontallyCenteredOnViewport||"left"!==p&&"right"!==p?(h+=o[p],this.isFixedPosition||("top"===p?h+=a.y:"bottom"===p?h-=a.y:"left"===p?h+=a.x:h-=a.x),e[p]=h):e[p]=(l.width-d.width)/2}}catch(e){t={error:e}}finally{try{m&&!m.done&&(i=c.return)&&i.call(c)}finally{if(t)throw t.error}}},n.prototype.maybeRestoreFocus=function(){var e=this,t=this.adapter.isFocused(),i=document.activeElement&&this.adapter.isElementInContainer(document.activeElement);(t||i)&&setTimeout((function(){e.adapter.restoreFocus()}),Qt.TOUCH_EVENT_WAIT_MS)},n.prototype.hasBit=function(e,t){return Boolean(e&t)},n.prototype.setBit=function(e,t){return e|t},n.prototype.unsetBit=function(e,t){return e^t},n.prototype.isFinite=function(e){return"number"==typeof e&&isFinite(e)},n}(ye),yi=_i,wi=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.closeAnimationEndTimerId=0,r.defaultFocusState=gi.LIST_ROOT,r.selectedIndex=-1,r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return bi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return vi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return xi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClassToElementAtIndex:function(){},removeClassFromElementAtIndex:function(){},addAttributeToElementAtIndex:function(){},removeAttributeFromElementAtIndex:function(){},getAttributeFromElementAtIndex:function(){return null},elementContainsClass:function(){return!1},closeSurface:function(){},getElementIndex:function(){return-1},notifySelected:function(){},getMenuItemCount:function(){return 0},focusItemAtIndex:function(){},focusListRoot:function(){},getSelectedSiblingOfItemAtIndex:function(){return-1},isSelectableItemAtIndex:function(){return!1}}},enumerable:!1,configurable:!0}),n.prototype.destroy=function(){this.closeAnimationEndTimerId&&clearTimeout(this.closeAnimationEndTimerId),this.adapter.closeSurface()},n.prototype.handleKeydown=function(e){var t=e.key,i=e.keyCode;("Tab"===t||9===i)&&this.adapter.closeSurface(!0)},n.prototype.handleItemAction=function(e){var t=this,i=this.adapter.getElementIndex(e);if(!(i<0)){this.adapter.notifySelected({index:i});var n="true"===this.adapter.getAttributeFromElementAtIndex(i,vi.SKIP_RESTORE_FOCUS);this.adapter.closeSurface(n),this.closeAnimationEndTimerId=setTimeout((function(){var i=t.adapter.getElementIndex(e);i>=0&&t.adapter.isSelectableItemAtIndex(i)&&t.setSelectedIndex(i)}),_i.numbers.TRANSITION_CLOSE_DURATION)}},n.prototype.handleMenuSurfaceOpened=function(){switch(this.defaultFocusState){case gi.FIRST_ITEM:this.adapter.focusItemAtIndex(0);break;case gi.LAST_ITEM:this.adapter.focusItemAtIndex(this.adapter.getMenuItemCount()-1);break;case gi.NONE:break;default:this.adapter.focusListRoot()}},n.prototype.setDefaultFocusState=function(e){this.defaultFocusState=e},n.prototype.getSelectedIndex=function(){return this.selectedIndex},n.prototype.setSelectedIndex=function(e){if(this.validatedIndex(e),!this.adapter.isSelectableItemAtIndex(e))throw new Error("MDCMenuFoundation: No selection group at specified index.");var t=this.adapter.getSelectedSiblingOfItemAtIndex(e);t>=0&&(this.adapter.removeAttributeFromElementAtIndex(t,vi.ARIA_CHECKED_ATTR),this.adapter.removeClassFromElementAtIndex(t,bi.MENU_SELECTED_LIST_ITEM)),this.adapter.addClassToElementAtIndex(e,bi.MENU_SELECTED_LIST_ITEM),this.adapter.addAttributeToElementAtIndex(e,vi.ARIA_CHECKED_ATTR,"true"),this.selectedIndex=e},n.prototype.setEnabled=function(e,t){this.validatedIndex(e),t?(this.adapter.removeClassFromElementAtIndex(e,Rt),this.adapter.addAttributeToElementAtIndex(e,vi.ARIA_DISABLED_ATTR,"false")):(this.adapter.addClassToElementAtIndex(e,Rt),this.adapter.addAttributeToElementAtIndex(e,vi.ARIA_DISABLED_ATTR,"true"))},n.prototype.validatedIndex=function(e){var t=this.adapter.getMenuItemCount();if(!(e>=0&&e + + + + `}createAdapter(){return{addClassToElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const n=i.items[e];n&&("mdc-menu-item--selected"===t?this.forceGroupSelection&&!n.selected&&i.toggle(e,!0):n.classList.add(t))},removeClassFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const n=i.items[e];n&&("mdc-menu-item--selected"===t?n.selected&&i.toggle(e,!1):n.classList.remove(t))},addAttributeToElementAtIndex:(e,t,i)=>{const n=this.listElement;if(!n)return;const r=n.items[e];r&&r.setAttribute(t,i)},removeAttributeFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const n=i.items[e];n&&n.removeAttribute(t)},getAttributeFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return null;const n=i.items[e];return n?n.getAttribute(t):null},elementContainsClass:(e,t)=>e.classList.contains(t),closeSurface:()=>{this.open=!1},getElementIndex:e=>{const t=this.listElement;return t?t.items.indexOf(e):-1},notifySelected:()=>{},getMenuItemCount:()=>{const e=this.listElement;return e?e.items.length:0},focusItemAtIndex:e=>{const t=this.listElement;if(!t)return;const i=t.items[e];i&&i.focus()},focusListRoot:()=>{this.listElement&&this.listElement.focus()},getSelectedSiblingOfItemAtIndex:e=>{const t=this.listElement;if(!t)return-1;const i=t.items[e];if(!i||!i.group)return-1;for(let n=0;n{const t=this.listElement;if(!t)return!1;const i=t.items[e];return!!i&&i.hasAttribute("group")}}}onKeydown(e){this.mdcFoundation&&this.mdcFoundation.handleKeydown(e)}onAction(e){const t=this.listElement;if(this.mdcFoundation&&t){const i=e.detail.index,n=t.items[i];n&&this.mdcFoundation.handleItemAction(n)}}onOpened(){this.open=!0,this.mdcFoundation&&this.mdcFoundation.handleMenuSurfaceOpened()}onClosed(){this.open=!1}async getUpdateComplete(){await this._listUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){super.firstUpdated();const e=this.listElement;e&&(this._listUpdateComplete=e.updateComplete,await this._listUpdateComplete)}select(e){const t=this.listElement;t&&t.select(e)}close(){this.open=!1}show(){this.open=!0}getFocusedItemIndex(){const e=this.listElement;return e?e.getFocusedItemIndex():-1}focusItemAtIndex(e){const t=this.listElement;t&&t.focusItemAtIndex(e)}layout(e=!0){const t=this.listElement;t&&t.layout(e)}}n([he(".mdc-menu")],Ei.prototype,"mdcRoot",void 0),n([he("slot")],Ei.prototype,"slotElement",void 0),n([se({type:Object})],Ei.prototype,"anchor",void 0),n([se({type:Boolean,reflect:!0})],Ei.prototype,"open",void 0),n([se({type:Boolean})],Ei.prototype,"quick",void 0),n([se({type:Boolean})],Ei.prototype,"wrapFocus",void 0),n([se({type:String})],Ei.prototype,"innerRole",void 0),n([se({type:String})],Ei.prototype,"innerAriaLabel",void 0),n([se({type:String})],Ei.prototype,"corner",void 0),n([se({type:Number})],Ei.prototype,"x",void 0),n([se({type:Number})],Ei.prototype,"y",void 0),n([se({type:Boolean})],Ei.prototype,"absolute",void 0),n([se({type:Boolean})],Ei.prototype,"multi",void 0),n([se({type:Boolean})],Ei.prototype,"activatable",void 0),n([se({type:Boolean})],Ei.prototype,"fixed",void 0),n([se({type:Boolean})],Ei.prototype,"forceGroupSelection",void 0),n([se({type:Boolean})],Ei.prototype,"fullwidth",void 0),n([se({type:String})],Ei.prototype,"menuCorner",void 0),n([se({type:Boolean})],Ei.prototype,"stayOpenOnBodyClick",void 0),n([se({type:String}),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setDefaultFocusState(gi[e])}))],Ei.prototype,"defaultFocus",void 0); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const Ti=Pe(class extends ze{constructor(e){var t;if(super(e),e.type!==Ne||"style"!==e.name||(null===(t=e.strings)||void 0===t?void 0:t.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(e){return Object.keys(e).reduce(((t,i)=>{const n=e[i];return null==n?t:t+`${i=i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${n};`}),"")}update(e,[t]){const{style:i}=e.element;if(void 0===this.vt){this.vt=new Set;for(const e in t)this.vt.add(e);return this.render(t)}this.vt.forEach((e=>{null==t[e]&&(this.vt.delete(e),e.includes("-")?i.removeProperty(e):i[e]="")}));for(const e in t){const n=t[e];null!=n&&(this.vt.add(e),e.includes("-")?i.setProperty(e,n):i[e]=n)}return z}}),Ii={TOP_LEFT:Yt.TOP_LEFT,TOP_RIGHT:Yt.TOP_RIGHT,BOTTOM_LEFT:Yt.BOTTOM_LEFT,BOTTOM_RIGHT:Yt.BOTTOM_RIGHT,TOP_START:Yt.TOP_START,TOP_END:Yt.TOP_END,BOTTOM_START:Yt.BOTTOM_START,BOTTOM_END:Yt.BOTTOM_END}; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class Ai extends Oe{constructor(){super(...arguments),this.mdcFoundationClass=yi,this.absolute=!1,this.fullwidth=!1,this.fixed=!1,this.x=null,this.y=null,this.quick=!1,this.open=!1,this.stayOpenOnBodyClick=!1,this.bitwiseCorner=Yt.TOP_START,this.previousMenuCorner=null,this.menuCorner="START",this.corner="TOP_START",this.styleTop="",this.styleLeft="",this.styleRight="",this.styleBottom="",this.styleMaxHeight="",this.styleTransformOrigin="",this.anchor=null,this.previouslyFocused=null,this.previousAnchor=null,this.onBodyClickBound=()=>{}}render(){const e={"mdc-menu-surface--fixed":this.fixed,"mdc-menu-surface--fullwidth":this.fullwidth},t={top:this.styleTop,left:this.styleLeft,right:this.styleRight,bottom:this.styleBottom,"max-height":this.styleMaxHeight,"transform-origin":this.styleTransformOrigin};return P` +
+ +
`}createAdapter(){return Object.assign(Object.assign({},Ae(this.mdcRoot)),{hasAnchor:()=>!!this.anchor,notifyClose:()=>{const e=new CustomEvent("closed",{bubbles:!0,composed:!0});this.open=!1,this.mdcRoot.dispatchEvent(e)},notifyClosing:()=>{const e=new CustomEvent("closing",{bubbles:!0,composed:!0});this.mdcRoot.dispatchEvent(e)},notifyOpen:()=>{const e=new CustomEvent("opened",{bubbles:!0,composed:!0});this.open=!0,this.mdcRoot.dispatchEvent(e)},isElementInContainer:()=>!1,isRtl:()=>!!this.mdcRoot&&"rtl"===getComputedStyle(this.mdcRoot).direction,setTransformOrigin:e=>{this.mdcRoot&&(this.styleTransformOrigin=e)},isFocused:()=>ke(this),saveFocus:()=>{const e=Re(),t=e.length;t||(this.previouslyFocused=null),this.previouslyFocused=e[t-1]},restoreFocus:()=>{this.previouslyFocused&&"focus"in this.previouslyFocused&&this.previouslyFocused.focus()},getInnerDimensions:()=>{const e=this.mdcRoot;return e?{width:e.offsetWidth,height:e.offsetHeight}:{width:0,height:0}},getAnchorDimensions:()=>{const e=this.anchor;return e?e.getBoundingClientRect():null},getBodyDimensions:()=>({width:document.body.clientWidth,height:document.body.clientHeight}),getWindowDimensions:()=>({width:window.innerWidth,height:window.innerHeight}),getWindowScroll:()=>({x:window.pageXOffset,y:window.pageYOffset}),setPosition:e=>{this.mdcRoot&&(this.styleLeft="left"in e?`${e.left}px`:"",this.styleRight="right"in e?`${e.right}px`:"",this.styleTop="top"in e?`${e.top}px`:"",this.styleBottom="bottom"in e?`${e.bottom}px`:"")},setMaxHeight:async e=>{this.mdcRoot&&(this.styleMaxHeight=e,await this.updateComplete,this.styleMaxHeight=`var(--mdc-menu-max-height, ${e})`)}})}onKeydown(e){this.mdcFoundation&&this.mdcFoundation.handleKeydown(e)}onBodyClick(e){if(this.stayOpenOnBodyClick)return;-1===e.composedPath().indexOf(this)&&this.close()}registerBodyClick(){this.onBodyClickBound=this.onBodyClick.bind(this),document.body.addEventListener("click",this.onBodyClickBound,{passive:!0,capture:!0})}deregisterBodyClick(){document.body.removeEventListener("click",this.onBodyClickBound,{capture:!0})}close(){this.open=!1}show(){this.open=!0}}n([he(".mdc-menu-surface")],Ai.prototype,"mdcRoot",void 0),n([he("slot")],Ai.prototype,"slotElement",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation&&!this.fixed&&this.mdcFoundation.setIsHoisted(e)}))],Ai.prototype,"absolute",void 0),n([se({type:Boolean})],Ai.prototype,"fullwidth",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation&&!this.absolute&&this.mdcFoundation.setFixedPosition(e)}))],Ai.prototype,"fixed",void 0),n([se({type:Number}),Me((function(e){this.mdcFoundation&&null!==this.y&&null!==e&&(this.mdcFoundation.setAbsolutePosition(e,this.y),this.mdcFoundation.setAnchorMargin({left:e,top:this.y,right:-e,bottom:this.y}))}))],Ai.prototype,"x",void 0),n([se({type:Number}),Me((function(e){this.mdcFoundation&&null!==this.x&&null!==e&&(this.mdcFoundation.setAbsolutePosition(this.x,e),this.mdcFoundation.setAnchorMargin({left:this.x,top:e,right:-this.x,bottom:e}))}))],Ai.prototype,"y",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setQuickOpen(e)}))],Ai.prototype,"quick",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e,t){this.mdcFoundation&&(e?this.mdcFoundation.open():void 0!==t&&this.mdcFoundation.close())}))],Ai.prototype,"open",void 0),n([se({type:Boolean})],Ai.prototype,"stayOpenOnBodyClick",void 0),n([ce(),Me((function(e){this.mdcFoundation&&this.mdcFoundation.setAnchorCorner(e)}))],Ai.prototype,"bitwiseCorner",void 0),n([se({type:String}),Me((function(e){if(this.mdcFoundation){const t="START"===e||"END"===e,i=null===this.previousMenuCorner,n=!i&&e!==this.previousMenuCorner,r=i&&"END"===e;t&&(n||r)&&(this.bitwiseCorner=this.bitwiseCorner^qt.RIGHT,this.mdcFoundation.flipCornerHorizontally(),this.previousMenuCorner=e)}}))],Ai.prototype,"menuCorner",void 0),n([se({type:String}),Me((function(e){if(this.mdcFoundation&&e){let t=Ii[e];"END"===this.menuCorner&&(t^=qt.RIGHT),this.bitwiseCorner=t}}))],Ai.prototype,"corner",void 0),n([ce()],Ai.prototype,"styleTop",void 0),n([ce()],Ai.prototype,"styleLeft",void 0),n([ce()],Ai.prototype,"styleRight",void 0),n([ce()],Ai.prototype,"styleBottom",void 0),n([ce()],Ai.prototype,"styleMaxHeight",void 0),n([ce()],Ai.prototype,"styleTransformOrigin",void 0); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Ci={BG_FOCUSED:"mdc-ripple-upgraded--background-focused",FG_ACTIVATION:"mdc-ripple-upgraded--foreground-activation",FG_DEACTIVATION:"mdc-ripple-upgraded--foreground-deactivation",ROOT:"mdc-ripple-upgraded",UNBOUNDED:"mdc-ripple-upgraded--unbounded"},Si={VAR_FG_SCALE:"--mdc-ripple-fg-scale",VAR_FG_SIZE:"--mdc-ripple-fg-size",VAR_FG_TRANSLATE_END:"--mdc-ripple-fg-translate-end",VAR_FG_TRANSLATE_START:"--mdc-ripple-fg-translate-start",VAR_LEFT:"--mdc-ripple-left",VAR_TOP:"--mdc-ripple-top"},Ri={DEACTIVATION_TIMEOUT_MS:225,FG_DEACTIVATION_MS:150,INITIAL_ORIGIN_SCALE:.6,PADDING:10,TAP_DELAY_MS:300}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var ki=["touchstart","pointerdown","mousedown","keydown"],Oi=["touchend","pointerup","mouseup","contextmenu"],Li=[],Fi=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.activationAnimationHasEnded=!1,r.activationTimer=0,r.fgDeactivationRemovalTimer=0,r.fgScale="0",r.frame={width:0,height:0},r.initialSize=0,r.layoutFrame=0,r.maxRadius=0,r.unboundedCoords={left:0,top:0},r.activationState=r.defaultActivationState(),r.activationTimerCallback=function(){r.activationAnimationHasEnded=!0,r.runDeactivationUXLogicIfReady()},r.activateHandler=function(e){r.activateImpl(e)},r.deactivateHandler=function(){r.deactivateImpl()},r.focusHandler=function(){r.handleFocus()},r.blurHandler=function(){r.handleBlur()},r.resizeHandler=function(){r.layout()},r}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return Ci},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return Si},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return Ri},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},browserSupportsCssVars:function(){return!0},computeBoundingRect:function(){return{top:0,right:0,bottom:0,left:0,width:0,height:0}},containsEventTarget:function(){return!0},deregisterDocumentInteractionHandler:function(){},deregisterInteractionHandler:function(){},deregisterResizeHandler:function(){},getWindowPageOffset:function(){return{x:0,y:0}},isSurfaceActive:function(){return!0},isSurfaceDisabled:function(){return!0},isUnbounded:function(){return!0},registerDocumentInteractionHandler:function(){},registerInteractionHandler:function(){},registerResizeHandler:function(){},removeClass:function(){},updateCssVariable:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){var e=this,t=this.supportsPressRipple();if(this.registerRootHandlers(t),t){var i=n.cssClasses,r=i.ROOT,a=i.UNBOUNDED;requestAnimationFrame((function(){e.adapter.addClass(r),e.adapter.isUnbounded()&&(e.adapter.addClass(a),e.layoutInternal())}))}},n.prototype.destroy=function(){var e=this;if(this.supportsPressRipple()){this.activationTimer&&(clearTimeout(this.activationTimer),this.activationTimer=0,this.adapter.removeClass(n.cssClasses.FG_ACTIVATION)),this.fgDeactivationRemovalTimer&&(clearTimeout(this.fgDeactivationRemovalTimer),this.fgDeactivationRemovalTimer=0,this.adapter.removeClass(n.cssClasses.FG_DEACTIVATION));var t=n.cssClasses,i=t.ROOT,r=t.UNBOUNDED;requestAnimationFrame((function(){e.adapter.removeClass(i),e.adapter.removeClass(r),e.removeCssVars()}))}this.deregisterRootHandlers(),this.deregisterDeactivationHandlers()},n.prototype.activate=function(e){this.activateImpl(e)},n.prototype.deactivate=function(){this.deactivateImpl()},n.prototype.layout=function(){var e=this;this.layoutFrame&&cancelAnimationFrame(this.layoutFrame),this.layoutFrame=requestAnimationFrame((function(){e.layoutInternal(),e.layoutFrame=0}))},n.prototype.setUnbounded=function(e){var t=n.cssClasses.UNBOUNDED;e?this.adapter.addClass(t):this.adapter.removeClass(t)},n.prototype.handleFocus=function(){var e=this;requestAnimationFrame((function(){return e.adapter.addClass(n.cssClasses.BG_FOCUSED)}))},n.prototype.handleBlur=function(){var e=this;requestAnimationFrame((function(){return e.adapter.removeClass(n.cssClasses.BG_FOCUSED)}))},n.prototype.supportsPressRipple=function(){return this.adapter.browserSupportsCssVars()},n.prototype.defaultActivationState=function(){return{activationEvent:void 0,hasDeactivationUXRun:!1,isActivated:!1,isProgrammatic:!1,wasActivatedByPointer:!1,wasElementMadeActive:!1}},n.prototype.registerRootHandlers=function(e){var t,i;if(e){try{for(var n=r(ki),a=n.next();!a.done;a=n.next()){var o=a.value;this.adapter.registerInteractionHandler(o,this.activateHandler)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(i=n.return)&&i.call(n)}finally{if(t)throw t.error}}this.adapter.isUnbounded()&&this.adapter.registerResizeHandler(this.resizeHandler)}this.adapter.registerInteractionHandler("focus",this.focusHandler),this.adapter.registerInteractionHandler("blur",this.blurHandler)},n.prototype.registerDeactivationHandlers=function(e){var t,i;if("keydown"===e.type)this.adapter.registerInteractionHandler("keyup",this.deactivateHandler);else try{for(var n=r(Oi),a=n.next();!a.done;a=n.next()){var o=a.value;this.adapter.registerDocumentInteractionHandler(o,this.deactivateHandler)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(i=n.return)&&i.call(n)}finally{if(t)throw t.error}}},n.prototype.deregisterRootHandlers=function(){var e,t;try{for(var i=r(ki),n=i.next();!n.done;n=i.next()){var a=n.value;this.adapter.deregisterInteractionHandler(a,this.activateHandler)}}catch(t){e={error:t}}finally{try{n&&!n.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}this.adapter.deregisterInteractionHandler("focus",this.focusHandler),this.adapter.deregisterInteractionHandler("blur",this.blurHandler),this.adapter.isUnbounded()&&this.adapter.deregisterResizeHandler(this.resizeHandler)},n.prototype.deregisterDeactivationHandlers=function(){var e,t;this.adapter.deregisterInteractionHandler("keyup",this.deactivateHandler);try{for(var i=r(Oi),n=i.next();!n.done;n=i.next()){var a=n.value;this.adapter.deregisterDocumentInteractionHandler(a,this.deactivateHandler)}}catch(t){e={error:t}}finally{try{n&&!n.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}},n.prototype.removeCssVars=function(){var e=this,t=n.strings;Object.keys(t).forEach((function(i){0===i.indexOf("VAR_")&&e.adapter.updateCssVariable(t[i],null)}))},n.prototype.activateImpl=function(e){var t=this;if(!this.adapter.isSurfaceDisabled()){var i=this.activationState;if(!i.isActivated){var n=this.previousActivationEvent;if(!(n&&void 0!==e&&n.type!==e.type))i.isActivated=!0,i.isProgrammatic=void 0===e,i.activationEvent=e,i.wasActivatedByPointer=!i.isProgrammatic&&(void 0!==e&&("mousedown"===e.type||"touchstart"===e.type||"pointerdown"===e.type)),void 0!==e&&Li.length>0&&Li.some((function(e){return t.adapter.containsEventTarget(e)}))?this.resetActivationState():(void 0!==e&&(Li.push(e.target),this.registerDeactivationHandlers(e)),i.wasElementMadeActive=this.checkElementMadeActive(e),i.wasElementMadeActive&&this.animateActivation(),requestAnimationFrame((function(){Li=[],i.wasElementMadeActive||void 0===e||" "!==e.key&&32!==e.keyCode||(i.wasElementMadeActive=t.checkElementMadeActive(e),i.wasElementMadeActive&&t.animateActivation()),i.wasElementMadeActive||(t.activationState=t.defaultActivationState())})))}}},n.prototype.checkElementMadeActive=function(e){return void 0===e||"keydown"!==e.type||this.adapter.isSurfaceActive()},n.prototype.animateActivation=function(){var e=this,t=n.strings,i=t.VAR_FG_TRANSLATE_START,r=t.VAR_FG_TRANSLATE_END,a=n.cssClasses,o=a.FG_DEACTIVATION,d=a.FG_ACTIVATION,l=n.numbers.DEACTIVATION_TIMEOUT_MS;this.layoutInternal();var s="",c="";if(!this.adapter.isUnbounded()){var m=this.getFgTranslationCoordinates(),p=m.startPoint,h=m.endPoint;s=p.x+"px, "+p.y+"px",c=h.x+"px, "+h.y+"px"}this.adapter.updateCssVariable(i,s),this.adapter.updateCssVariable(r,c),clearTimeout(this.activationTimer),clearTimeout(this.fgDeactivationRemovalTimer),this.rmBoundedActivationClasses(),this.adapter.removeClass(o),this.adapter.computeBoundingRect(),this.adapter.addClass(d),this.activationTimer=setTimeout((function(){e.activationTimerCallback()}),l)},n.prototype.getFgTranslationCoordinates=function(){var e,t=this.activationState,i=t.activationEvent;return e=t.wasActivatedByPointer?function(e,t,i){if(!e)return{x:0,y:0};var n,r,a=t.x,o=t.y,d=a+i.left,l=o+i.top;if("touchstart"===e.type){var s=e;n=s.changedTouches[0].pageX-d,r=s.changedTouches[0].pageY-l}else{var c=e;n=c.pageX-d,r=c.pageY-l}return{x:n,y:r}}(i,this.adapter.getWindowPageOffset(),this.adapter.computeBoundingRect()):{x:this.frame.width/2,y:this.frame.height/2},{startPoint:e={x:e.x-this.initialSize/2,y:e.y-this.initialSize/2},endPoint:{x:this.frame.width/2-this.initialSize/2,y:this.frame.height/2-this.initialSize/2}}},n.prototype.runDeactivationUXLogicIfReady=function(){var e=this,t=n.cssClasses.FG_DEACTIVATION,i=this.activationState,r=i.hasDeactivationUXRun,a=i.isActivated;(r||!a)&&this.activationAnimationHasEnded&&(this.rmBoundedActivationClasses(),this.adapter.addClass(t),this.fgDeactivationRemovalTimer=setTimeout((function(){e.adapter.removeClass(t)}),Ri.FG_DEACTIVATION_MS))},n.prototype.rmBoundedActivationClasses=function(){var e=n.cssClasses.FG_ACTIVATION;this.adapter.removeClass(e),this.activationAnimationHasEnded=!1,this.adapter.computeBoundingRect()},n.prototype.resetActivationState=function(){var e=this;this.previousActivationEvent=this.activationState.activationEvent,this.activationState=this.defaultActivationState(),setTimeout((function(){return e.previousActivationEvent=void 0}),n.numbers.TAP_DELAY_MS)},n.prototype.deactivateImpl=function(){var e=this,t=this.activationState;if(t.isActivated){var n=i({},t);t.isProgrammatic?(requestAnimationFrame((function(){e.animateDeactivation(n)})),this.resetActivationState()):(this.deregisterDeactivationHandlers(),requestAnimationFrame((function(){e.activationState.hasDeactivationUXRun=!0,e.animateDeactivation(n),e.resetActivationState()})))}},n.prototype.animateDeactivation=function(e){var t=e.wasActivatedByPointer,i=e.wasElementMadeActive;(t||i)&&this.runDeactivationUXLogicIfReady()},n.prototype.layoutInternal=function(){var e=this;this.frame=this.adapter.computeBoundingRect();var t=Math.max(this.frame.height,this.frame.width);this.maxRadius=this.adapter.isUnbounded()?t:Math.sqrt(Math.pow(e.frame.width,2)+Math.pow(e.frame.height,2))+n.numbers.PADDING;var i=Math.floor(t*n.numbers.INITIAL_ORIGIN_SCALE);this.adapter.isUnbounded()&&i%2!=0?this.initialSize=i-1:this.initialSize=i,this.fgScale=""+this.maxRadius/this.initialSize,this.updateLayoutCssVars()},n.prototype.updateLayoutCssVars=function(){var e=n.strings,t=e.VAR_FG_SIZE,i=e.VAR_LEFT,r=e.VAR_TOP,a=e.VAR_FG_SCALE;this.adapter.updateCssVariable(t,this.initialSize+"px"),this.adapter.updateCssVariable(a,this.fgScale),this.adapter.isUnbounded()&&(this.unboundedCoords={left:Math.round(this.frame.width/2-this.initialSize/2),top:Math.round(this.frame.height/2-this.initialSize/2)},this.adapter.updateCssVariable(i,this.unboundedCoords.left+"px"),this.adapter.updateCssVariable(r,this.unboundedCoords.top+"px"))},n}(ye),Di=Fi; +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class $i extends Oe{constructor(){super(...arguments),this.primary=!1,this.accent=!1,this.unbounded=!1,this.disabled=!1,this.activated=!1,this.selected=!1,this.internalUseStateLayerCustomProperties=!1,this.hovering=!1,this.bgFocused=!1,this.fgActivation=!1,this.fgDeactivation=!1,this.fgScale="",this.fgSize="",this.translateStart="",this.translateEnd="",this.leftPos="",this.topPos="",this.mdcFoundationClass=Di}get isActive(){return e=this.parentElement||this,t=":active",(e.matches||e.webkitMatchesSelector||e.msMatchesSelector).call(e,t); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var e,t}createAdapter(){return{browserSupportsCssVars:()=>!0,isUnbounded:()=>this.unbounded,isSurfaceActive:()=>this.isActive,isSurfaceDisabled:()=>this.disabled,addClass:e=>{switch(e){case"mdc-ripple-upgraded--background-focused":this.bgFocused=!0;break;case"mdc-ripple-upgraded--foreground-activation":this.fgActivation=!0;break;case"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation=!0}},removeClass:e=>{switch(e){case"mdc-ripple-upgraded--background-focused":this.bgFocused=!1;break;case"mdc-ripple-upgraded--foreground-activation":this.fgActivation=!1;break;case"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation=!1}},containsEventTarget:()=>!0,registerInteractionHandler:()=>{},deregisterInteractionHandler:()=>{},registerDocumentInteractionHandler:()=>{},deregisterDocumentInteractionHandler:()=>{},registerResizeHandler:()=>{},deregisterResizeHandler:()=>{},updateCssVariable:(e,t)=>{switch(e){case"--mdc-ripple-fg-scale":this.fgScale=t;break;case"--mdc-ripple-fg-size":this.fgSize=t;break;case"--mdc-ripple-fg-translate-end":this.translateEnd=t;break;case"--mdc-ripple-fg-translate-start":this.translateStart=t;break;case"--mdc-ripple-left":this.leftPos=t;break;case"--mdc-ripple-top":this.topPos=t}},computeBoundingRect:()=>(this.parentElement||this).getBoundingClientRect(),getWindowPageOffset:()=>({x:window.pageXOffset,y:window.pageYOffset})}}startPress(e){this.waitForFoundation((()=>{this.mdcFoundation.activate(e)}))}endPress(){this.waitForFoundation((()=>{this.mdcFoundation.deactivate()}))}startFocus(){this.waitForFoundation((()=>{this.mdcFoundation.handleFocus()}))}endFocus(){this.waitForFoundation((()=>{this.mdcFoundation.handleBlur()}))}startHover(){this.hovering=!0}endHover(){this.hovering=!1}waitForFoundation(e){this.mdcFoundation?e():this.updateComplete.then(e)}update(e){e.has("disabled")&&this.disabled&&this.endHover(),super.update(e)}render(){const e=this.activated&&(this.primary||!this.accent),t=this.selected&&(this.primary||!this.accent),i={"mdc-ripple-surface--accent":this.accent,"mdc-ripple-surface--primary--activated":e,"mdc-ripple-surface--accent--activated":this.accent&&this.activated,"mdc-ripple-surface--primary--selected":t,"mdc-ripple-surface--accent--selected":this.accent&&this.selected,"mdc-ripple-surface--disabled":this.disabled,"mdc-ripple-surface--hover":this.hovering,"mdc-ripple-surface--primary":this.primary,"mdc-ripple-surface--selected":this.selected,"mdc-ripple-upgraded--background-focused":this.bgFocused,"mdc-ripple-upgraded--foreground-activation":this.fgActivation,"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation,"mdc-ripple-upgraded--unbounded":this.unbounded,"mdc-ripple-surface--internal-use-state-layer-custom-properties":this.internalUseStateLayerCustomProperties};return P` +
`}}n([he(".mdc-ripple-surface")],$i.prototype,"mdcRoot",void 0),n([se({type:Boolean})],$i.prototype,"primary",void 0),n([se({type:Boolean})],$i.prototype,"accent",void 0),n([se({type:Boolean})],$i.prototype,"unbounded",void 0),n([se({type:Boolean})],$i.prototype,"disabled",void 0),n([se({type:Boolean})],$i.prototype,"activated",void 0),n([se({type:Boolean})],$i.prototype,"selected",void 0),n([se({type:Boolean})],$i.prototype,"internalUseStateLayerCustomProperties",void 0),n([ce()],$i.prototype,"hovering",void 0),n([ce()],$i.prototype,"bgFocused",void 0),n([ce()],$i.prototype,"fgActivation",void 0),n([ce()],$i.prototype,"fgDeactivation",void 0),n([ce()],$i.prototype,"fgScale",void 0),n([ce()],$i.prototype,"fgSize",void 0),n([ce()],$i.prototype,"translateStart",void 0),n([ce()],$i.prototype,"translateEnd",void 0),n([ce()],$i.prototype,"leftPos",void 0),n([ce()],$i.prototype,"topPos",void 0); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Mi={NOTCH_ELEMENT_SELECTOR:".mdc-notched-outline__notch"},Ni={NOTCH_ELEMENT_PADDING:8},Hi={NO_LABEL:"mdc-notched-outline--no-label",OUTLINE_NOTCHED:"mdc-notched-outline--notched",OUTLINE_UPGRADED:"mdc-notched-outline--upgraded"},Bi=function(e){function n(t){return e.call(this,i(i({},n.defaultAdapter),t))||this}return t(n,e),Object.defineProperty(n,"strings",{get:function(){return Mi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"cssClasses",{get:function(){return Hi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return Ni},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},setNotchWidthProperty:function(){},removeNotchWidthProperty:function(){}}},enumerable:!1,configurable:!0}),n.prototype.notch=function(e){var t=n.cssClasses.OUTLINE_NOTCHED;e>0&&(e+=Ni.NOTCH_ELEMENT_PADDING),this.adapter.setNotchWidthProperty(e),this.adapter.addClass(t)},n.prototype.closeNotch=function(){var e=n.cssClasses.OUTLINE_NOTCHED;this.adapter.removeClass(e),this.adapter.removeNotchWidthProperty()},n}(ye); +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class Pi extends Oe{constructor(){super(...arguments),this.mdcFoundationClass=Bi,this.width=0,this.open=!1,this.lastOpen=this.open}createAdapter(){return{addClass:e=>this.mdcRoot.classList.add(e),removeClass:e=>this.mdcRoot.classList.remove(e),setNotchWidthProperty:e=>this.notchElement.style.setProperty("width",`${e}px`),removeNotchWidthProperty:()=>this.notchElement.style.removeProperty("width")}}openOrClose(e,t){this.mdcFoundation&&(e&&void 0!==t?this.mdcFoundation.notch(t):this.mdcFoundation.closeNotch())}render(){this.openOrClose(this.open,this.width);const e=Ve({"mdc-notched-outline--notched":this.open});return P` + + + + + + + `}}n([he(".mdc-notched-outline")],Pi.prototype,"mdcRoot",void 0),n([se({type:Number})],Pi.prototype,"width",void 0),n([se({type:Boolean,reflect:!0})],Pi.prototype,"open",void 0),n([he(".mdc-notched-outline__notch")],Pi.prototype,"notchElement",void 0); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const zi=c`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px;z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}.mdc-select{display:inline-flex;position:relative}.mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87)}.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-select.mdc-select--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#6200ee;fill:var(--mdc-theme-primary, #6200ee)}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled)+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__icon{color:rgba(0, 0, 0, 0.54)}.mdc-select.mdc-select--disabled .mdc-select__icon{color:rgba(0, 0, 0, 0.38)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:red}.mdc-select.mdc-select--disabled .mdc-floating-label{color:GrayText}.mdc-select.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}.mdc-select.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select.mdc-select--disabled .mdc-notched-outline__trailing{border-color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__icon{color:GrayText}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:GrayText}}.mdc-select .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-select .mdc-select__anchor{padding-left:16px;padding-right:0}[dir=rtl] .mdc-select .mdc-select__anchor,.mdc-select .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:16px}.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor{padding-left:0;padding-right:0}[dir=rtl] .mdc-select.mdc-select--with-leading-icon .mdc-select__anchor,.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:0}.mdc-select .mdc-select__icon{width:24px;height:24px;font-size:24px}.mdc-select .mdc-select__dropdown-icon{width:24px;height:24px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item,.mdc-select .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:12px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic,.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:12px;margin-right:0}.mdc-select__dropdown-icon{margin-left:12px;margin-right:12px;display:inline-flex;position:relative;align-self:center;align-items:center;justify-content:center;flex-shrink:0;pointer-events:none}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active,.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{position:absolute;top:0;left:0}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-graphic{width:41.6666666667%;height:20.8333333333%}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:1;transition:opacity 75ms linear 75ms}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:0;transition:opacity 75ms linear}[dir=rtl] .mdc-select__dropdown-icon,.mdc-select__dropdown-icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:0;transition:opacity 49.5ms linear}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:1;transition:opacity 100.5ms linear 49.5ms}.mdc-select__anchor{width:200px;min-width:0;flex:1 1 auto;position:relative;box-sizing:border-box;overflow:hidden;outline:none;cursor:pointer}.mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-select__selected-text-container{display:flex;appearance:none;pointer-events:none;box-sizing:border-box;width:auto;min-width:0;flex-grow:1;height:28px;border:none;outline:none;padding:0;background-color:transparent;color:inherit}.mdc-select__selected-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;width:100%;text-align:left}[dir=rtl] .mdc-select__selected-text,.mdc-select__selected-text[dir=rtl]{text-align:right}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--invalid+.mdc-select-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--disabled{cursor:default;pointer-events:none}.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item{padding-left:12px;padding-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item,.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:12px;padding-right:12px}.mdc-select__menu .mdc-deprecated-list .mdc-select__icon,.mdc-select__menu .mdc-list .mdc-select__icon{margin-left:0;margin-right:0}[dir=rtl] .mdc-select__menu .mdc-deprecated-list .mdc-select__icon,[dir=rtl] .mdc-select__menu .mdc-list .mdc-select__icon,.mdc-select__menu .mdc-deprecated-list .mdc-select__icon[dir=rtl],.mdc-select__menu .mdc-list .mdc-select__icon[dir=rtl]{margin-left:0;margin-right:0}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-list-item__start{display:inline-flex;align-items:center}.mdc-select__option{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select__option,.mdc-select__option[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select__one-line-option.mdc-list-item--with-one-line{height:48px}.mdc-select__two-line-option.mdc-list-item--with-two-lines{height:64px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__start{margin-top:20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end{display:block;margin-top:0;line-height:normal}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before{display:inline-block;width:0;height:36px;content:"";vertical-align:0}.mdc-select__option-with-leading-content{padding-left:0;padding-right:12px}.mdc-select__option-with-leading-content.mdc-list-item{padding-left:0;padding-right:auto}[dir=rtl] .mdc-select__option-with-leading-content.mdc-list-item,.mdc-select__option-with-leading-content.mdc-list-item[dir=rtl]{padding-left:auto;padding-right:0}.mdc-select__option-with-leading-content .mdc-list-item__start{margin-left:12px;margin-right:0}[dir=rtl] .mdc-select__option-with-leading-content .mdc-list-item__start,.mdc-select__option-with-leading-content .mdc-list-item__start[dir=rtl]{margin-left:0;margin-right:12px}.mdc-select__option-with-leading-content .mdc-list-item__start{width:36px;height:24px}[dir=rtl] .mdc-select__option-with-leading-content,.mdc-select__option-with-leading-content[dir=rtl]{padding-left:12px;padding-right:0}.mdc-select__option-with-meta.mdc-list-item{padding-left:auto;padding-right:0}[dir=rtl] .mdc-select__option-with-meta.mdc-list-item,.mdc-select__option-with-meta.mdc-list-item[dir=rtl]{padding-left:0;padding-right:auto}.mdc-select__option-with-meta .mdc-list-item__end{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select__option-with-meta .mdc-list-item__end,.mdc-select__option-with-meta .mdc-list-item__end[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--filled .mdc-select__anchor{height:56px;display:flex;align-items:baseline}.mdc-select--filled .mdc-select__anchor::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor::before{display:none}.mdc-select--filled .mdc-select__anchor{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0}.mdc-select--filled:not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke}.mdc-select--filled.mdc-select--disabled .mdc-select__anchor{background-color:#fafafa}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-select--filled:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--filled.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-select--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-select--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-select--filled .mdc-menu-surface--is-open-below{border-top-left-radius:0px;border-top-right-radius:0px}.mdc-select--filled.mdc-select--focused.mdc-line-ripple::after{transform:scale(1, 2);opacity:1}.mdc-select--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-select--filled .mdc-floating-label,.mdc-select--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{left:48px;right:initial}[dir=rtl] .mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined{border:none}.mdc-select--outlined .mdc-select__anchor{height:56px}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-56px{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-select--outlined .mdc-select__anchor{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-select--outlined+.mdc-select-helper-text{margin-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined.mdc-select--disabled .mdc-select__anchor{background-color:transparent}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}.mdc-select--outlined .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-select--outlined .mdc-select__anchor{display:flex;align-items:baseline;overflow:visible}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--outlined .mdc-select__anchor::before{display:none}.mdc-select--outlined .mdc-select__selected-text-container{display:flex;border:none;z-index:1;background-color:transparent}.mdc-select--outlined .mdc-select__icon{z-index:2}.mdc-select--outlined .mdc-floating-label{line-height:1.15rem;left:4px;right:initial}[dir=rtl] .mdc-select--outlined .mdc-floating-label,.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-select--outlined.mdc-select--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake,.mdc-select--outlined.mdc-select--with-leading-icon[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 96px)}.mdc-select--outlined .mdc-menu-surface{margin-bottom:8px}.mdc-select--outlined.mdc-select--no-label .mdc-menu-surface,.mdc-select--outlined .mdc-menu-surface--is-open-below{margin-bottom:0}.mdc-select__anchor{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-select__anchor .mdc-select__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-select__anchor .mdc-select__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-select__anchor.mdc-ripple-upgraded--unbounded .mdc-select__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-select__anchor.mdc-ripple-upgraded--foreground-activation .mdc-select__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-select__anchor.mdc-ripple-upgraded--foreground-deactivation .mdc-select__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-select__anchor:hover .mdc-select__ripple::before,.mdc-select__anchor.mdc-ripple-surface--hover .mdc-select__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__anchor.mdc-ripple-upgraded--background-focused .mdc-select__ripple::before,.mdc-select__anchor:not(.mdc-ripple-upgraded):focus .mdc-select__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__anchor .mdc-select__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-deprecated-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-deprecated-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-deprecated-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-deprecated-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select-helper-text{margin:0;margin-left:16px;margin-right:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal}[dir=rtl] .mdc-select-helper-text,.mdc-select-helper-text[dir=rtl]{margin-left:16px;margin-right:16px}.mdc-select-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-select-helper-text--validation-msg{opacity:0;transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select--invalid+.mdc-select-helper-text--validation-msg,.mdc-select-helper-text--validation-msg-persistent{opacity:1}.mdc-select--with-leading-icon .mdc-select__icon{display:inline-block;box-sizing:border-box;border:none;text-decoration:none;cursor:pointer;user-select:none;flex-shrink:0;align-self:center;background-color:transparent;fill:currentColor}.mdc-select--with-leading-icon .mdc-select__icon{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__icon,.mdc-select--with-leading-icon .mdc-select__icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select__icon:not([tabindex]),.mdc-select__icon[tabindex="-1"]{cursor:default;pointer-events:none}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-block;vertical-align:top;outline:none}.mdc-select{width:100%}[hidden]{display:none}.mdc-select__icon{z-index:2}.mdc-select--with-leading-icon{--mdc-list-item-graphic-margin: calc( 48px - var(--mdc-list-item-graphic-size, 24px) - var(--mdc-list-side-padding, 16px) )}.mdc-select .mdc-select__anchor .mdc-select__selected-text{overflow:hidden}.mdc-select .mdc-select__anchor *{display:inline-flex}.mdc-select .mdc-select__anchor .mdc-floating-label{display:inline-block}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-idle-border-color, rgba(0, 0, 0, 0.38) );--mdc-notched-outline-notch-offset: 1px}:host(:not([disabled]):hover) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87);color:var(--mdc-select-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-select-idle-line-color, rgba(0, 0, 0, 0.42))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-select-hover-line-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--outlined):not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke;background-color:var(--mdc-select-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-select__dropdown-icon{fill:var(--mdc-select-error-dropdown-icon-color, var(--mdc-select-error-color, var(--mdc-theme-error, #b00020)))}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label,:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label::after{color:var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select.mdc-select--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}.mdc-select__menu--invalid{--mdc-theme-primary: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.6);color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54);fill:var(--mdc-select-dropdown-icon-color, rgba(0, 0, 0, 0.54))}:host(:not([disabled])) .mdc-select.mdc-select--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px;--mdc-notched-outline-notch-offset: 2px}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-select__dropdown-icon{fill:rgba(98,0,238,.87);fill:var(--mdc-select-focused-dropdown-icon-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)))}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label::after{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select-helper-text:not(.mdc-select-helper-text--validation-msg){color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]){pointer-events:none}:host([disabled]) .mdc-select:not(.mdc-select--outlined).mdc-select--disabled .mdc-select__anchor{background-color:#fafafa;background-color:var(--mdc-select-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-select.mdc-select--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-select .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38);fill:var(--mdc-select-disabled-dropdown-icon-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select-helper-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Vi=c`@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}:host{display:block}.mdc-deprecated-list{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);line-height:1.5rem;margin:0;padding:8px 0;list-style-type:none;color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));padding:var(--mdc-list-vertical-padding, 8px) 0}.mdc-deprecated-list:focus{outline:none}.mdc-deprecated-list-item{height:48px}.mdc-deprecated-list--dense{padding-top:4px;padding-bottom:4px;font-size:.812rem}.mdc-deprecated-list ::slotted([divider]){height:0;margin:0;border:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:rgba(0, 0, 0, 0.12)}.mdc-deprecated-list ::slotted([divider][padded]){margin:0 var(--mdc-list-side-padding, 16px)}.mdc-deprecated-list ::slotted([divider][inset]){margin-left:var(--mdc-list-inset-margin, 72px);margin-right:0;width:calc( 100% - var(--mdc-list-inset-margin, 72px) )}[dir=rtl] .mdc-deprecated-list ::slotted([divider][inset]),.mdc-deprecated-list ::slotted([divider][inset][dir=rtl]){margin-left:0;margin-right:var(--mdc-list-inset-margin, 72px)}.mdc-deprecated-list ::slotted([divider][inset][padded]){width:calc( 100% - var(--mdc-list-inset-margin, 72px) - var(--mdc-list-side-padding, 16px) )}.mdc-deprecated-list--dense ::slotted([mwc-list-item]){height:40px}.mdc-deprecated-list--dense ::slotted([mwc-list]){--mdc-list-item-graphic-size: 20px}.mdc-deprecated-list--two-line.mdc-deprecated-list--dense ::slotted([mwc-list-item]),.mdc-deprecated-list--avatar-list.mdc-deprecated-list--dense ::slotted([mwc-list-item]){height:60px}.mdc-deprecated-list--avatar-list.mdc-deprecated-list--dense ::slotted([mwc-list]){--mdc-list-item-graphic-size: 36px}:host([noninteractive]){pointer-events:none;cursor:default}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text){display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text)::before{display:inline-block;width:0;height:24px;content:"";vertical-align:0}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text)::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Ui=c`:host{cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;height:48px;display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding:0;padding-left:var(--mdc-list-side-padding, 16px);padding-right:var(--mdc-list-side-padding, 16px);outline:none;height:48px;color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}:host:focus{outline:none}:host([activated]){color:#6200ee;color:var(--mdc-theme-primary, #6200ee);--mdc-ripple-color: var( --mdc-theme-primary, #6200ee )}:host([activated]) .mdc-deprecated-list-item__graphic{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host([activated]) .fake-activated-ripple::before{position:absolute;display:block;top:0;bottom:0;left:0;right:0;width:100%;height:100%;pointer-events:none;z-index:1;content:"";opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12);background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-deprecated-list-item__graphic{flex-shrink:0;align-items:center;justify-content:center;fill:currentColor;display:inline-flex}.mdc-deprecated-list-item__graphic ::slotted(*){flex-shrink:0;align-items:center;justify-content:center;fill:currentColor;width:100%;height:100%;text-align:center}.mdc-deprecated-list-item__meta{width:var(--mdc-list-item-meta-size, 24px);height:var(--mdc-list-item-meta-size, 24px);margin-left:auto;margin-right:0;color:rgba(0, 0, 0, 0.38);color:var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.38))}.mdc-deprecated-list-item__meta.multi{width:auto}.mdc-deprecated-list-item__meta ::slotted(*){width:var(--mdc-list-item-meta-size, 24px);line-height:var(--mdc-list-item-meta-size, 24px)}.mdc-deprecated-list-item__meta ::slotted(.material-icons),.mdc-deprecated-list-item__meta ::slotted(mwc-icon){line-height:var(--mdc-list-item-meta-size, 24px) !important}.mdc-deprecated-list-item__meta ::slotted(:not(.material-icons):not(mwc-icon)){-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit)}[dir=rtl] .mdc-deprecated-list-item__meta,.mdc-deprecated-list-item__meta[dir=rtl]{margin-left:0;margin-right:auto}.mdc-deprecated-list-item__meta ::slotted(*){width:100%;height:100%}.mdc-deprecated-list-item__text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mdc-deprecated-list-item__text ::slotted([for]),.mdc-deprecated-list-item__text[for]{pointer-events:none}.mdc-deprecated-list-item__primary-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal;margin-bottom:-20px;display:block}.mdc-deprecated-list-item__primary-text::before{display:inline-block;width:0;height:32px;content:"";vertical-align:0}.mdc-deprecated-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-deprecated-list-item__secondary-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal;display:block}.mdc-deprecated-list-item__secondary-text::before{display:inline-block;width:0;height:20px;content:"";vertical-align:0}.mdc-deprecated-list--dense .mdc-deprecated-list-item__secondary-text{font-size:inherit}* ::slotted(a),a{color:inherit;text-decoration:none}:host([twoline]){height:72px}:host([twoline]) .mdc-deprecated-list-item__text{align-self:flex-start}:host([disabled]),:host([noninteractive]){cursor:default;pointer-events:none}:host([disabled]) .mdc-deprecated-list-item__text ::slotted(*){opacity:.38}:host([disabled]) .mdc-deprecated-list-item__text ::slotted(*),:host([disabled]) .mdc-deprecated-list-item__primary-text ::slotted(*),:host([disabled]) .mdc-deprecated-list-item__secondary-text ::slotted(*){color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-deprecated-list-item__secondary-text ::slotted(*){color:rgba(0, 0, 0, 0.54);color:var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54))}.mdc-deprecated-list-item__graphic ::slotted(*){background-color:transparent;color:rgba(0, 0, 0, 0.38);color:var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38))}.mdc-deprecated-list-group__subheader ::slotted(*){color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 40px);height:var(--mdc-list-item-graphic-size, 40px)}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 40px);line-height:var(--mdc-list-item-graphic-size, 40px)}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 40px) !important}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(*){border-radius:50%}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic,:host([graphic=medium]) .mdc-deprecated-list-item__graphic,:host([graphic=large]) .mdc-deprecated-list-item__graphic,:host([graphic=control]) .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:var(--mdc-list-item-graphic-margin, 16px)}[dir=rtl] :host([graphic=avatar]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=medium]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=large]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=control]) .mdc-deprecated-list-item__graphic,:host([graphic=avatar]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=medium]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=large]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=control]) .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:var(--mdc-list-item-graphic-margin, 16px);margin-right:0}:host([graphic=icon]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 24px);height:var(--mdc-list-item-graphic-size, 24px);margin-left:0;margin-right:var(--mdc-list-item-graphic-margin, 32px)}:host([graphic=icon]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 24px);line-height:var(--mdc-list-item-graphic-size, 24px)}:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 24px) !important}[dir=rtl] :host([graphic=icon]) .mdc-deprecated-list-item__graphic,:host([graphic=icon]) .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:var(--mdc-list-item-graphic-margin, 32px);margin-right:0}:host([graphic=avatar]:not([twoLine])),:host([graphic=icon]:not([twoLine])){height:56px}:host([graphic=medium]:not([twoLine])),:host([graphic=large]:not([twoLine])){height:72px}:host([graphic=medium]) .mdc-deprecated-list-item__graphic,:host([graphic=large]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 56px);height:var(--mdc-list-item-graphic-size, 56px)}:host([graphic=medium]) .mdc-deprecated-list-item__graphic.multi,:host([graphic=large]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(*),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 56px);line-height:var(--mdc-list-item-graphic-size, 56px)}:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 56px) !important}:host([graphic=large]){padding-left:0px}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Xi=c`.mdc-ripple-surface{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;position:relative;outline:none;overflow:hidden}.mdc-ripple-surface::before,.mdc-ripple-surface::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-ripple-surface::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-ripple-surface::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-ripple-surface.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-ripple-surface.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-activation::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-deactivation::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface::before,.mdc-ripple-surface::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-ripple-surface.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded],.mdc-ripple-upgraded--unbounded{overflow:visible}.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::after,.mdc-ripple-upgraded--unbounded::before,.mdc-ripple-upgraded--unbounded::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::before,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface::before,.mdc-ripple-surface::after{background-color:#000;background-color:var(--mdc-ripple-color, #000)}.mdc-ripple-surface:hover::before,.mdc-ripple-surface.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}:host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:block}:host .mdc-ripple-surface{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;will-change:unset}.mdc-ripple-surface--primary::before,.mdc-ripple-surface--primary::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary:hover::before,.mdc-ripple-surface--primary.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface--primary.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--primary.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--primary--activated::before{opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12)}.mdc-ripple-surface--primary--activated::before,.mdc-ripple-surface--primary--activated::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary--activated:hover::before,.mdc-ripple-surface--primary--activated.mdc-ripple-surface--hover::before{opacity:0.16;opacity:var(--mdc-ripple-hover-opacity, 0.16)}.mdc-ripple-surface--primary--activated.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-focus-opacity, 0.24)}.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--primary--activated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--primary--selected::before{opacity:0.08;opacity:var(--mdc-ripple-selected-opacity, 0.08)}.mdc-ripple-surface--primary--selected::before,.mdc-ripple-surface--primary--selected::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary--selected:hover::before,.mdc-ripple-surface--primary--selected.mdc-ripple-surface--hover::before{opacity:0.12;opacity:var(--mdc-ripple-hover-opacity, 0.12)}.mdc-ripple-surface--primary--selected.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-focus-opacity, 0.2)}.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--primary--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--accent::before,.mdc-ripple-surface--accent::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent:hover::before,.mdc-ripple-surface--accent.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface--accent.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--accent.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--accent--activated::before{opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12)}.mdc-ripple-surface--accent--activated::before,.mdc-ripple-surface--accent--activated::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent--activated:hover::before,.mdc-ripple-surface--accent--activated.mdc-ripple-surface--hover::before{opacity:0.16;opacity:var(--mdc-ripple-hover-opacity, 0.16)}.mdc-ripple-surface--accent--activated.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-focus-opacity, 0.24)}.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--accent--activated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--accent--selected::before{opacity:0.08;opacity:var(--mdc-ripple-selected-opacity, 0.08)}.mdc-ripple-surface--accent--selected::before,.mdc-ripple-surface--accent--selected::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent--selected:hover::before,.mdc-ripple-surface--accent--selected.mdc-ripple-surface--hover::before{opacity:0.12;opacity:var(--mdc-ripple-hover-opacity, 0.12)}.mdc-ripple-surface--accent--selected.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-focus-opacity, 0.2)}.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--accent--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--disabled{opacity:0}.mdc-ripple-surface--internal-use-state-layer-custom-properties::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties::after{background-color:#000;background-color:var(--mdc-ripple-hover-state-layer-color, #000)}.mdc-ripple-surface--internal-use-state-layer-custom-properties:hover::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-state-layer-opacity, 0.04)}.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-state-layer-opacity, 0.12)}.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-pressed-state-layer-opacity, 0.12)}.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-pressed-state-layer-opacity, 0.12)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,ji=c`mwc-list ::slotted([mwc-list-item]:not([twoline])),mwc-list ::slotted([noninteractive]:not([twoline])){height:var(--mdc-menu-item-height, 48px)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Wi=c`.mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:calc(100vw - 32px);max-width:var(--mdc-menu-max-width, calc(100vw - 32px));max-height:calc(100vh - 32px);max-height:var(--mdc-menu-max-height, calc(100vh - 32px));margin:0;padding:0;transform:scale(1);transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;transition:opacity .03s linear,transform .12s cubic-bezier(0, 0, 0.2, 1),height 250ms cubic-bezier(0, 0, 0.2, 1);box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12);background-color:#fff;background-color:var(--mdc-theme-surface, #fff);color:#000;color:var(--mdc-theme-on-surface, #000);border-radius:4px;border-radius:var(--mdc-shape-medium, 4px);transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--animating-open{display:inline-block;transform:scale(0.8);opacity:0}.mdc-menu-surface--open{display:inline-block;transform:scale(1);opacity:1}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0;transition:opacity .075s linear}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-menu-surface--fullwidth{width:100%}:host(:not([open])){display:none}.mdc-menu-surface{z-index:8;z-index:var(--mdc-menu-z-index, 8);min-width:112px;min-width:var(--mdc-menu-min-width, 112px)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Gi=c`.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}:host{display:block;position:absolute;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] :host,:host([dir=rtl]){text-align:right}::slotted(.mdc-floating-label){display:inline-block;position:relative;top:17px;bottom:auto;max-width:100%}::slotted(.mdc-floating-label--float-above){text-overflow:clip}.mdc-notched-outline--upgraded ::slotted(.mdc-floating-label--float-above){max-width:calc(100% / 0.75)}.mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-notched-outline .mdc-notched-outline__leading,.mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{border-color:var(--mdc-notched-outline-border-color, var(--mdc-theme-primary, #6200ee));border-width:1px;border-width:var(--mdc-notched-outline-stroke-width, 1px)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0;padding-top:var(--mdc-notched-outline-notch-offset, 0)}`,qi={"mwc-select":class extends ai{static get styles(){return zi}},"mwc-list":class extends hi{static get styles(){return Vi}},"mwc-list-item":class extends fi{static get styles(){return Ui}},"mwc-ripple":class extends $i{static get styles(){return Xi}},"mwc-menu":class extends Ei{static get styles(){return ji}},"mwc-menu-surface":class extends Ai{static get styles(){return Wi}},"mwc-notched-outline":class extends Pi{static get styles(){return Gi}}};function Yi(e,t,i){if(void 0!==t) +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +return function(e,t,i){const n=e.constructor;if(!i){const e=`__${t}`;if(!(i=n.getPropertyDescriptor(t,e)))throw new Error("@ariaProperty must be used after a @property decorator")}const r=i;let a="";if(!r.set)throw new Error(`@ariaProperty requires a setter for ${t}`);if(e.dispatchWizEvent)return i;const o={configurable:!0,enumerable:!0,set(e){if(""===a){const e=n.getPropertyOptions(t);a="string"==typeof e.attribute?e.attribute:t}this.hasAttribute(a)&&this.removeAttribute(a),r.set.call(this,e)}};return r.get&&(o.get=function(){return r.get.call(this)}),o}(e,t,i);throw new Error("@ariaProperty only supports TypeScript Decorators")} +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var Ki={CHECKED:"mdc-switch--checked",DISABLED:"mdc-switch--disabled"},Zi={ARIA_CHECKED_ATTR:"aria-checked",NATIVE_CONTROL_SELECTOR:".mdc-switch__native-control",RIPPLE_SURFACE_SELECTOR:".mdc-switch__thumb-underlay"},Qi=function(e){function n(t){return e.call(this,i(i({},n.defaultAdapter),t))||this}return t(n,e),Object.defineProperty(n,"strings",{get:function(){return Zi},enumerable:!1,configurable:!0}),Object.defineProperty(n,"cssClasses",{get:function(){return Ki},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},setNativeControlChecked:function(){},setNativeControlDisabled:function(){},setNativeControlAttr:function(){}}},enumerable:!1,configurable:!0}),n.prototype.setChecked=function(e){this.adapter.setNativeControlChecked(e),this.updateAriaChecked(e),this.updateCheckedStyling(e)},n.prototype.setDisabled=function(e){this.adapter.setNativeControlDisabled(e),e?this.adapter.addClass(Ki.DISABLED):this.adapter.removeClass(Ki.DISABLED)},n.prototype.handleChange=function(e){var t=e.target;this.updateAriaChecked(t.checked),this.updateCheckedStyling(t.checked)},n.prototype.updateCheckedStyling=function(e){e?this.adapter.addClass(Ki.CHECKED):this.adapter.removeClass(Ki.CHECKED)},n.prototype.updateAriaChecked=function(e){this.adapter.setNativeControlAttr(Zi.ARIA_CHECKED_ATTR,""+!!e)},n}(ye); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class Ji extends Oe{constructor(){super(...arguments),this.checked=!1,this.disabled=!1,this.shouldRenderRipple=!1,this.mdcFoundationClass=Qi,this.rippleHandlers=new ui((()=>(this.shouldRenderRipple=!0,this.ripple)))}changeHandler(e){this.mdcFoundation.handleChange(e),this.checked=this.formElement.checked}createAdapter(){return Object.assign(Object.assign({},Ae(this.mdcRoot)),{setNativeControlChecked:e=>{this.formElement.checked=e},setNativeControlDisabled:e=>{this.formElement.disabled=e},setNativeControlAttr:(e,t)=>{this.formElement.setAttribute(e,t)}})}renderRipple(){return this.shouldRenderRipple?P` + + `:""}focus(){const e=this.formElement;e&&(this.rippleHandlers.startFocus(),e.focus())}blur(){const e=this.formElement;e&&(this.rippleHandlers.endFocus(),e.blur())}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(e=>{this.dispatchEvent(new Event("change",e))}))}render(){return P` +
+
+
+ ${this.renderRipple()} +
+ +
+
+
`}handleRippleMouseDown(e){const t=()=>{window.removeEventListener("mouseup",t),this.handleRippleDeactivate()};window.addEventListener("mouseup",t),this.rippleHandlers.startPress(e)}handleRippleTouchStart(e){this.rippleHandlers.startPress(e)}handleRippleDeactivate(){this.rippleHandlers.endPress()}handleRippleMouseEnter(){this.rippleHandlers.startHover()}handleRippleMouseLeave(){this.rippleHandlers.endHover()}handleRippleFocus(){this.rippleHandlers.startFocus()}handleRippleBlur(){this.rippleHandlers.endFocus()}}n([se({type:Boolean}),Me((function(e){this.mdcFoundation.setChecked(e)}))],Ji.prototype,"checked",void 0),n([se({type:Boolean}),Me((function(e){this.mdcFoundation.setDisabled(e)}))],Ji.prototype,"disabled",void 0),n([Yi,se({attribute:"aria-label"})],Ji.prototype,"ariaLabel",void 0),n([Yi,se({attribute:"aria-labelledby"})],Ji.prototype,"ariaLabelledBy",void 0),n([he(".mdc-switch")],Ji.prototype,"mdcRoot",void 0),n([he("input")],Ji.prototype,"formElement",void 0),n([ue("mwc-ripple")],Ji.prototype,"ripple",void 0),n([ce()],Ji.prototype,"shouldRenderRipple",void 0),n([pe({passive:!0})],Ji.prototype,"handleRippleMouseDown",null),n([pe({passive:!0})],Ji.prototype,"handleRippleTouchStart",null); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const en=c`.mdc-switch__thumb-underlay{left:-14px;right:initial;top:-17px;width:48px;height:48px}[dir=rtl] .mdc-switch__thumb-underlay,.mdc-switch__thumb-underlay[dir=rtl]{left:initial;right:-14px}.mdc-switch__native-control{width:64px;height:48px}.mdc-switch{display:inline-block;position:relative;outline:none;user-select:none}.mdc-switch.mdc-switch--checked .mdc-switch__track{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-switch.mdc-switch--checked .mdc-switch__thumb{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);border-color:#fff;border-color:var(--mdc-theme-surface, #fff)}.mdc-switch__native-control{left:0;right:initial;position:absolute;top:0;margin:0;opacity:0;cursor:pointer;pointer-events:auto;transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-switch__native-control,.mdc-switch__native-control[dir=rtl]{left:initial;right:0}.mdc-switch__track{box-sizing:border-box;width:36px;height:14px;border:1px solid transparent;border-radius:7px;opacity:.38;transition:opacity 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb-underlay{display:flex;position:absolute;align-items:center;justify-content:center;transform:translateX(0);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);box-sizing:border-box;width:20px;height:20px;border:10px solid;border-radius:50%;pointer-events:none;z-index:1}.mdc-switch--checked .mdc-switch__track{opacity:.54}.mdc-switch--checked .mdc-switch__thumb-underlay{transform:translateX(16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__thumb-underlay,.mdc-switch--checked .mdc-switch__thumb-underlay[dir=rtl]{transform:translateX(-16px)}.mdc-switch--checked .mdc-switch__native-control{transform:translateX(-16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__native-control,.mdc-switch--checked .mdc-switch__native-control[dir=rtl]{transform:translateX(16px)}.mdc-switch--disabled{opacity:.38;pointer-events:none}.mdc-switch--disabled .mdc-switch__thumb{border-width:1px}.mdc-switch--disabled .mdc-switch__native-control{cursor:default;pointer-events:none}:host{display:inline-flex;outline:none;-webkit-tap-highlight-color:transparent}`,tn={"mwc-switch":class extends Ji{static get styles(){return en}},"mwc-ripple":class extends $i{static get styles(){return Xi}}}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var nn={ARIA_CONTROLS:"aria-controls",ARIA_DESCRIBEDBY:"aria-describedby",INPUT_SELECTOR:".mdc-text-field__input",LABEL_SELECTOR:".mdc-floating-label",LEADING_ICON_SELECTOR:".mdc-text-field__icon--leading",LINE_RIPPLE_SELECTOR:".mdc-line-ripple",OUTLINE_SELECTOR:".mdc-notched-outline",PREFIX_SELECTOR:".mdc-text-field__affix--prefix",SUFFIX_SELECTOR:".mdc-text-field__affix--suffix",TRAILING_ICON_SELECTOR:".mdc-text-field__icon--trailing"},rn={DISABLED:"mdc-text-field--disabled",FOCUSED:"mdc-text-field--focused",HELPER_LINE:"mdc-text-field-helper-line",INVALID:"mdc-text-field--invalid",LABEL_FLOATING:"mdc-text-field--label-floating",NO_LABEL:"mdc-text-field--no-label",OUTLINED:"mdc-text-field--outlined",ROOT:"mdc-text-field",TEXTAREA:"mdc-text-field--textarea",WITH_LEADING_ICON:"mdc-text-field--with-leading-icon",WITH_TRAILING_ICON:"mdc-text-field--with-trailing-icon",WITH_INTERNAL_COUNTER:"mdc-text-field--with-internal-counter"},an={LABEL_SCALE:.75},on=["pattern","min","max","required","step","minlength","maxlength"],dn=["color","date","datetime-local","month","range","time","week"],ln=["mousedown","touchstart"],sn=["click","keydown"],cn=function(e){function n(t,r){void 0===r&&(r={});var a=e.call(this,i(i({},n.defaultAdapter),t))||this;return a.isFocused=!1,a.receivedUserInput=!1,a.valid=!0,a.useNativeValidation=!0,a.validateOnValueChange=!0,a.helperText=r.helperText,a.characterCounter=r.characterCounter,a.leadingIcon=r.leadingIcon,a.trailingIcon=r.trailingIcon,a.inputFocusHandler=function(){a.activateFocus()},a.inputBlurHandler=function(){a.deactivateFocus()},a.inputInputHandler=function(){a.handleInput()},a.setPointerXOffset=function(e){a.setTransformOrigin(e)},a.textFieldInteractionHandler=function(){a.handleTextFieldInteraction()},a.validationAttributeChangeHandler=function(e){a.handleValidationAttributeChange(e)},a}return t(n,e),Object.defineProperty(n,"cssClasses",{get:function(){return rn},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return nn},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return an},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldAlwaysFloat",{get:function(){var e=this.getNativeInput().type;return dn.indexOf(e)>=0},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldFloat",{get:function(){return this.shouldAlwaysFloat||this.isFocused||!!this.getValue()||this.isBadInput()},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldShake",{get:function(){return!this.isFocused&&!this.isValid()&&!!this.getValue()},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!0},setInputAttr:function(){},removeInputAttr:function(){},registerTextFieldInteractionHandler:function(){},deregisterTextFieldInteractionHandler:function(){},registerInputInteractionHandler:function(){},deregisterInputInteractionHandler:function(){},registerValidationAttributeChangeHandler:function(){return new MutationObserver((function(){}))},deregisterValidationAttributeChangeHandler:function(){},getNativeInput:function(){return null},isFocused:function(){return!1},activateLineRipple:function(){},deactivateLineRipple:function(){},setLineRippleTransformOrigin:function(){},shakeLabel:function(){},floatLabel:function(){},setLabelRequired:function(){},hasLabel:function(){return!1},getLabelWidth:function(){return 0},hasOutline:function(){return!1},notchOutline:function(){},closeOutline:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){var e,t,i,n;this.adapter.hasLabel()&&this.getNativeInput().required&&this.adapter.setLabelRequired(!0),this.adapter.isFocused()?this.inputFocusHandler():this.adapter.hasLabel()&&this.shouldFloat&&(this.notchOutline(!0),this.adapter.floatLabel(!0),this.styleFloating(!0)),this.adapter.registerInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.registerInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.registerInputInteractionHandler("input",this.inputInputHandler);try{for(var a=r(ln),o=a.next();!o.done;o=a.next()){var d=o.value;this.adapter.registerInputInteractionHandler(d,this.setPointerXOffset)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(t=a.return)&&t.call(a)}finally{if(e)throw e.error}}try{for(var l=r(sn),s=l.next();!s.done;s=l.next()){d=s.value;this.adapter.registerTextFieldInteractionHandler(d,this.textFieldInteractionHandler)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=l.return)&&n.call(l)}finally{if(i)throw i.error}}this.validationObserver=this.adapter.registerValidationAttributeChangeHandler(this.validationAttributeChangeHandler),this.setcharacterCounter(this.getValue().length)},n.prototype.destroy=function(){var e,t,i,n;this.adapter.deregisterInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.deregisterInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.deregisterInputInteractionHandler("input",this.inputInputHandler);try{for(var a=r(ln),o=a.next();!o.done;o=a.next()){var d=o.value;this.adapter.deregisterInputInteractionHandler(d,this.setPointerXOffset)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(t=a.return)&&t.call(a)}finally{if(e)throw e.error}}try{for(var l=r(sn),s=l.next();!s.done;s=l.next()){d=s.value;this.adapter.deregisterTextFieldInteractionHandler(d,this.textFieldInteractionHandler)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=l.return)&&n.call(l)}finally{if(i)throw i.error}}this.adapter.deregisterValidationAttributeChangeHandler(this.validationObserver)},n.prototype.handleTextFieldInteraction=function(){var e=this.adapter.getNativeInput();e&&e.disabled||(this.receivedUserInput=!0)},n.prototype.handleValidationAttributeChange=function(e){var t=this;e.some((function(e){return on.indexOf(e)>-1&&(t.styleValidity(!0),t.adapter.setLabelRequired(t.getNativeInput().required),!0)})),e.indexOf("maxlength")>-1&&this.setcharacterCounter(this.getValue().length)},n.prototype.notchOutline=function(e){if(this.adapter.hasOutline()&&this.adapter.hasLabel())if(e){var t=this.adapter.getLabelWidth()*an.LABEL_SCALE;this.adapter.notchOutline(t)}else this.adapter.closeOutline()},n.prototype.activateFocus=function(){this.isFocused=!0,this.styleFocused(this.isFocused),this.adapter.activateLineRipple(),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),!this.helperText||!this.helperText.isPersistent()&&this.helperText.isValidation()&&this.valid||this.helperText.showToScreenReader()},n.prototype.setTransformOrigin=function(e){if(!this.isDisabled()&&!this.adapter.hasOutline()){var t=e.touches,i=t?t[0]:e,n=i.target.getBoundingClientRect(),r=i.clientX-n.left;this.adapter.setLineRippleTransformOrigin(r)}},n.prototype.handleInput=function(){this.autoCompleteFocus(),this.setcharacterCounter(this.getValue().length)},n.prototype.autoCompleteFocus=function(){this.receivedUserInput||this.activateFocus()},n.prototype.deactivateFocus=function(){this.isFocused=!1,this.adapter.deactivateLineRipple();var e=this.isValid();this.styleValidity(e),this.styleFocused(this.isFocused),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),this.shouldFloat||(this.receivedUserInput=!1)},n.prototype.getValue=function(){return this.getNativeInput().value},n.prototype.setValue=function(e){if(this.getValue()!==e&&(this.getNativeInput().value=e),this.setcharacterCounter(e.length),this.validateOnValueChange){var t=this.isValid();this.styleValidity(t)}this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.validateOnValueChange&&this.adapter.shakeLabel(this.shouldShake))},n.prototype.isValid=function(){return this.useNativeValidation?this.isNativeInputValid():this.valid},n.prototype.setValid=function(e){this.valid=e,this.styleValidity(e);var t=!e&&!this.isFocused&&!!this.getValue();this.adapter.hasLabel()&&this.adapter.shakeLabel(t)},n.prototype.setValidateOnValueChange=function(e){this.validateOnValueChange=e},n.prototype.getValidateOnValueChange=function(){return this.validateOnValueChange},n.prototype.setUseNativeValidation=function(e){this.useNativeValidation=e},n.prototype.isDisabled=function(){return this.getNativeInput().disabled},n.prototype.setDisabled=function(e){this.getNativeInput().disabled=e,this.styleDisabled(e)},n.prototype.setHelperTextContent=function(e){this.helperText&&this.helperText.setContent(e)},n.prototype.setLeadingIconAriaLabel=function(e){this.leadingIcon&&this.leadingIcon.setAriaLabel(e)},n.prototype.setLeadingIconContent=function(e){this.leadingIcon&&this.leadingIcon.setContent(e)},n.prototype.setTrailingIconAriaLabel=function(e){this.trailingIcon&&this.trailingIcon.setAriaLabel(e)},n.prototype.setTrailingIconContent=function(e){this.trailingIcon&&this.trailingIcon.setContent(e)},n.prototype.setcharacterCounter=function(e){if(this.characterCounter){var t=this.getNativeInput().maxLength;if(-1===t)throw new Error("MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.");this.characterCounter.setCounterValue(e,t)}},n.prototype.isBadInput=function(){return this.getNativeInput().validity.badInput||!1},n.prototype.isNativeInputValid=function(){return this.getNativeInput().validity.valid},n.prototype.styleValidity=function(e){var t=n.cssClasses.INVALID;if(e?this.adapter.removeClass(t):this.adapter.addClass(t),this.helperText){if(this.helperText.setValidity(e),!this.helperText.isValidation())return;var i=this.helperText.isVisible(),r=this.helperText.getId();i&&r?this.adapter.setInputAttr(nn.ARIA_DESCRIBEDBY,r):this.adapter.removeInputAttr(nn.ARIA_DESCRIBEDBY)}},n.prototype.styleFocused=function(e){var t=n.cssClasses.FOCUSED;e?this.adapter.addClass(t):this.adapter.removeClass(t)},n.prototype.styleDisabled=function(e){var t=n.cssClasses,i=t.DISABLED,r=t.INVALID;e?(this.adapter.addClass(i),this.adapter.removeClass(r)):this.adapter.removeClass(i),this.leadingIcon&&this.leadingIcon.setDisabled(e),this.trailingIcon&&this.trailingIcon.setDisabled(e)},n.prototype.styleFloating=function(e){var t=n.cssClasses.LABEL_FLOATING;e?this.adapter.addClass(t):this.adapter.removeClass(t)},n.prototype.getNativeInput=function(){return(this.adapter?this.adapter.getNativeInput():null)||{disabled:!1,maxLength:-1,required:!1,type:"input",validity:{badInput:!1,valid:!0},value:""}},n}(ye),mn=cn; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const pn={},hn=Pe(class extends ze{constructor(e){if(super(e),e.type!==He&&e.type!==Ne&&e.type!==Be)throw Error("The `live` directive is not allowed on child or event bindings");if(!(e=>void 0===e.strings)(e))throw Error("`live` bindings can only contain a single expression")}render(e){return e}update(e,[t]){if(t===z||t===V)return t;const i=e.element,n=e.name;if(e.type===He){if(t===i[n])return z}else if(e.type===Be){if(!!t===i.hasAttribute(n))return z}else if(e.type===Ne&&i.getAttribute(n)===t+"")return z;return((e,t=pn)=>{e._$AH=t; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */})(e),t}}),un=["touchstart","touchmove","scroll","mousewheel"],fn=(e={})=>{const t={};for(const i in e)t[i]=e[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},t)};class gn extends $e{constructor(){super(...arguments),this.mdcFoundationClass=mn,this.value="",this.type="text",this.placeholder="",this.label="",this.icon="",this.iconTrailing="",this.disabled=!1,this.required=!1,this.minLength=-1,this.maxLength=-1,this.outlined=!1,this.helper="",this.validateOnInitialRender=!1,this.validationMessage="",this.autoValidate=!1,this.pattern="",this.min="",this.max="",this.step=null,this.size=null,this.helperPersistent=!1,this.charCounter=!1,this.endAligned=!1,this.prefix="",this.suffix="",this.name="",this.readOnly=!1,this.autocapitalize="",this.outlineOpen=!1,this.outlineWidth=0,this.isUiValid=!0,this.focused=!1,this._validity=fn(),this.validityTransform=null}get validity(){return this._checkValidity(this.value),this._validity}get willValidate(){return this.formElement.willValidate}get selectionStart(){return this.formElement.selectionStart}get selectionEnd(){return this.formElement.selectionEnd}focus(){const e=new CustomEvent("focus");this.formElement.dispatchEvent(e),this.formElement.focus()}blur(){const e=new CustomEvent("blur");this.formElement.dispatchEvent(e),this.formElement.blur()}select(){this.formElement.select()}setSelectionRange(e,t,i){this.formElement.setSelectionRange(e,t,i)}update(e){e.has("autoValidate")&&this.mdcFoundation&&this.mdcFoundation.setValidateOnValueChange(this.autoValidate),e.has("value")&&"string"!=typeof this.value&&(this.value=`${this.value}`),super.update(e)}setFormData(e){this.name&&e.append(this.name,this.value)}render(){const e=this.charCounter&&-1!==this.maxLength,t=!!this.helper||!!this.validationMessage||e,i={"mdc-text-field--disabled":this.disabled,"mdc-text-field--no-label":!this.label,"mdc-text-field--filled":!this.outlined,"mdc-text-field--outlined":this.outlined,"mdc-text-field--with-leading-icon":this.icon,"mdc-text-field--with-trailing-icon":this.iconTrailing,"mdc-text-field--end-aligned":this.endAligned};return P` + + ${this.renderHelperText(t,e)} + `}updated(e){e.has("value")&&void 0!==e.get("value")&&(this.mdcFoundation.setValue(this.value),this.autoValidate&&this.reportValidity())}renderRipple(){return this.outlined?"":P` + + `}renderOutline(){return this.outlined?P` + + ${this.renderLabel()} + `:""}renderLabel(){return this.label?P` + ${this.label} + `:""}renderLeadingIcon(){return this.icon?this.renderIcon(this.icon):""}renderTrailingIcon(){return this.iconTrailing?this.renderIcon(this.iconTrailing,!0):""}renderIcon(e,t=!1){return P`${e}`}renderPrefix(){return this.prefix?this.renderAffix(this.prefix):""}renderSuffix(){return this.suffix?this.renderAffix(this.suffix,!0):""}renderAffix(e,t=!1){return P` + ${e}`}renderInput(e){const t=-1===this.minLength?void 0:this.minLength,i=-1===this.maxLength?void 0:this.maxLength,n=this.autocapitalize?this.autocapitalize:void 0,r=this.validationMessage&&!this.isUiValid,a=this.label?"label":void 0,o=e?"helper-text":void 0,d=this.focused||this.helperPersistent||r?"helper-text":void 0;return P` + `}renderLineRipple(){return this.outlined?"":P` + + `}renderHelperText(e,t){const i=this.validationMessage&&!this.isUiValid,n={"mdc-text-field-helper-text--persistent":this.helperPersistent,"mdc-text-field-helper-text--validation-msg":i},r=this.focused||this.helperPersistent||i?void 0:"true",a=i?this.validationMessage:this.helper;return e?P` +
+
${a}
+ ${this.renderCharCounter(t)} +
`:""}renderCharCounter(e){const t=Math.min(this.value.length,this.maxLength);return e?P` + ${t} / ${this.maxLength}`:""}onInputFocus(){this.focused=!0}onInputBlur(){this.focused=!1,this.reportValidity()}checkValidity(){const e=this._checkValidity(this.value);if(!e){const e=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(e)}return e}reportValidity(){const e=this.checkValidity();return this.mdcFoundation.setValid(e),this.isUiValid=e,e}_checkValidity(e){const t=this.formElement.validity;let i=fn(t);if(this.validityTransform){const t=this.validityTransform(e,i);i=Object.assign(Object.assign({},i),t),this.mdcFoundation.setUseNativeValidation(!1)}else this.mdcFoundation.setUseNativeValidation(!0);return this._validity=i,this._validity.valid}setCustomValidity(e){this.validationMessage=e,this.formElement.setCustomValidity(e)}handleInputChange(){this.value=this.formElement.value}createAdapter(){return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},this.getRootAdapterMethods()),this.getInputAdapterMethods()),this.getLabelAdapterMethods()),this.getLineRippleAdapterMethods()),this.getOutlineAdapterMethods())}getRootAdapterMethods(){return Object.assign({registerTextFieldInteractionHandler:(e,t)=>this.addEventListener(e,t),deregisterTextFieldInteractionHandler:(e,t)=>this.removeEventListener(e,t),registerValidationAttributeChangeHandler:e=>{const t=new MutationObserver((t=>{e((e=>e.map((e=>e.attributeName)).filter((e=>e)))(t))}));return t.observe(this.formElement,{attributes:!0}),t},deregisterValidationAttributeChangeHandler:e=>e.disconnect()},Ae(this.mdcRoot))}getInputAdapterMethods(){return{getNativeInput:()=>this.formElement,setInputAttr:()=>{},removeInputAttr:()=>{},isFocused:()=>!!this.shadowRoot&&this.shadowRoot.activeElement===this.formElement,registerInputInteractionHandler:(e,t)=>this.formElement.addEventListener(e,t,{passive:e in un}),deregisterInputInteractionHandler:(e,t)=>this.formElement.removeEventListener(e,t)}}getLabelAdapterMethods(){return{floatLabel:e=>this.labelElement&&this.labelElement.floatingLabelFoundation.float(e),getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,hasLabel:()=>Boolean(this.labelElement),shakeLabel:e=>this.labelElement&&this.labelElement.floatingLabelFoundation.shake(e),setLabelRequired:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(e)}}}getLineRippleAdapterMethods(){return{activateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},setLineRippleTransformOrigin:e=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.setRippleCenter(e)}}}async getUpdateComplete(){var e;const t=await super.getUpdateComplete();return await(null===(e=this.outlineElement)||void 0===e?void 0:e.updateComplete),t}firstUpdated(){var e;super.firstUpdated(),this.mdcFoundation.setValidateOnValueChange(this.autoValidate),this.validateOnInitialRender&&this.reportValidity(),null===(e=this.outlineElement)||void 0===e||e.updateComplete.then((()=>{var e;this.outlineWidth=(null===(e=this.labelElement)||void 0===e?void 0:e.floatingLabelFoundation.getWidth())||0}))}getOutlineAdapterMethods(){return{closeOutline:()=>this.outlineElement&&(this.outlineOpen=!1),hasOutline:()=>Boolean(this.outlineElement),notchOutline:e=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=e,this.outlineOpen=!0)}}}async layout(){await this.updateComplete;const e=this.labelElement;if(!e)return void(this.outlineOpen=!1);const t=!!this.label&&!!this.value;if(e.floatingLabelFoundation.float(t),!this.outlined)return;this.outlineOpen=t,await this.updateComplete;const i=e.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=i,await this.updateComplete)}}n([he(".mdc-text-field")],gn.prototype,"mdcRoot",void 0),n([he("input")],gn.prototype,"formElement",void 0),n([he(".mdc-floating-label")],gn.prototype,"labelElement",void 0),n([he(".mdc-line-ripple")],gn.prototype,"lineRippleElement",void 0),n([he("mwc-notched-outline")],gn.prototype,"outlineElement",void 0),n([he(".mdc-notched-outline__notch")],gn.prototype,"notchElement",void 0),n([se({type:String})],gn.prototype,"value",void 0),n([se({type:String})],gn.prototype,"type",void 0),n([se({type:String})],gn.prototype,"placeholder",void 0),n([se({type:String}),Me((function(e,t){void 0!==t&&this.label!==t&&this.layout()}))],gn.prototype,"label",void 0),n([se({type:String})],gn.prototype,"icon",void 0),n([se({type:String})],gn.prototype,"iconTrailing",void 0),n([se({type:Boolean,reflect:!0})],gn.prototype,"disabled",void 0),n([se({type:Boolean})],gn.prototype,"required",void 0),n([se({type:Number})],gn.prototype,"minLength",void 0),n([se({type:Number})],gn.prototype,"maxLength",void 0),n([se({type:Boolean,reflect:!0}),Me((function(e,t){void 0!==t&&this.outlined!==t&&this.layout()}))],gn.prototype,"outlined",void 0),n([se({type:String})],gn.prototype,"helper",void 0),n([se({type:Boolean})],gn.prototype,"validateOnInitialRender",void 0),n([se({type:String})],gn.prototype,"validationMessage",void 0),n([se({type:Boolean})],gn.prototype,"autoValidate",void 0),n([se({type:String})],gn.prototype,"pattern",void 0),n([se({type:String})],gn.prototype,"min",void 0),n([se({type:String})],gn.prototype,"max",void 0),n([se({type:String})],gn.prototype,"step",void 0),n([se({type:Number})],gn.prototype,"size",void 0),n([se({type:Boolean})],gn.prototype,"helperPersistent",void 0),n([se({type:Boolean})],gn.prototype,"charCounter",void 0),n([se({type:Boolean})],gn.prototype,"endAligned",void 0),n([se({type:String})],gn.prototype,"prefix",void 0),n([se({type:String})],gn.prototype,"suffix",void 0),n([se({type:String})],gn.prototype,"name",void 0),n([se({type:String})],gn.prototype,"inputMode",void 0),n([se({type:Boolean})],gn.prototype,"readOnly",void 0),n([se({type:String})],gn.prototype,"autocapitalize",void 0),n([ce()],gn.prototype,"outlineOpen",void 0),n([ce()],gn.prototype,"outlineWidth",void 0),n([ce()],gn.prototype,"isUiValid",void 0),n([ce()],gn.prototype,"focused",void 0),n([pe({passive:!0})],gn.prototype,"handleInputChange",null); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const bn=c`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px;z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-text-field--filled{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-text-field--filled .mdc-text-field__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-text-field--filled .mdc-text-field__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-text-field--filled.mdc-ripple-upgraded--unbounded .mdc-text-field__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-activation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-deactivation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-text-field__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-text-field{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0;display:inline-flex;align-items:baseline;padding:0 16px;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input{color:rgba(0, 0, 0, 0.87)}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.54)}}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.54)}}.mdc-text-field .mdc-text-field__input{caret-color:#6200ee;caret-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field-character-counter,.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-text-field__input{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);width:100%;min-width:0;border:none;border-radius:0;background:none;appearance:none;padding:0}.mdc-text-field__input::-ms-clear{display:none}.mdc-text-field__input::-webkit-calendar-picker-indicator{display:none}.mdc-text-field__input:focus{outline:none}.mdc-text-field__input:invalid{box-shadow:none}@media all{.mdc-text-field__input::placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mdc-text-field--focused .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}@media all{.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}.mdc-text-field__affix{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0;white-space:nowrap}.mdc-text-field--label-floating .mdc-text-field__affix,.mdc-text-field--no-label .mdc-text-field__affix{opacity:1}@supports(-webkit-hyphens: none){.mdc-text-field--outlined .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field__affix--prefix,.mdc-text-field__affix--prefix[dir=rtl]{padding-left:2px;padding-right:0}.mdc-text-field--end-aligned .mdc-text-field__affix--prefix{padding-left:0;padding-right:12px}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--end-aligned .mdc-text-field__affix--prefix[dir=rtl]{padding-left:12px;padding-right:0}.mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field__affix--suffix,.mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:12px}.mdc-text-field--end-aligned .mdc-text-field__affix--suffix{padding-left:2px;padding-right:0}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--end-aligned .mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:2px}.mdc-text-field--filled{height:56px}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-text-field--filled:hover .mdc-text-field__ripple::before,.mdc-text-field--filled.mdc-ripple-surface--hover .mdc-text-field__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-text-field--filled.mdc-ripple-upgraded--background-focused .mdc-text-field__ripple::before,.mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-text-field--filled::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-text-field--filled:not(.mdc-text-field--disabled){background-color:whitesmoke}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-text-field--filled:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--filled .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-text-field--filled .mdc-floating-label,.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{height:100%}.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label{display:none}.mdc-text-field--filled.mdc-text-field--no-label::before{display:none}@supports(-webkit-hyphens: none){.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field--outlined{height:56px;overflow:visible}.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--outlined .mdc-text-field__input{height:100%}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-text-field--outlined{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined{padding-right:max(16px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-right:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-left:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-right:max(16px, var(--mdc-shape-small, 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-right:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-right:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-text-field--outlined .mdc-text-field__ripple::before,.mdc-text-field--outlined .mdc-text-field__ripple::after{content:none}.mdc-text-field--outlined .mdc-floating-label{left:4px;right:initial}[dir=rtl] .mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-text-field--outlined .mdc-text-field__input{display:flex;border:none !important;background-color:transparent}.mdc-text-field--outlined .mdc-notched-outline{z-index:1}.mdc-text-field--textarea{flex-direction:column;align-items:center;width:auto;height:auto;padding:0;transition:none}.mdc-text-field--textarea .mdc-floating-label{top:19px}.mdc-text-field--textarea .mdc-floating-label:not(.mdc-floating-label--float-above){transform:none}.mdc-text-field--textarea .mdc-text-field__input{flex-grow:1;height:auto;min-height:1.5rem;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;resize:none;padding:0 16px;line-height:1.5rem}.mdc-text-field--textarea.mdc-text-field--filled::before{display:none}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-10.25px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-filled 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-filled{0%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-10.25px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-10.25px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--filled .mdc-text-field__input{margin-top:23px;margin-bottom:9px}.mdc-text-field--textarea.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-27.25px) scale(1)}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-24.75px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-24.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-24.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label{top:18px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field__input{margin-bottom:2px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter{align-self:flex-end;padding:0 16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::after{display:inline-block;width:0;height:16px;content:"";vertical-align:-16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::before{display:none}.mdc-text-field__resizer{align-self:stretch;display:inline-flex;flex-direction:column;flex-grow:1;max-height:100%;max-width:100%;min-height:56px;min-width:fit-content;min-width:-moz-available;min-width:-webkit-fill-available;overflow:hidden;resize:both}.mdc-text-field--filled .mdc-text-field__resizer{transform:translateY(-1px)}.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateY(1px)}.mdc-text-field--outlined .mdc-text-field__resizer{transform:translateX(-1px) translateY(-1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer,.mdc-text-field--outlined .mdc-text-field__resizer[dir=rtl]{transform:translateX(1px) translateY(-1px)}.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateX(1px) translateY(1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input[dir=rtl],.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter[dir=rtl]{transform:translateX(-1px) translateY(1px)}.mdc-text-field--with-leading-icon{padding-left:0;padding-right:16px}[dir=rtl] .mdc-text-field--with-leading-icon,.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:16px;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 48px);left:48px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-text-field--with-leading-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake,.mdc-text-field--with-leading-icon.mdc-text-field--outlined[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--with-trailing-icon{padding-left:16px;padding-right:0}[dir=rtl] .mdc-text-field--with-trailing-icon,.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-trailing-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-text-field-helper-line{display:flex;justify-content:space-between;box-sizing:border-box}.mdc-text-field+.mdc-text-field-helper-line{padding-right:16px;padding-left:16px}.mdc-form-field>.mdc-text-field+label{align-self:flex-start}.mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--focused .mdc-notched-outline__trailing{border-width:2px}.mdc-text-field--focused+.mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg){opacity:1}.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-text-field--focused.mdc-text-field--outlined.mdc-text-field--textarea .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid .mdc-text-field__input{caret-color:#b00020;caret-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{opacity:1}.mdc-text-field--disabled{pointer-events:none}.mdc-text-field--disabled .mdc-text-field__input{color:rgba(0, 0, 0, 0.38)}@media all{.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.38)}}@media all{.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.38)}}.mdc-text-field--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-floating-label{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--leading{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:GrayText}}@media screen and (forced-colors: active){.mdc-text-field--disabled .mdc-text-field__input{background-color:Window}.mdc-text-field--disabled .mdc-floating-label{z-index:1}}.mdc-text-field--disabled .mdc-floating-label{cursor:default}.mdc-text-field--disabled.mdc-text-field--filled{background-color:#fafafa}.mdc-text-field--disabled.mdc-text-field--filled .mdc-text-field__ripple{display:none}.mdc-text-field--disabled .mdc-text-field__input{pointer-events:auto}.mdc-text-field--end-aligned .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--end-aligned .mdc-text-field__input[dir=rtl]{text-align:left}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix{direction:ltr}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--leading,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--leading{order:1}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{order:2}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input{order:3}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{order:4}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--trailing,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--trailing{order:5}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--prefix{padding-right:12px}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--suffix{padding-left:2px}.mdc-text-field-helper-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;opacity:0;will-change:opacity;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-text-field-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-text-field-helper-text--persistent{transition:none;opacity:1;will-change:initial}.mdc-text-field-character-counter{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-left:auto;margin-right:0;padding-left:16px;padding-right:0;white-space:nowrap}.mdc-text-field-character-counter::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field__icon{align-self:center;cursor:pointer}.mdc-text-field__icon:not([tabindex]),.mdc-text-field__icon[tabindex="-1"]{cursor:default;pointer-events:none}.mdc-text-field__icon svg{display:block}.mdc-text-field__icon--leading{margin-left:16px;margin-right:8px}[dir=rtl] .mdc-text-field__icon--leading,.mdc-text-field__icon--leading[dir=rtl]{margin-left:8px;margin-right:16px}.mdc-text-field__icon--trailing{padding:12px;margin-left:0px;margin-right:0px}[dir=rtl] .mdc-text-field__icon--trailing,.mdc-text-field__icon--trailing[dir=rtl]{margin-left:0px;margin-right:0px}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-flex;flex-direction:column;outline:none}.mdc-text-field{width:100%}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42))}.mdc-text-field:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-text-field-hover-line-color, rgba(0, 0, 0, 0.87))}.mdc-text-field.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06);border-bottom-color:var(--mdc-text-field-disabled-line-color, rgba(0, 0, 0, 0.06))}.mdc-text-field.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field__input{direction:inherit}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-idle-border-color, rgba(0, 0, 0, 0.38) )}:host(:not([disabled]):hover) :not(.mdc-text-field--invalid):not(.mdc-text-field--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-error-color, var(--mdc-theme-error, #b00020) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-character-counter,:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid .mdc-text-field__icon{color:var(--mdc-text-field-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input{color:var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg),:host(:not([disabled])) .mdc-text-field-helper-line:not(.mdc-text-field--invalid) .mdc-text-field-character-counter{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-text-field.mdc-text-field--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field .mdc-text-field__input,:host([disabled]) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-helper-text,:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-character-counter{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}`,vn={"mwc-textfield":class extends gn{static get styles(){return bn}},"mwc-notched-outline":class extends Pi{static get styles(){return Gi}}}; +/** + * @license + * Copyright 2020 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var xn,_n;!function(e){e[e.ACTIVE=0]="ACTIVE",e[e.INACTIVE=1]="INACTIVE"}(xn||(xn={})),function(e){e[e.START=1]="START",e[e.END=2]="END"}(_n||(_n={})); +/** + * @license + * Copyright 2020 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var yn=function(){function e(){this.rafIDs=new Map}return e.prototype.request=function(e,t){var i=this;this.cancel(e);var n=requestAnimationFrame((function(n){i.rafIDs.delete(e),t(n)}));this.rafIDs.set(e,n)},e.prototype.cancel=function(e){var t=this.rafIDs.get(e);t&&(cancelAnimationFrame(t),this.rafIDs.delete(e))},e.prototype.cancelAll=function(){var e=this;this.rafIDs.forEach((function(t,i){e.cancel(i)}))},e.prototype.getQueue=function(){var e=[];return this.rafIDs.forEach((function(t,i){e.push(i)})),e},e}(),wn={animation:{prefixed:"-webkit-animation",standard:"animation"},transform:{prefixed:"-webkit-transform",standard:"transform"},transition:{prefixed:"-webkit-transition",standard:"transition"}}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */function En(e,t){if(function(e){return Boolean(e.document)&&"function"==typeof e.document.createElement}(e)&&t in wn){var i=e.document.createElement("div"),n=wn[t],r=n.standard,a=n.prefixed;return r in i.style?r:a}return t} +/** + * @license + * Copyright 2020 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var Tn,In="mdc-slider--disabled",An="mdc-slider--discrete",Cn="mdc-slider--range",Sn="mdc-slider__thumb--focused",Rn="mdc-slider__thumb--top",kn="mdc-slider__thumb--with-indicator",On="mdc-slider--tick-marks",Ln=1,Fn=5,Dn="aria-valuetext",$n="disabled",Mn="min",Nn="max",Hn="value",Bn="step";!function(e){e.SLIDER_UPDATE="slider_update"}(Tn||(Tn={}));var Pn="undefined"!=typeof window,zn=function(e){function n(t){var r=e.call(this,i(i({},n.defaultAdapter),t))||this;return r.initialStylesRemoved=!1,r.isDisabled=!1,r.isDiscrete=!1,r.step=Ln,r.hasTickMarks=!1,r.isRange=!1,r.thumb=null,r.downEventClientX=null,r.startThumbKnobWidth=0,r.endThumbKnobWidth=0,r.animFrame=new yn,r}return t(n,e),Object.defineProperty(n,"defaultAdapter",{get:function(){return{hasClass:function(){return!1},addClass:function(){},removeClass:function(){},addThumbClass:function(){},removeThumbClass:function(){},getAttribute:function(){return null},getInputValue:function(){return""},setInputValue:function(){},getInputAttribute:function(){return null},setInputAttribute:function(){return null},removeInputAttribute:function(){return null},focusInput:function(){},isInputFocused:function(){return!1},getThumbKnobWidth:function(){return 0},getThumbBoundingClientRect:function(){return{top:0,right:0,bottom:0,left:0,width:0,height:0}},getBoundingClientRect:function(){return{top:0,right:0,bottom:0,left:0,width:0,height:0}},isRTL:function(){return!1},setThumbStyleProperty:function(){},removeThumbStyleProperty:function(){},setTrackActiveStyleProperty:function(){},removeTrackActiveStyleProperty:function(){},setValueIndicatorText:function(){},getValueToAriaValueTextFn:function(){return null},updateTickMarks:function(){},setPointerCapture:function(){},emitChangeEvent:function(){},emitInputEvent:function(){},emitDragStartEvent:function(){},emitDragEndEvent:function(){},registerEventHandler:function(){},deregisterEventHandler:function(){},registerThumbEventHandler:function(){},deregisterThumbEventHandler:function(){},registerInputEventHandler:function(){},deregisterInputEventHandler:function(){},registerBodyEventHandler:function(){},deregisterBodyEventHandler:function(){},registerWindowEventHandler:function(){},deregisterWindowEventHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){var e=this;this.isDisabled=this.adapter.hasClass(In),this.isDiscrete=this.adapter.hasClass(An),this.hasTickMarks=this.adapter.hasClass(On),this.isRange=this.adapter.hasClass(Cn);var t=this.convertAttributeValueToNumber(this.adapter.getInputAttribute(Mn,this.isRange?_n.START:_n.END),Mn),i=this.convertAttributeValueToNumber(this.adapter.getInputAttribute(Nn,_n.END),Nn),n=this.convertAttributeValueToNumber(this.adapter.getInputAttribute(Hn,_n.END),Hn),r=this.isRange?this.convertAttributeValueToNumber(this.adapter.getInputAttribute(Hn,_n.START),Hn):t,a=this.adapter.getInputAttribute(Bn,_n.END),o=a?this.convertAttributeValueToNumber(a,Bn):this.step;this.validateProperties({min:t,max:i,value:n,valueStart:r,step:o}),this.min=t,this.max=i,this.value=n,this.valueStart=r,this.step=o,this.numDecimalPlaces=Vn(this.step),this.valueBeforeDownEvent=n,this.valueStartBeforeDownEvent=r,this.mousedownOrTouchstartListener=this.handleMousedownOrTouchstart.bind(this),this.moveListener=this.handleMove.bind(this),this.pointerdownListener=this.handlePointerdown.bind(this),this.pointerupListener=this.handlePointerup.bind(this),this.thumbMouseenterListener=this.handleThumbMouseenter.bind(this),this.thumbMouseleaveListener=this.handleThumbMouseleave.bind(this),this.inputStartChangeListener=function(){e.handleInputChange(_n.START)},this.inputEndChangeListener=function(){e.handleInputChange(_n.END)},this.inputStartFocusListener=function(){e.handleInputFocus(_n.START)},this.inputEndFocusListener=function(){e.handleInputFocus(_n.END)},this.inputStartBlurListener=function(){e.handleInputBlur(_n.START)},this.inputEndBlurListener=function(){e.handleInputBlur(_n.END)},this.resizeListener=this.handleResize.bind(this),this.registerEventHandlers()},n.prototype.destroy=function(){this.deregisterEventHandlers()},n.prototype.setMin=function(e){this.min=e,this.isRange||(this.valueStart=e),this.updateUI()},n.prototype.setMax=function(e){this.max=e,this.updateUI()},n.prototype.getMin=function(){return this.min},n.prototype.getMax=function(){return this.max},n.prototype.getValue=function(){return this.value},n.prototype.setValue=function(e){if(this.isRange&&e= start thumb value ("+this.valueStart+")");this.updateValue(e,_n.END)},n.prototype.getValueStart=function(){if(!this.isRange)throw new Error("`valueStart` is only applicable for range sliders.");return this.valueStart},n.prototype.setValueStart=function(e){if(!this.isRange)throw new Error("`valueStart` is only applicable for range sliders.");if(this.isRange&&e>this.value)throw new Error("start thumb value ("+e+") must be <= end thumb value ("+this.value+")");this.updateValue(e,_n.START)},n.prototype.setStep=function(e){this.step=e,this.numDecimalPlaces=Vn(e),this.updateUI()},n.prototype.setIsDiscrete=function(e){this.isDiscrete=e,this.updateValueIndicatorUI(),this.updateTickMarksUI()},n.prototype.getStep=function(){return this.step},n.prototype.setHasTickMarks=function(e){this.hasTickMarks=e,this.updateTickMarksUI()},n.prototype.getDisabled=function(){return this.isDisabled},n.prototype.setDisabled=function(e){this.isDisabled=e,e?(this.adapter.addClass(In),this.isRange&&this.adapter.setInputAttribute($n,"",_n.START),this.adapter.setInputAttribute($n,"",_n.END)):(this.adapter.removeClass(In),this.isRange&&this.adapter.removeInputAttribute($n,_n.START),this.adapter.removeInputAttribute($n,_n.END))},n.prototype.getIsRange=function(){return this.isRange},n.prototype.layout=function(e){var t=(void 0===e?{}:e).skipUpdateUI;this.rect=this.adapter.getBoundingClientRect(),this.isRange&&(this.startThumbKnobWidth=this.adapter.getThumbKnobWidth(_n.START),this.endThumbKnobWidth=this.adapter.getThumbKnobWidth(_n.END)),t||this.updateUI()},n.prototype.handleResize=function(){this.layout()},n.prototype.handleDown=function(e){if(!this.isDisabled){this.valueStartBeforeDownEvent=this.valueStart,this.valueBeforeDownEvent=this.value;var t=null!=e.clientX?e.clientX:e.targetTouches[0].clientX;this.downEventClientX=t;var i=this.mapClientXOnSliderScale(t);this.thumb=this.getThumbFromDownEvent(t,i),null!==this.thumb&&(this.handleDragStart(e,i,this.thumb),this.updateValue(i,this.thumb,{emitInputEvent:!0}))}},n.prototype.handleMove=function(e){if(!this.isDisabled){e.preventDefault();var t=null!=e.clientX?e.clientX:e.targetTouches[0].clientX,i=null!=this.thumb;if(this.thumb=this.getThumbFromMoveEvent(t),null!==this.thumb){var n=this.mapClientXOnSliderScale(t);i||(this.handleDragStart(e,n,this.thumb),this.adapter.emitDragStartEvent(n,this.thumb)),this.updateValue(n,this.thumb,{emitInputEvent:!0})}}},n.prototype.handleUp=function(){if(!this.isDisabled&&null!==this.thumb){var e=this.thumb===_n.START?this.valueStartBeforeDownEvent:this.valueBeforeDownEvent,t=this.thumb===_n.START?this.valueStart:this.value;e!==t&&this.adapter.emitChangeEvent(t,this.thumb),this.adapter.emitDragEndEvent(t,this.thumb),this.thumb=null}},n.prototype.handleThumbMouseenter=function(){this.isDiscrete&&this.isRange&&(this.adapter.addThumbClass(kn,_n.START),this.adapter.addThumbClass(kn,_n.END))},n.prototype.handleThumbMouseleave=function(){this.isDiscrete&&this.isRange&&(this.adapter.isInputFocused(_n.START)||this.adapter.isInputFocused(_n.END)||(this.adapter.removeThumbClass(kn,_n.START),this.adapter.removeThumbClass(kn,_n.END)))},n.prototype.handleMousedownOrTouchstart=function(e){var t=this,i="mousedown"===e.type?"mousemove":"touchmove";this.adapter.registerBodyEventHandler(i,this.moveListener);var n=function(){t.handleUp(),t.adapter.deregisterBodyEventHandler(i,t.moveListener),t.adapter.deregisterEventHandler("mouseup",n),t.adapter.deregisterEventHandler("touchend",n)};this.adapter.registerBodyEventHandler("mouseup",n),this.adapter.registerBodyEventHandler("touchend",n),this.handleDown(e)},n.prototype.handlePointerdown=function(e){this.adapter.setPointerCapture(e.pointerId),this.adapter.registerEventHandler("pointermove",this.moveListener),this.handleDown(e)},n.prototype.handleInputChange=function(e){var t=Number(this.adapter.getInputValue(e));e===_n.START?this.setValueStart(t):this.setValue(t),this.adapter.emitChangeEvent(e===_n.START?this.valueStart:this.value,e),this.adapter.emitInputEvent(e===_n.START?this.valueStart:this.value,e)},n.prototype.handleInputFocus=function(e){if(this.adapter.addThumbClass(Sn,e),this.isDiscrete&&(this.adapter.addThumbClass(kn,e),this.isRange)){var t=e===_n.START?_n.END:_n.START;this.adapter.addThumbClass(kn,t)}},n.prototype.handleInputBlur=function(e){if(this.adapter.removeThumbClass(Sn,e),this.isDiscrete&&(this.adapter.removeThumbClass(kn,e),this.isRange)){var t=e===_n.START?_n.END:_n.START;this.adapter.removeThumbClass(kn,t)}},n.prototype.handleDragStart=function(e,t,i){this.adapter.emitDragStartEvent(t,i),this.adapter.focusInput(i),e.preventDefault()},n.prototype.getThumbFromDownEvent=function(e,t){if(!this.isRange)return _n.END;var i=this.adapter.getThumbBoundingClientRect(_n.START),n=this.adapter.getThumbBoundingClientRect(_n.END),r=e>=i.left&&e<=i.right,a=e>=n.left&&e<=n.right;return r&&a?null:r?_n.START:a?_n.END:tthis.value?_n.END:t-this.valueStart<=this.value-t?_n.START:_n.END},n.prototype.getThumbFromMoveEvent=function(e){if(null!==this.thumb)return this.thumb;if(null===this.downEventClientX)throw new Error("`downEventClientX` is null after move event.");return Math.abs(this.downEventClientX-e)this.value?this.value:this.isRange&&t===_n.END&&e=r}else{n=e+this.startThumbKnobWidth/2>=t-this.endThumbKnobWidth/2}n?(this.adapter.addThumbClass(Rn,i||_n.END),this.adapter.removeThumbClass(Rn,i===_n.START?_n.END:_n.START)):(this.adapter.removeThumbClass(Rn,_n.START),this.adapter.removeThumbClass(Rn,_n.END))},n.prototype.convertAttributeValueToNumber=function(e,t){if(null===e)throw new Error("MDCSliderFoundation: `"+t+"` must be non-null.");var i=Number(e);if(isNaN(i))throw new Error("MDCSliderFoundation: `"+t+"` value is `"+e+"`, but must be a number.");return i},n.prototype.validateProperties=function(e){var t=e.min,i=e.max,n=e.value,r=e.valueStart,a=e.step;if(t>=i)throw new Error("MDCSliderFoundation: min must be strictly less than max. Current: [min: "+t+", max: "+i+"]");if(a<=0)throw new Error("MDCSliderFoundation: step must be a positive number. Current step: "+this.step);if(this.isRange){if(ni||ri)throw new Error("MDCSliderFoundation: values must be in [min, max] range. Current values: [start value: "+r+", end value: "+n+"]");if(r>n)throw new Error("MDCSliderFoundation: start value must be <= end value. Current values: [start value: "+r+", end value: "+n+"]");var o=(n-t)/a;if((r-t)/a%1!=0||o%1!=0)throw new Error("MDCSliderFoundation: Slider values must be valid based on the step value. Current values: [start value: "+r+", end value: "+n+"]")}else{if(ni)throw new Error("MDCSliderFoundation: value must be in [min, max] range. Current value: "+n);if((o=(n-t)/a)%1!=0)throw new Error("MDCSliderFoundation: Slider value must be valid based on the step value. Current value: "+n)}},n.prototype.registerEventHandlers=function(){this.adapter.registerWindowEventHandler("resize",this.resizeListener),n.SUPPORTS_POINTER_EVENTS?(this.adapter.registerEventHandler("pointerdown",this.pointerdownListener),this.adapter.registerEventHandler("pointerup",this.pointerupListener)):(this.adapter.registerEventHandler("mousedown",this.mousedownOrTouchstartListener),this.adapter.registerEventHandler("touchstart",this.mousedownOrTouchstartListener)),this.isRange&&(this.adapter.registerThumbEventHandler(_n.START,"mouseenter",this.thumbMouseenterListener),this.adapter.registerThumbEventHandler(_n.START,"mouseleave",this.thumbMouseleaveListener),this.adapter.registerInputEventHandler(_n.START,"change",this.inputStartChangeListener),this.adapter.registerInputEventHandler(_n.START,"focus",this.inputStartFocusListener),this.adapter.registerInputEventHandler(_n.START,"blur",this.inputStartBlurListener)),this.adapter.registerThumbEventHandler(_n.END,"mouseenter",this.thumbMouseenterListener),this.adapter.registerThumbEventHandler(_n.END,"mouseleave",this.thumbMouseleaveListener),this.adapter.registerInputEventHandler(_n.END,"change",this.inputEndChangeListener),this.adapter.registerInputEventHandler(_n.END,"focus",this.inputEndFocusListener),this.adapter.registerInputEventHandler(_n.END,"blur",this.inputEndBlurListener)},n.prototype.deregisterEventHandlers=function(){this.adapter.deregisterWindowEventHandler("resize",this.resizeListener),n.SUPPORTS_POINTER_EVENTS?(this.adapter.deregisterEventHandler("pointerdown",this.pointerdownListener),this.adapter.deregisterEventHandler("pointerup",this.pointerupListener)):(this.adapter.deregisterEventHandler("mousedown",this.mousedownOrTouchstartListener),this.adapter.deregisterEventHandler("touchstart",this.mousedownOrTouchstartListener)),this.isRange&&(this.adapter.deregisterThumbEventHandler(_n.START,"mouseenter",this.thumbMouseenterListener),this.adapter.deregisterThumbEventHandler(_n.START,"mouseleave",this.thumbMouseleaveListener),this.adapter.deregisterInputEventHandler(_n.START,"change",this.inputStartChangeListener),this.adapter.deregisterInputEventHandler(_n.START,"focus",this.inputStartFocusListener),this.adapter.deregisterInputEventHandler(_n.START,"blur",this.inputStartBlurListener)),this.adapter.deregisterThumbEventHandler(_n.END,"mouseenter",this.thumbMouseenterListener),this.adapter.deregisterThumbEventHandler(_n.END,"mouseleave",this.thumbMouseleaveListener),this.adapter.deregisterInputEventHandler(_n.END,"change",this.inputEndChangeListener),this.adapter.deregisterInputEventHandler(_n.END,"focus",this.inputEndFocusListener),this.adapter.deregisterInputEventHandler(_n.END,"blur",this.inputEndBlurListener)},n.prototype.handlePointerup=function(){this.handleUp(),this.adapter.deregisterEventHandler("pointermove",this.moveListener)},n.SUPPORTS_POINTER_EVENTS=Pn&&Boolean(window.PointerEvent)&&!(["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&"ontouchend"in document),n}(ye);function Vn(e){var t=/(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(String(e));if(!t)return 0;var i=t[1]||"",n=t[2]||0;return Math.max(0,("0"===i?0:i.length)-Number(n))} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class Un extends $e{constructor(){super(...arguments),this.mdcFoundationClass=zn,this.disabled=!1,this.min=0,this.max=100,this.valueEnd=0,this.name="",this.step=1,this.withTickMarks=!1,this.discrete=!1,this.tickMarks=[],this.trackTransformOriginStyle="",this.trackLeftStyle="",this.trackRightStyle="",this.trackTransitionStyle="",this.endThumbWithIndicator=!1,this.endThumbTop=!1,this.shouldRenderEndRipple=!1,this.endThumbTransformStyle="",this.endThumbTransitionStyle="",this.valueToAriaTextTransform=null,this.valueToValueIndicatorTransform=e=>`${e}`,this.boundMoveListener=null,this.endRippleHandlers=new ui((()=>(this.shouldRenderEndRipple=!0,this.endRipple)))}update(e){if(e.has("valueEnd")&&this.mdcFoundation){this.mdcFoundation.setValue(this.valueEnd);const e=this.mdcFoundation.getValue();e!==this.valueEnd&&(this.valueEnd=e)}e.has("discrete")&&(this.discrete||(this.tickMarks=[])),super.update(e)}render(){return this.renderRootEl(P` + ${this.renderStartInput()} + ${this.renderEndInput()} + ${this.renderTrack()} + ${this.renderTickMarks()} + ${this.renderStartThumb()} + ${this.renderEndThumb()}`)}renderRootEl(e){const t=Ve({"mdc-slider--disabled":this.disabled,"mdc-slider--discrete":this.discrete});return P` +
+ ${e} +
`}renderStartInput(){return V}renderEndInput(){var e;return P` + + `}renderTrack(){return V}renderTickMarks(){return this.withTickMarks?P` +
+ ${this.tickMarks.map((e=>{const t=e===xn.ACTIVE;return P`
`}))} +
`:V}renderStartThumb(){return V}renderEndThumb(){const e=Ve({"mdc-slider__thumb--with-indicator":this.endThumbWithIndicator,"mdc-slider__thumb--top":this.endThumbTop}),t=Ti({"-webkit-transform":this.endThumbTransformStyle,transform:this.endThumbTransformStyle,"-webkit-transition":this.endThumbTransitionStyle,transition:this.endThumbTransitionStyle,left:this.endThumbTransformStyle||"rtl"===getComputedStyle(this).direction?"":`calc(${(this.valueEnd-this.min)/(this.max-this.min)*100}% - 24px)`,right:this.endThumbTransformStyle||"rtl"!==getComputedStyle(this).direction?"":`calc(${(this.valueEnd-this.min)/(this.max-this.min)*100}% - 24px)`}),i=this.shouldRenderEndRipple?P``:V;return P` +
+ ${i} + ${this.renderValueIndicator(this.valueToValueIndicatorTransform(this.valueEnd))} +
+
+ `}renderValueIndicator(e){return this.discrete?P` + `:V}disconnectedCallback(){super.disconnectedCallback(),this.mdcFoundation&&this.mdcFoundation.destroy()}createAdapter(){}async firstUpdated(){super.firstUpdated(),await this.layout(!0)}updated(e){super.updated(e),this.mdcFoundation&&(e.has("disabled")&&this.mdcFoundation.setDisabled(this.disabled),e.has("min")&&this.mdcFoundation.setMin(this.min),e.has("max")&&this.mdcFoundation.setMax(this.max),e.has("step")&&this.mdcFoundation.setStep(this.step),e.has("discrete")&&this.mdcFoundation.setIsDiscrete(this.discrete),e.has("withTickMarks")&&this.mdcFoundation.setHasTickMarks(this.withTickMarks))}async layout(e=!1){var t;null===(t=this.mdcFoundation)||void 0===t||t.layout({skipUpdateUI:e}),this.requestUpdate(),await this.updateComplete}onEndChange(e){var t;this.valueEnd=Number(e.target.value),null===(t=this.mdcFoundation)||void 0===t||t.handleInputChange(_n.END)}onEndFocus(){var e;null===(e=this.mdcFoundation)||void 0===e||e.handleInputFocus(_n.END),this.endRippleHandlers.startFocus()}onEndBlur(){var e;null===(e=this.mdcFoundation)||void 0===e||e.handleInputBlur(_n.END),this.endRippleHandlers.endFocus()}onEndMouseenter(){var e;null===(e=this.mdcFoundation)||void 0===e||e.handleThumbMouseenter(),this.endRippleHandlers.startHover()}onEndMouseleave(){var e;null===(e=this.mdcFoundation)||void 0===e||e.handleThumbMouseleave(),this.endRippleHandlers.endHover()}onPointerdown(e){this.layout(),this.mdcFoundation&&(this.mdcFoundation.handlePointerdown(e),this.boundMoveListener=this.mdcFoundation.handleMove.bind(this.mdcFoundation),this.mdcRoot.addEventListener("pointermove",this.boundMoveListener))}onPointerup(){this.mdcFoundation&&(this.mdcFoundation.handleUp(),this.boundMoveListener&&(this.mdcRoot.removeEventListener("pointermove",this.boundMoveListener),this.boundMoveListener=null))}onContextmenu(e){e.preventDefault()}setFormData(e){this.name&&e.append(this.name,`${this.valueEnd}`)}}n([he("input.end")],Un.prototype,"formElement",void 0),n([he(".mdc-slider")],Un.prototype,"mdcRoot",void 0),n([he(".end.mdc-slider__thumb")],Un.prototype,"endThumb",void 0),n([he(".end.mdc-slider__thumb .mdc-slider__thumb-knob")],Un.prototype,"endThumbKnob",void 0),n([ue(".end .ripple")],Un.prototype,"endRipple",void 0),n([se({type:Boolean,reflect:!0})],Un.prototype,"disabled",void 0),n([se({type:Number})],Un.prototype,"min",void 0),n([se({type:Number})],Un.prototype,"max",void 0),n([se({type:Number})],Un.prototype,"valueEnd",void 0),n([se({type:String})],Un.prototype,"name",void 0),n([se({type:Number})],Un.prototype,"step",void 0),n([se({type:Boolean})],Un.prototype,"withTickMarks",void 0),n([se({type:Boolean})],Un.prototype,"discrete",void 0),n([ce()],Un.prototype,"tickMarks",void 0),n([ce()],Un.prototype,"trackTransformOriginStyle",void 0),n([ce()],Un.prototype,"trackLeftStyle",void 0),n([ce()],Un.prototype,"trackRightStyle",void 0),n([ce()],Un.prototype,"trackTransitionStyle",void 0),n([ce()],Un.prototype,"endThumbWithIndicator",void 0),n([ce()],Un.prototype,"endThumbTop",void 0),n([ce()],Un.prototype,"shouldRenderEndRipple",void 0),n([ce()],Un.prototype,"endThumbTransformStyle",void 0),n([ce()],Un.prototype,"endThumbTransitionStyle",void 0),n([Yi,se({type:String,attribute:"aria-label"})],Un.prototype,"ariaLabel",void 0),n([Yi,se({type:String,attribute:"aria-labelledby"})],Un.prototype,"ariaLabelledBy",void 0),n([Yi,se({type:String,attribute:"aria-describedby"})],Un.prototype,"ariaDescribedBy",void 0); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class Xn extends Un{get value(){return this.valueEnd}set value(e){this.valueEnd=e}renderTrack(){const e=Ti({"transform-origin":this.trackTransformOriginStyle,left:this.trackLeftStyle,right:this.trackRightStyle,"-webkit-transform":`scaleX(${(this.valueEnd-this.min)/(this.max-this.min)})`,transform:`scaleX(${(this.valueEnd-this.min)/(this.max-this.min)})`,"-webkit-transition":this.trackTransitionStyle,transition:this.trackTransitionStyle});return P` +
+
+
+
+
+
+
`}createAdapter(){return{addClass:e=>{if("mdc-slider--disabled"===e)this.disabled=!0},removeClass:e=>{if("mdc-slider--disabled"===e)this.disabled=!1},hasClass:e=>{switch(e){case"mdc-slider--disabled":return this.disabled;case"mdc-slider--discrete":return this.discrete;default:return!1}},addThumbClass:(e,t)=>{if(t!==_n.START&&"mdc-slider__thumb--with-indicator"===e)this.endThumbWithIndicator=!0},removeThumbClass:(e,t)=>{if(t!==_n.START&&"mdc-slider__thumb--with-indicator"===e)this.endThumbWithIndicator=!1},registerEventHandler:()=>{},deregisterEventHandler:()=>{},registerBodyEventHandler:(e,t)=>{document.body.addEventListener(e,t)},deregisterBodyEventHandler:(e,t)=>{document.body.removeEventListener(e,t)},registerInputEventHandler:(e,t,i)=>{e!==_n.START&&this.formElement.addEventListener(t,i)},deregisterInputEventHandler:(e,t,i)=>{e!==_n.START&&this.formElement.removeEventListener(t,i)},registerThumbEventHandler:()=>{},deregisterThumbEventHandler:()=>{},registerWindowEventHandler:(e,t)=>{window.addEventListener(e,t)},deregisterWindowEventHandler:(e,t)=>{window.addEventListener(e,t)},emitChangeEvent:(e,t)=>{if(t===_n.START)return;const i=new CustomEvent("change",{bubbles:!0,composed:!0,detail:{value:e,thumb:t}});this.dispatchEvent(i)},emitDragEndEvent:(e,t)=>{t!==_n.START&&this.endRippleHandlers.endPress()},emitDragStartEvent:(e,t)=>{t!==_n.START&&this.endRippleHandlers.startPress()},emitInputEvent:(e,t)=>{if(t===_n.START)return;const i=new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:e,thumb:t}});this.dispatchEvent(i)},focusInput:e=>{e!==_n.START&&this.formElement.focus()},getAttribute:()=>"",getBoundingClientRect:()=>this.mdcRoot.getBoundingClientRect(),getInputAttribute:(e,t)=>{if(t===_n.START)return null;switch(e){case"min":return this.min.toString();case"max":return this.max.toString();case"value":return this.valueEnd.toString();case"step":return this.step.toString();default:return null}},getInputValue:e=>e===_n.START?"":this.valueEnd.toString(),getThumbBoundingClientRect:e=>e===_n.START?this.getBoundingClientRect():this.endThumb.getBoundingClientRect(),getThumbKnobWidth:e=>e===_n.START?0:this.endThumbKnob.getBoundingClientRect().width,getValueToAriaValueTextFn:()=>this.valueToAriaTextTransform,isInputFocused:e=>{if(e===_n.START)return!1;const t=Re();return t[t.length-1]===this.formElement},isRTL:()=>"rtl"===getComputedStyle(this).direction,setInputAttribute:(e,t,i)=>{_n.START},removeInputAttribute:e=>{},setThumbStyleProperty:(e,t,i)=>{if(i!==_n.START)switch(e){case"transform":case"-webkit-transform":this.endThumbTransformStyle=t;break;case"transition":case"-webkit-transition":this.endThumbTransitionStyle=t}},removeThumbStyleProperty:(e,t)=>{if(t!==_n.START)switch(e){case"left":case"right":break;case"transition":case"-webkit-transition":this.endThumbTransitionStyle=""}},setTrackActiveStyleProperty:(e,t)=>{switch(e){case"transform-origin":this.trackTransformOriginStyle=t;break;case"left":this.trackLeftStyle=t;break;case"right":this.trackRightStyle=t;break;case"transform":case"-webkit-transform":break;case"transition":case"-webkit-transition":this.trackTransitionStyle=t}},removeTrackActiveStyleProperty:e=>{switch(e){case"transition":case"-webkit-transition":this.trackTransitionStyle=""}},setInputValue:(e,t)=>{t!==_n.START&&(this.valueEnd=Number(e))},setPointerCapture:e=>{this.mdcRoot.setPointerCapture(e)},setValueIndicatorText:()=>{},updateTickMarks:e=>{this.tickMarks=e}}}}n([se({type:Number})],Xn.prototype,"value",null); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const jn=c`.mdc-slider{cursor:pointer;height:48px;margin:0 24px;position:relative;touch-action:pan-y}.mdc-slider .mdc-slider__track{height:4px;position:absolute;top:50%;transform:translateY(-50%);width:100%}.mdc-slider .mdc-slider__track--active,.mdc-slider .mdc-slider__track--inactive{display:flex;height:100%;position:absolute;width:100%}.mdc-slider .mdc-slider__track--active{border-radius:3px;height:6px;overflow:hidden;top:-1px}.mdc-slider .mdc-slider__track--active_fill{border-top:6px solid;box-sizing:border-box;height:100%;width:100%;position:relative;-webkit-transform-origin:left;transform-origin:left}[dir=rtl] .mdc-slider .mdc-slider__track--active_fill,.mdc-slider .mdc-slider__track--active_fill[dir=rtl]{-webkit-transform-origin:right;transform-origin:right}.mdc-slider .mdc-slider__track--inactive{border-radius:2px;height:4px;left:0;top:0}.mdc-slider .mdc-slider__track--inactive::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid transparent;border-radius:inherit;content:"";pointer-events:none}.mdc-slider .mdc-slider__track--active_fill{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-slider.mdc-slider--disabled .mdc-slider__track--active_fill{border-color:#000;border-color:var(--mdc-theme-on-surface, #000)}.mdc-slider .mdc-slider__track--inactive{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee);opacity:.24}.mdc-slider.mdc-slider--disabled .mdc-slider__track--inactive{background-color:#000;background-color:var(--mdc-theme-on-surface, #000);opacity:.24}.mdc-slider .mdc-slider__value-indicator-container{bottom:44px;left:50%;pointer-events:none;position:absolute;transform:translateX(-50%)}.mdc-slider .mdc-slider__value-indicator{transition:transform 100ms 0ms cubic-bezier(0.4, 0, 1, 1);align-items:center;border-radius:4px;display:flex;height:32px;padding:0 12px;transform:scale(0);transform-origin:bottom}.mdc-slider .mdc-slider__value-indicator::before{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid;bottom:-5px;content:"";height:0;left:50%;position:absolute;transform:translateX(-50%);width:0}.mdc-slider .mdc-slider__value-indicator::after{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid transparent;border-radius:inherit;content:"";pointer-events:none}.mdc-slider .mdc-slider__thumb--with-indicator .mdc-slider__value-indicator-container{pointer-events:auto}.mdc-slider .mdc-slider__thumb--with-indicator .mdc-slider__value-indicator{transition:transform 100ms 0ms cubic-bezier(0, 0, 0.2, 1);transform:scale(1)}@media(prefers-reduced-motion){.mdc-slider .mdc-slider__value-indicator,.mdc-slider .mdc-slider__thumb--with-indicator .mdc-slider__value-indicator{transition:none}}.mdc-slider .mdc-slider__value-indicator-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-subtitle2-font-size, 0.875rem);line-height:1.375rem;line-height:var(--mdc-typography-subtitle2-line-height, 1.375rem);font-weight:500;font-weight:var(--mdc-typography-subtitle2-font-weight, 500);letter-spacing:0.0071428571em;letter-spacing:var(--mdc-typography-subtitle2-letter-spacing, 0.0071428571em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle2-text-transform, inherit)}.mdc-slider .mdc-slider__value-indicator{background-color:#000;opacity:.6}.mdc-slider .mdc-slider__value-indicator::before{border-top-color:#000}.mdc-slider .mdc-slider__value-indicator{color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-slider .mdc-slider__thumb{display:flex;height:48px;left:-24px;outline:none;position:absolute;user-select:none;width:48px}.mdc-slider .mdc-slider__thumb--top{z-index:1}.mdc-slider .mdc-slider__thumb--top .mdc-slider__thumb-knob,.mdc-slider .mdc-slider__thumb--top.mdc-slider__thumb:hover .mdc-slider__thumb-knob,.mdc-slider .mdc-slider__thumb--top.mdc-slider__thumb--focused .mdc-slider__thumb-knob{border-style:solid;border-width:1px;box-sizing:content-box}.mdc-slider .mdc-slider__thumb-knob{box-shadow:0px 2px 1px -1px rgba(0, 0, 0, 0.2),0px 1px 1px 0px rgba(0, 0, 0, 0.14),0px 1px 3px 0px rgba(0,0,0,.12);border:10px solid;border-radius:50%;box-sizing:border-box;height:20px;left:50%;position:absolute;top:50%;transform:translate(-50%, -50%);width:20px}.mdc-slider .mdc-slider__thumb-knob{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee);border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-slider .mdc-slider__thumb--top .mdc-slider__thumb-knob,.mdc-slider .mdc-slider__thumb--top.mdc-slider__thumb:hover .mdc-slider__thumb-knob,.mdc-slider .mdc-slider__thumb--top.mdc-slider__thumb--focused .mdc-slider__thumb-knob{border-color:#fff}.mdc-slider.mdc-slider--disabled .mdc-slider__thumb-knob{background-color:#000;background-color:var(--mdc-theme-on-surface, #000);border-color:#000;border-color:var(--mdc-theme-on-surface, #000)}.mdc-slider.mdc-slider--disabled .mdc-slider__thumb--top .mdc-slider__thumb-knob,.mdc-slider.mdc-slider--disabled .mdc-slider__thumb--top.mdc-slider__thumb:hover .mdc-slider__thumb-knob,.mdc-slider.mdc-slider--disabled .mdc-slider__thumb--top.mdc-slider__thumb--focused .mdc-slider__thumb-knob{border-color:#fff}.mdc-slider .mdc-slider__thumb::before,.mdc-slider .mdc-slider__thumb::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-slider .mdc-slider__thumb:hover::before,.mdc-slider .mdc-slider__thumb.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-slider .mdc-slider__thumb.mdc-ripple-upgraded--background-focused::before,.mdc-slider .mdc-slider__thumb:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-slider .mdc-slider__thumb:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-slider .mdc-slider__thumb:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-slider .mdc-slider__thumb.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-slider .mdc-slider__tick-marks{align-items:center;box-sizing:border-box;display:flex;height:100%;justify-content:space-between;padding:0 1px;position:absolute;width:100%}.mdc-slider .mdc-slider__tick-mark--active,.mdc-slider .mdc-slider__tick-mark--inactive{border-radius:50%;height:2px;width:2px}.mdc-slider .mdc-slider__tick-mark--active{background-color:#fff;background-color:var(--mdc-theme-on-primary, #fff);opacity:.6}.mdc-slider.mdc-slider--disabled .mdc-slider__tick-mark--active{background-color:#fff;background-color:var(--mdc-theme-on-primary, #fff);opacity:.6}.mdc-slider .mdc-slider__tick-mark--inactive{background-color:#6200ee;background-color:var(--mdc-theme-primary, #6200ee);opacity:.6}.mdc-slider.mdc-slider--disabled .mdc-slider__tick-mark--inactive{background-color:#000;background-color:var(--mdc-theme-on-surface, #000);opacity:.6}.mdc-slider.mdc-slider--disabled{opacity:.38;cursor:auto}.mdc-slider.mdc-slider--disabled .mdc-slider__thumb{pointer-events:none}.mdc-slider--discrete .mdc-slider__thumb,.mdc-slider--discrete .mdc-slider__track--active_fill{transition:transform 80ms ease}@media(prefers-reduced-motion){.mdc-slider--discrete .mdc-slider__thumb,.mdc-slider--discrete .mdc-slider__track--active_fill{transition:none}}.mdc-slider__input{cursor:pointer;left:0;margin:0;height:100%;opacity:0;pointer-events:none;position:absolute;top:0;width:100%}:host{outline:none;display:block;-webkit-tap-highlight-color:transparent}.ripple{--mdc-ripple-color:#6200ee;--mdc-ripple-color:var(--mdc-theme-primary, #6200ee)}`,Wn={"mwc-slider":class extends Xn{static get styles(){return jn}},"mwc-ripple":class extends $i{static get styles(){return Xi}}};let Gn=class extends(function(e){return class extends e{createRenderRoot(){const e=this.constructor,{registry:t,elementDefinitions:i,shadowRootOptions:n}=e;i&&!t&&(e.registry=new CustomElementRegistry,Object.entries(i).forEach((([t,i])=>e.registry.define(t,i))));const r=this.renderOptions.creationScope=this.attachShadow({...n,customElements:e.registry});return m(r,this.constructor.elementStyles),r}}}(ae)){constructor(){super(...arguments),this._initialized=!1}setConfig(e){this._config=e,this.loadCardHelpers()}shouldUpdate(){return this._initialized||this._initialize(),!0}get _name(){var e;return(null===(e=this._config)||void 0===e?void 0:e.name)||""}get _entity(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity)||""}get _show_warning(){var e;return(null===(e=this._config)||void 0===e?void 0:e.show_warning)||!1}get _show_error(){var e;return(null===(e=this._config)||void 0===e?void 0:e.show_error)||!1}render(){if(!this.hass||!this._helpers)return P``;let e;return e=this._config,P` +
+ + e.stopPropagation()} + > + + RainViewer - Original + RainViewer - Universal Blue + RainViewer - TITAN + RainViewer - The Weather Channel + RainViewer - Meteored + RainViewer - NEXRAD Level III + RainViewer - Rainbow @ SELEX-IS + RainViewer - Dark Sky + +
+ e.stopPropagation()} + > + + Light + Voyager + Satellite + Dark + + e.stopPropagation()} + > + + 4 + 5 + 6 + 7 + 8 + 9 + 10 + +
+ + + + +
+ + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ `}_initialize(){void 0!==this.hass&&void 0!==this._config&&void 0!==this._helpers&&(this._initialized=!0)}async loadCardHelpers(){this._helpers=await window.loadCardHelpers()}_valueChangedSwitch(e){const t=e.target;this._config&&this.hass&&t&&(this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:t.checked}),_e(this,"config-changed",{config:this._config}))}_valueChangedNumber(e){if(!this._config||!this.hass)return;const t=e.target;this[`_${t.configValue}`]!==t.value&&(t.configValue&&(""===t.value||null===t.value?delete this._config[t.configValue]:this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:Number(t.value)})),_e(this,"config-changed",{config:this._config}))}_valueChangedString(e){if(!this._config||!this.hass)return;const t=e.target;this[`_${t.configValue}`]!==t.value&&(t.configValue&&(""===t.value?delete this._config[t.configValue]:this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:t.value})),_e(this,"config-changed",{config:this._config}))}};Gn.elementDefinitions=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},vn),qi),tn),je),Wn),Gn.styles=c` + mwc-select, + mwc-textfield { + margin-bottom: 16px; + display: block; + } + mwc-formfield { + padding-bottom: 8px; + } + mwc-switch { + --mdc-theme-secondary: var(--switch-checked-color); + } + .option { + padding: 4px 0px; + cursor: pointer; + } + .row { + display: flex; + margin-bottom: -14px; + pointer-events: none; + } + .title { + padding-left: 16px; + margin-top: -6px; + pointer-events: none; + } + .secondary { + padding-left: 40px; + color: var(--secondary-text-color); + pointer-events: none; + } + .values { + padding-left: 16px; + background: var(--secondary-background-color); + } + ha-switch { + padding: 16px 6px; + } + .side-by-side { + display: flex; + } + .side-by-side > * { + flex: 1; + padding-right: 4px; + } + `,n([se({attribute:!1})],Gn.prototype,"hass",void 0),n([ce()],Gn.prototype,"_config",void 0),n([ce()],Gn.prototype,"_helpers",void 0),Gn=n([de("weather-radar-card-editor")],Gn);var qn={version:"Version",invalid_configuration:"Invalid configuration",show_warning:"Show Warning"},Yn={common:qn},Kn={version:"Versjon",invalid_configuration:"Ikke gyldig konfiguration",show_warning:"Vis advarsel"},Zn={common:Kn};const Qn={en:Object.freeze({__proto__:null,common:qn,default:Yn}),nb:Object.freeze({__proto__:null,common:Kn,default:Zn})};function Jn(e,t="",i=""){const n=(localStorage.getItem("selectedLanguage")||"en").replace(/['"]+/g,"").replace("-","_");let r;try{r=e.split(".").reduce(((e,t)=>e[t]),Qn[n])}catch(t){r=e.split(".").reduce(((e,t)=>e[t]),Qn.en)}return void 0===r&&(r=e.split(".").reduce(((e,t)=>e[t]),Qn.en)),""!==t&&""!==i&&(r=r.replace(t,i)),r}console.info(`%c WEATHER-RADAR-CARD \n%c ${Jn("common.version")} 2.1.1 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),window.customCards=window.customCards||[],window.customCards.push({type:"weather-radar-card",name:"Weather Radar Card",description:"A rain radar card using the new tiled images from RainViewer"});let er=class extends ae{constructor(){super(...arguments),this.isPanel=!1}static async getConfigElement(){return document.createElement("weather-radar-card-editor")}static getStubConfig(){return{}}setConfig(e){this._config=e}getCardSize(){return 10}shouldUpdate(){return!0}render(){if(this._config.show_warning)return this.showWarning(Jn("common.show_warning"));const e=`\n \n \n \n Weather Radar Card\n \n \n \n \n