diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..45dd97a4 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,8 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "06:00" diff --git a/.github/workflows/esphome-dummy.yaml b/.github/workflows/esphome-dummy.yaml index bb6cb9ac..081c0ccc 100644 --- a/.github/workflows/esphome-dummy.yaml +++ b/.github/workflows/esphome-dummy.yaml @@ -8,10 +8,12 @@ on: pull_request: paths-ignore: - 'esphome/**' + - '.github/workflows/esphome**' push: branches: [main] paths-ignore: - 'esphome/**' + - '.github/workflows/esphome**' schedule: - cron: 0 12 * * * diff --git a/.github/workflows/esphome-parallel.yaml b/.github/workflows/esphome-parallel.yaml index c75839a5..32be7d32 100644 --- a/.github/workflows/esphome-parallel.yaml +++ b/.github/workflows/esphome-parallel.yaml @@ -6,10 +6,12 @@ on: pull_request: paths: - 'esphome/**' + - '.github/workflows/esphome**' push: branches: [main] paths: - 'esphome/**' + - '.github/workflows/esphome**' schedule: - cron: 0 12 * * * @@ -21,7 +23,7 @@ jobs: file: ${{ steps.set-files.outputs.file }} # generate output name file by using inner step output steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get list of files id: set-files # Give it an id to handle to get step outputs in the outputs key above run: echo "::set-output name=file::$(ls esphome/*.yaml | jq -R -s -c 'split("\n")[:-1]')" @@ -36,7 +38,7 @@ jobs: file: ${{fromJson(needs.files.outputs.file)}} # List matrix strategy from files dynamically steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: ESPHome Version uses: "docker://esphome/esphome:stable" with: @@ -46,8 +48,11 @@ jobs: run: | cp -R esphome/travis_secrets.yaml.txt esphome/common/secrets.yaml cp -R esphome/travis_secrets.yaml.txt esphome/secrets.yaml - - run: echo Compiling ${{matrix.file}} - - run: docker run --rm -v "${PWD}":/config esphome/esphome:stable compile ${{matrix.file}} + - name: Compile all ESPHome ${{matrix.file}} + uses: esphome/build-action@v1 + with: + version: stable + yaml_file: ${{matrix.file}} loop-beta: name: Test ESPHome Beta firmware @@ -58,7 +63,7 @@ jobs: file: ${{fromJson(needs.files.outputs.file)}} # List matrix strategy from files dynamically steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: ESPHome Version uses: "docker://esphome/esphome:beta" with: @@ -68,8 +73,11 @@ jobs: run: | cp -R esphome/travis_secrets.yaml.txt esphome/common/secrets.yaml cp -R esphome/travis_secrets.yaml.txt esphome/secrets.yaml - - run: echo Compiling ${{matrix.file}} - - run: docker run --rm -v "${PWD}":/config esphome/esphome:beta compile ${{matrix.file}} + - name: Compile all ESPHome ${{matrix.file}} + uses: esphome/build-action@v1 + with: + version: beta + yaml_file: ${{matrix.file}} loop-dev: name: Test ESPHome Dev firmware @@ -80,7 +88,7 @@ jobs: file: ${{fromJson(needs.files.outputs.file)}} # List matrix strategy from files dynamically steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: ESPHome Version uses: "docker://esphome/esphome:dev" with: @@ -90,8 +98,11 @@ jobs: run: | cp -R esphome/travis_secrets.yaml.txt esphome/common/secrets.yaml cp -R esphome/travis_secrets.yaml.txt esphome/secrets.yaml - - run: echo Compiling ${{matrix.file}} - - run: docker run --rm -v "${PWD}":/config esphome/esphome:dev compile ${{matrix.file}} + - name: Compile all ESPHome ${{matrix.file}} + uses: esphome/build-action@v1 + with: + version: dev + yaml_file: ${{matrix.file}} # This is used by branch protections final: @@ -100,4 +111,4 @@ jobs: needs: [loop-stable, loop-beta, loop-dev] steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v4 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 15a26a08..7fd5d399 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Running YAMLlint - uses: ibiqlik/action-yamllint@v1 + uses: ibiqlik/action-yamllint@v3 continue-on-error: false with: config_file: .github/yamllint-config.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Running Remark lint uses: "docker://pipelinecomponents/remark-lint:latest" continue-on-error: false @@ -34,7 +34,7 @@ jobs: needs: [yamllint, remarklint] steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Copy stub files into configuration folder run: | cp -R travis_secrets.yaml secrets.yaml @@ -55,7 +55,7 @@ jobs: needs: [yamllint, remarklint] steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Copy stub files into configuration folder run: | cp -R travis_secrets.yaml secrets.yaml @@ -76,7 +76,7 @@ jobs: needs: [yamllint, remarklint] steps: - name: Getting your configuration from GitHub - uses: actions/checkout@v1 + uses: actions/checkout@v4 - name: Copy stub files into configuration folder run: | cp -R travis_secrets.yaml secrets.yaml diff --git a/automation/aurora_alert.yaml b/automation/aurora_alert.yaml index d123e899..c6b2662e 100644 --- a/automation/aurora_alert.yaml +++ b/automation/aurora_alert.yaml @@ -1,10 +1,9 @@ --- alias: Aurora Alert trigger: - platform: state - entity_id: binary_sensor.aurora_visibility_alert - from: "off" - to: "on" + platform: numeric_state + entity_id: sensor.aurora_visibility + above: 1 action: - service: notify.alexa_media_everywhere data: @@ -16,7 +15,7 @@ action: data: title: "Aurora Alert" message: "Alert! The Aurora Borealis might be visible right now!" - - service: notify.mobile_app + - service: notify.mobile_app_nothing_phone_1 data: title: "Aurora Alert" message: "Alert! The Aurora Borealis might be visible right now!" diff --git a/automation/classicfm.yaml b/automation/classicfm.yaml deleted file mode 100644 index b871e11f..00000000 --- a/automation/classicfm.yaml +++ /dev/null @@ -1,14 +0,0 @@ -alias: "Play Classic FM" -trigger: - platform: state - entity_id: input_boolean.classic_fm - from: 'off' - to: 'on' -action: - - service: media_player.play_media - data: - entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f - media_content_id: "http://media-ice.musicradio.com:80/ClassicFMMP3" - media_content_type: music - - service: homeassistant.turn_off - entity_id: input_boolean.classic_fm diff --git a/automation/craftroom_touch_toggle.yaml b/automation/craftroom_touch_toggle.yaml deleted file mode 100644 index 4d710b4b..00000000 --- a/automation/craftroom_touch_toggle.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -alias: Craft Room light toggle -trigger: - - platform: event - event_type: esphome.button_pressed - event_data: - device_name: craftroom_switch - click_type: single -action: - - service: switch.turn_on - entity_id: switch.craft_room_switch_relay - - service: light.toggle - entity_id: group.craft_room_lighting - - service: logbook.log - data_template: - name: EVENT - message: "Toggling craft room light" diff --git a/automation/living_room_sofa_dim.yaml b/automation/living_room_sofa_dim.yaml index 4bc0adc9..175af9cf 100644 --- a/automation/living_room_sofa_dim.yaml +++ b/automation/living_room_sofa_dim.yaml @@ -1,5 +1,6 @@ --- alias: Living Room TV dimmer +id: living_room_tv_dimmer trigger: - platform: state entity_id: media_player.living_room_kodi @@ -14,10 +15,10 @@ condition: entity_id: light.living_room_2 state: 'on' - condition: state - entity_id: light.dining_nook_group + entity_id: light.dining_nook state: 'on' action: - service: light.turn_off entity_id: - light.living_room - - light.dining_nook_group + - light.dining_nook diff --git a/automation/mark_person_as_arrived.yaml b/automation/mark_person_as_arrived.yaml index cc4ede3b..23f3aa12 100644 --- a/automation/mark_person_as_arrived.yaml +++ b/automation/mark_person_as_arrived.yaml @@ -33,6 +33,6 @@ action: "A resident has just arrived!" ] | random + " http://amzn.to/2D3J8jW" }} - - service: notify.twitter_thegordonhome + - service: notify.mastodon_viewpoint data: message: "A resident has just arrived!" diff --git a/automation/media_playing_night.yaml b/automation/media_playing_night.yaml index 9345ad21..4cdc003a 100644 --- a/automation/media_playing_night.yaml +++ b/automation/media_playing_night.yaml @@ -1,4 +1,6 @@ +--- alias: "Media playing at night" +id: media_playing_night trigger: - platform: state entity_id: media_player.living_room_kodi @@ -8,7 +10,7 @@ condition: entity_id: sensor.average_external_light_level below: 1000 - condition: state - entity_id: binary_sensor.kitchen_motion + entity_id: binary_sensor.kitchen_motion_occupancy state: "off" action: - service: light.turn_off diff --git a/automation/media_stopped_night.yaml b/automation/media_stopped_night.yaml index 0a2e3843..cd0f5c73 100644 --- a/automation/media_stopped_night.yaml +++ b/automation/media_stopped_night.yaml @@ -1,4 +1,6 @@ +--- alias: "Media stopped at night" +id: media_stopped_night trigger: - platform: state entity_id: media_player.living_room_kodi diff --git a/cameras.yaml b/cameras.yaml deleted file mode 100644 index 2f6b8245..00000000 --- a/cameras.yaml +++ /dev/null @@ -1,29 +0,0 @@ -- platform: generic - name: Kyle - still_image_url: https://maps.googleapis.com/maps/api/staticmap?center={{ states.device_tracker.bagpuss_a0001.attributes.latitude }},{{ states.device_tracker.bagpuss_a0001.attributes.longitude }}&zoom=13&size=500x500&maptype=roadmap&markers=color:blue%7Clabel:P%7C{{ states.device_tracker.bagpuss_a0001.attributes.latitude }},{{ states.device_tracker.bagpuss_a0001.attributes.longitude }} - limit_refetch_to_url_change: true - -- platform: generic - name: Charlotte - still_image_url: https://maps.googleapis.com/maps/api/staticmap?center={{ states.device_tracker.charlotte_thea.attributes.latitude }},{{ states.device_tracker.charlotte_thea.attributes.longitude }}&zoom=13&size=500x500&maptype=roadmap&markers=color:blue%7Clabel:P%7C{{ states.device_tracker.charlotte_thea.attributes.latitude }},{{ states.device_tracker.charlotte_thea.attributes.longitude }} - limit_refetch_to_url_change: true - -# - platform: generic -# name: ISS -# still_image_url: https://maps.googleapis.com/maps/api/staticmap?center={{ states.binary_sensor.iss.attributes.lat }},{{ states.binary_sensor.iss.attributes.long }}&zoom=5&size=500x500&maptype=roadmap&markers=color:blue%7Clabel:P%7C{{ states.binary_sensor.iss.attributes.lat }},{{ states.binary_sensor.iss.attributes.long }} -# limit_refetch_to_url_change: true - -- platform: generic - name: BackDoor - still_image_url: http://viewpoint.house:4999/api/back_door/latest.jpg?h=400&motion=1 - stream_source: rtmp://viewpoint.house/live/back_door - -- platform: generic - name: Driveway - still_image_url: http://viewpoint.house:4999/api/driveway/latest.jpg?h=400&motion=1 - stream_source: rtmp://viewpoint.house/live/driveway - -- platform: generic - name: FrontDoor - still_image_url: http://viewpoint.house:4999/api/front_door/latest.jpg?h=400&motion=1 - stream_source: rtmp://viewpoint.house/live/front_door diff --git a/configuration.yaml b/configuration.yaml index 938ffc2b..904c845f 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -9,6 +9,7 @@ homeassistant: trusted_networks: - 172.24.32.0/24 - 172.24.34.0/24 + - 172.18.0.0/24 - 10.8.0.18/32 - 127.0.0.1 - type: legacy_api_password @@ -90,6 +91,11 @@ config: recorder: !include recorder.yaml http: + use_x_forwarded_for: true + trusted_proxies: + - 172.18.0.0/24 + - 127.0.0.1 + - ::1 cors_allowed_origins: - http://homeauto.vpn.glasgownet.com - http://viewpoint.house @@ -102,10 +108,6 @@ alarm_control_panel: code_arm_required: false code: !secret alarm_code -deconz: - host: 172.24.32.13 - port: 8100 - influxdb: host: 172.24.32.13 port: 8086 @@ -115,58 +117,6 @@ influxdb: bucket: !secret influxdb_bucket token: !secret influxdb_token -# Discover some devices automatically -discovery: - -rfxtrx: - # Remember this is mapped in the docker-compose file - device: /dev/ttyUSB0 - devices: - ### Lights ### - # 0a140029f0dd3c0e010060: - # name: Living Room - 0a140001f0dd3c01010070: - # White Remote A1 - # name: Christmas Star - 0a140075f0dd3c10000060: - # Black Remote D4 - 0a14000bf0dd3c0f010070: - # name: Lava Lamp - 0a140037f0dd3c0b010060: - # name: Christmas Tree - 0a14003cf0dd3c0a010060: - # name: Craft room fairy lights - 0710080041020000: - # name: Star Burst - 0710080041030000: - # name: Energenie 4 way - 0710080041040000: - # name: Energenie 4 way - 0a140041f0dd3c09010070: - # name: Arc Lamp - 0a14005ff0dd3c05010060: - # name: Energenie 4 way - ### Switches ### - 0710080041010000: - # name: Cordless phone - ### Binary Sensors ### - 0b1100d800b8196e0a010f70: - # name: Outside Driveway Motion - device_class: motion - 0b11000000b82b560a000060: - # name: Outside Front Motion - device_class: motion - - ### Sensors ### - ## RFXcom ## - # Old 115a0100e452000000046600006462cb3e79: - 115a0102e20200000003e500000000d02979: - # name: Electricity - -# zwave: -# usb_path: /dev/zwave -# debug: false - # Allows you to issue voice commands from the frontend in enabled browsers conversation: @@ -278,7 +228,6 @@ switch: !include switches.yaml media_player: !include media_players.yaml device_tracker: !include device_trackers.yaml group: !include groups.yaml -camera: !include cameras.yaml zone: !include zones/places.yaml shell_command: !include shell_commands.yaml climate: !include climate.yaml @@ -323,29 +272,29 @@ smartir: panel_iframe: esphome: title: "ESPHome" - url: "http://viewpoint.house:6052" + url: "https://esphome.viewpoint.house" icon: mdi:car-esp + zigbee2mqtt: + title: "Z2M" + url: "https://z2m.viewpoint.house" + icon: mdi:antenna cctv: title: "CCTV" - url: "http://viewpoint.house:4999" + url: "https://cctv.viewpoint.house" icon: mdi:cctv sonarr: - title: "Sonarr TV" - url: "http://viewpoint.house:8989/sonarr" + title: "TV Shows" + url: "https://tv.viewpoint.house" icon: mdi:television-classic radarr: - title: "Radar Movies" - url: "http://viewpoint.house:7878/radarr" + title: "Movies" + url: "https://movies.viewpoint.house" icon: mdi:movie-open lazylibrarian: - title: "LazyLibrarian Books" - url: "http://viewpoint.house:5299" + title: "Books" + url: "https://books.viewpoint.house" icon: mdi:book-open-page-variant - airsonic: - title: "AirSonic Music" - url: "http://viewpoint.house:4040/index" - icon: mdi:music-circle-outline - nzbget: - title: "NZBGet Downloader" - url: "http://viewpoint.house:6789" - icon: mdi:cloud-download-outline + homepage: + title: "Home Page" + url: "https://home.viewpoint.house" + icon: mdi:home diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 33881c75..f985e8c5 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -35,6 +35,11 @@ def _all_unique_names(value): ) +async def reload_configuration_yaml(event: dict, hass: HomeAssistant): + """Reload configuration.yaml.""" + await hass.services.async_call("homeassistant", "check_config", {}) + + async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" @@ -52,6 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up the component.""" data = hass.data.setdefault(DOMAIN, {}) + # This will reload any changes the user made to any YAML configurations. + # Called during 'quick reload' or hass.reload_config_entry + hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml) + undo_listener = config_entry.add_update_listener(async_update_options) data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} for platform in PLATFORMS: @@ -80,8 +89,7 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data: # no more config_entries turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER) - turn_on_off_listener.remove_listener() - turn_on_off_listener.remove_listener2() + turn_on_off_listener.disable() if not data: hass.data.pop(DOMAIN) diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py new file mode 100644 index 00000000..40afc235 --- /dev/null +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -0,0 +1,116 @@ +from typing import Any + +from homeassistant.helpers import selector +import homeassistant.helpers.config_validation as cv +import pandas as pd +import voluptuous as vol + +from .const import ( + DOCS, + DOCS_APPLY, + DOCS_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, + VALIDATION_TUPLES, + apply_service_schema, +) + + +def _format_voluptuous_instance(instance): + coerce_type = None + min_val = None + max_val = None + + for validator in instance.validators: + if isinstance(validator, vol.Coerce): + coerce_type = validator.type.__name__ + elif isinstance(validator, (vol.Clamp, vol.Range)): + min_val = validator.min + max_val = validator.max + + if min_val is not None and max_val is not None: + return f"`{coerce_type}` {min_val}-{max_val}" + elif min_val is not None: + return f"`{coerce_type} > {min_val}`" + elif max_val is not None: + return f"`{coerce_type} < {max_val}`" + else: + return f"`{coerce_type}`" + + +def _type_to_str(type_: Any) -> str: + """Convert a (voluptuous) type to a string.""" + if type_ == cv.entity_ids: + return "list of `entity_id`s" + elif type_ in (bool, int, float, str): + return f"`{type_.__name__}`" + elif type_ == cv.boolean: + return "bool" + elif isinstance(type_, vol.All): + return _format_voluptuous_instance(type_) + elif isinstance(type_, vol.In): + return f"one of `{type_.container}`" + elif isinstance(type_, selector.SelectSelector): + return f"one of `{type_.config['options']}`" + elif isinstance(type_, selector.ColorRGBSelector): + return "RGB color" + else: + raise ValueError(f"Unknown type: {type_}") + + +def generate_config_markdown_table(): + import pandas as pd + + rows = [] + for k, default, type_ in VALIDATION_TUPLES: + description = DOCS[k] + row = { + "Variable name": f"`{k}`", + "Description": description, + "Default": f"`{default}`", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: + result = {} + for key, value in schema.schema.items(): + if isinstance(key, vol.Optional): + default_value = key.default + result[key.schema] = (default_value, value) + return result + + +def _generate_service_markdown_table( + schema: dict[str, tuple[Any, Any]], alternative_docs: dict[str, str] = None +): + schema = _schema_to_dict(schema) + rows = [] + for k, (default, type_) in schema.items(): + if alternative_docs is not None and k in alternative_docs: + description = alternative_docs[k] + else: + description = DOCS[k] + row = { + "Service data attribute": f"`{k}`", + "Description": description, + "Required": "✅" if default == vol.UNDEFINED else "❌", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def generate_apply_markdown_table(): + return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY) + + +def generate_set_manual_control_markdown_table(): + return _generate_service_markdown_table( + SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL + ) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py new file mode 100644 index 00000000..a5f33a30 --- /dev/null +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -0,0 +1,203 @@ +"""Utility functions for adaptation commands.""" +from collections.abc import AsyncGenerator +from dataclasses import dataclass +import logging +from typing import Any, Literal + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant, State + +_LOGGER = logging.getLogger(__name__) + +COLOR_ATTRS = { # Should ATTR_PROFILE be in here? + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, +} + +BRIGHTNESS_ATTRS = { + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, +} + +ServiceData = dict[str, Any] + + +def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: + """Splits the service data by the adapted attributes, i.e., into separate data + items for brightness and color. + """ + + common_attrs = {ATTR_ENTITY_ID} + common_data = {k: service_data[k] for k in common_attrs if k in service_data} + + attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] + service_datas = [] + + for attributes in attributes_split_sequence: + split_data = { + attribute: service_data[attribute] + for attribute in attributes + if service_data.get(attribute) + } + if split_data: + service_datas.append(common_data | split_data) + + # Distribute the transition duration across all service calls + if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: + transition = service_data[ATTR_TRANSITION] / len(service_datas) + + for service_data in service_datas: + service_data[ATTR_TRANSITION] = transition + + return service_datas + + +def _filter_service_data(service_data: ServiceData, state: State | None) -> ServiceData: + """Filter service data by removing attributes that already equal the given state. + + Removes all attributes from service call data whose values are already present + in the target entity's state.""" + + if not state: + return service_data + + filtered_service_data = { + k: service_data[k] + for k in service_data.keys() + if k not in state.attributes or service_data[k] != state.attributes[k] + } + + return filtered_service_data + + +def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: + """Determines whether the service data justifies an adaptation service call. + + A service call is not justified for data which does not contain any entries that + change relevant attributes of an adapting entity, e.g., brightness or color.""" + common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION} + relevant_attrs = set(service_data) - common_attrs + + return bool(relevant_attrs) + + +async def _create_service_call_data_iterator( + hass: HomeAssistant, + service_datas: list[ServiceData], + filter_by_state: bool, +) -> AsyncGenerator[ServiceData, None]: + """Enumerates and filters a list of service datas on the fly. + + If filtering is enabled, every service data is filtered by the current state of + the related entity and only returned if it contains relevant data that justifies + a service call. + The main advantage of this generator over a list is that it applies the filter + at the time when the service data is read instead of up front. This gives greater + flexibility because entity states can change while the items are iterated. + """ + + for service_data in service_datas: + if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)): + current_entity_state = hass.states.get(entity_id) + + # Filter data to remove attributes that equal the current state + if current_entity_state: + service_data = _filter_service_data(service_data, current_entity_state) + + # Emit service data if it still contains relevant attributes (else try next) + if _has_relevant_service_data_attributes(service_data): + yield service_data + else: + yield service_data + + +@dataclass +class AdaptationData: + """Holds all data required to execute an adaptation.""" + + entity_id: str + context: Context + sleep_time: float + service_call_datas: AsyncGenerator[ServiceData, None] + max_length: int + which: Literal["brightness", "color", "both"] + initial_sleep: bool = False + + async def next_service_call_data(self) -> ServiceData | None: + """Return data for the next service call, or none if no more data exists.""" + return await anext(self.service_call_datas, None) + + +class NoColorOrBrightnessInServiceData(Exception): + """Exception raised when no color or brightness attributes are found in service data.""" + + +def is_color_brightness_or_both( + service_data: ServiceData, +) -> Literal["brightness", "color", "both"]: + """Extract the 'which' attribute from the service data.""" + has_brightness = ATTR_BRIGHTNESS in service_data + has_color = any(attr in service_data for attr in COLOR_ATTRS) + if has_brightness and has_color: + return "both" + if has_brightness: + return "brightness" + if has_color: + return "color" + msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" + raise NoColorOrBrightnessInServiceData(msg) + + +def prepare_adaptation_data( + hass: HomeAssistant, + entity_id: str, + context: Context, + transition: float | None, + split_delay: float, + service_data: ServiceData, + split: bool, + filter_by_state: bool, +) -> AdaptationData: + """Prepares a data object carrying all data required to execute an adaptation.""" + _LOGGER.debug( + "Preparing adaptation data for %s with service data %s", + entity_id, + service_data, + ) + service_datas = ( + [service_data] if not split else _split_service_call_data(service_data) + ) + + sleep_time = ( + transition / max(1, len(service_datas)) if transition is not None else 0 + ) + split_delay + + service_data_iterator = _create_service_call_data_iterator( + hass, service_datas, filter_by_state + ) + + return AdaptationData( + entity_id, + context, + sleep_time=sleep_time, + service_call_datas=service_data_iterator, + max_length=len(service_datas), + which=is_color_brightness_or_both(service_data), + ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 62c71fa2..241689fc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,50 +1,191 @@ """Constants for the Adaptive Lighting integration.""" from homeassistant.components.light import VALID_TRANSITION +from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import voluptuous as vol -ICON = "mdi:theme-light-dark" +ICON_MAIN = "mdi:theme-light-dark" +ICON_BRIGHTNESS = "mdi:brightness-4" +ICON_COLOR_TEMP = "mdi:sun-thermometer" +ICON_SLEEP = "mdi:sleep" DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" +DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"} + + CONF_NAME, DEFAULT_NAME = "name", "default" +DOCS[CONF_NAME] = "Display name for this switch. 📝" + CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟" + CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", False, ) +DOCS[CONF_DETECT_NON_HA_CHANGES] = ( + "Detect non-`light.turn_on` state changes and stop adapting lights. " + "Requires `take_over_control`. 🕵️" +) + +CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( + "include_config_in_attributes", + False, +) +DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = ( + "Show all options as attributes on the switch in " + "Home Assistant when set to `true`. 📝" +) + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +DOCS[CONF_INITIAL_TRANSITION] = ( + "Duration of the first transition when lights turn " + "from `off` to `on` in seconds. ⏲️" +) + CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 +DOCS[CONF_SLEEP_TRANSITION] = ( + 'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴" +) + CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" + CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 +DOCS[CONF_MAX_BRIGHTNESS] = "Maximum brightness percentage. 💡" + CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 +DOCS[CONF_MAX_COLOR_TEMP] = "Coldest color temperature in Kelvin. ❄️" + CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 +DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡" + CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 +DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥" + CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False +DOCS[CONF_ONLY_ONCE] = ( + "Adapt lights only when they are turned on (`true`) or keep adapting them " + "(`false`). 🔄" +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False +DOCS[CONF_PREFER_RGB_COLOR] = ( + "Whether to prefer RGB color adjustment over " + "light color temperature when possible. 🌈" +) + CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( "separate_turn_on_commands", False, ) +DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = ( + "Use separate `light.turn_on` calls for color and brightness, needed for " + "some light types. 🔀" +) + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 +DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness percentage of lights in sleep mode. 😴" + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 +DOCS[CONF_SLEEP_COLOR_TEMP] = ( + "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is " + "`color_temp`) in Kelvin. 😴" +) + CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] +DOCS[CONF_SLEEP_RGB_COLOR] = ( + "RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈' +) + CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) +DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( + 'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙" +) + CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 +DOCS[CONF_SUNRISE_OFFSET] = ( + "Adjust sunrise time with a positive or negative offset " "in seconds. ⏰" +) + CONF_SUNRISE_TIME = "sunrise_time" +DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" + CONF_MAX_SUNRISE_TIME = "max_sunrise_time" +DOCS[CONF_MAX_SUNRISE_TIME] = ( + "Set the latest virtual sunrise time (HH:MM:SS), allowing" + " for earlier real sunrises. 🌅" +) + CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 +DOCS[ + CONF_SUNSET_OFFSET +] = "Adjust sunset time with a positive or negative offset in seconds. ⏰" + CONF_SUNSET_TIME = "sunset_time" +DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇" + CONF_MIN_SUNSET_TIME = "min_sunset_time" +DOCS[CONF_MIN_SUNSET_TIME] = ( + "Set the earliest virtual sunset time (HH:MM:SS), allowing" + " for later real sunsets. 🌇" +) + CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +DOCS[CONF_TAKE_OVER_CONTROL] = ( + "Disable Adaptive Lighting if another source calls `light.turn_on` while lights " + "are on and being adapted. Note that this calls `homeassistant.update_entity` " + "every `interval`! 🔒" +) + CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑" + +CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = ( + "transition_until_sleep", + False, +) +DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( + "When enabled, Adaptive Lighting will treat sleep settings as the minimum, " + "transitioning to these values after sunset. 🌙" +) + +CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 +DOCS[CONF_ADAPT_DELAY] = ( + "Wait time (seconds) between light turn on and Adaptive Lighting applying " + "changes. Might help to avoid flickering. ⏲️" +) + +CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 +DOCS[CONF_SEND_SPLIT_DELAY] = ( + "Delay (ms) between `separate_turn_on_commands` for lights that don't support " + "simultaneous brightness and color setting. ⏲️" +) + +CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0 +DOCS[CONF_AUTORESET_CONTROL] = ( + "Automatically reset the manual control after a number of seconds. " + "Set to 0 to disable. ⏲️" +) + +CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = ( + "skip_redundant_commands", + False, +) +DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( + "Skip sending adaptation commands whose target state already " + "equals the light's known state. Minimizes network traffic and improves the " + "adaptation responsivity in some situations. " + "Disable if physical light states get out of sync with HA's recorded state." +) SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" @@ -53,16 +194,39 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" ATTR_ADAPT_COLOR = "adapt_color" +DOCS[ATTR_ADAPT_COLOR] = "Whether to adapt the color on supporting lights. 🌈" ATTR_ADAPT_BRIGHTNESS = "adapt_brightness" +DOCS[ATTR_ADAPT_BRIGHTNESS] = "Whether to adapt the brightness of the light. 🌞" SERVICE_SET_MANUAL_CONTROL = "set_manual_control" CONF_MANUAL_CONTROL = "manual_control" +DOCS[CONF_MANUAL_CONTROL] = "Whether to manually control the lights. 🔒" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" +DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆" +SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" +CONF_USE_DEFAULTS = "use_defaults" +DOCS[CONF_USE_DEFAULTS] = ( + "Sets the default values not specified in this service call. Options: " + '"current" (default, retains current values), "factory" (resets to ' + 'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' +) -CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 TURNING_OFF_DELAY = 5 -CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 + +DOCS_MANUAL_CONTROL = { + CONF_ENTITY_ID: "The `entity_id` of the switch in which to (un)mark the " + "light as being `manually controlled`. 📝", + CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the " + "switch are selected. 💡", + CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the ' + 'light from the "manual_control" list. 🔒', +} + +DOCS_APPLY = { + CONF_ENTITY_ID: "The `entity_id` of the switch with the settings to apply. 📝", + CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡", +} def int_between(min_int, max_int): @@ -73,9 +237,11 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), + (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), @@ -110,7 +276,17 @@ def int_between(min_int, max_int): (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), - (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 10000)), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), + ( + CONF_AUTORESET_CONTROL, + DEFAULT_AUTORESET_CONTROL, + int_between(0, 365 * 24 * 60 * 60), # 1 year max + ), + ( + CONF_SKIP_REDUNDANT_COMMANDS, + DEFAULT_SKIP_REDUNDANT_COMMANDS, + bool, + ), ] @@ -159,3 +335,30 @@ def replace_none_str(value, replace_with=None): for key, default, validation in _yaml_validation_tuples } ) + + +def apply_service_schema(initial_transition: int = 1): + """Return the schema for the apply service.""" + return vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional( + CONF_TRANSITION, + default=initial_transition, + ): VALID_TRANSITION, + vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, + vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, + vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, + } + ) + + +SET_MANUAL_CONTROL_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, + } +) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py new file mode 100644 index 00000000..5a195bcc --- /dev/null +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -0,0 +1,64 @@ +"""Utility functions for HA core.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.util.read_only_dict import ReadOnlyDict + +from .adaptation_utils import ServiceData + + +def setup_service_call_interceptor( + hass: HomeAssistant, + domain: str, + service: str, + intercept_func: Callable[[ServiceCall, ServiceData], Awaitable[None] | None], +) -> Callable[[], None]: + """Inject a function into a registered service call to preprocess service data. + + The injected interceptor function receives the service call and a writeable data dictionary + (the data of the service call is read-only) before the service call is executed.""" + try: + # HACK: Access protected attribute of HA service registry. + # This is necessary to replace a registered service handler with our + # proxy handler to intercept calls. + registered_services = ( + hass.services._services # pylint: disable=protected-access + ) + except AttributeError as error: + raise RuntimeError( + "Intercept failed because registered services are no longer accessible " + "(internal API may have changed)" + ) from error + + if domain not in registered_services or service not in registered_services[domain]: + raise RuntimeError( + f"Intercept failed because service {domain}.{service} is not registered" + ) + + existing_service = registered_services[domain][service] + + async def service_func_proxy(call: ServiceCall) -> None: + # Convert read-only data to writeable dictionary for modification by interceptor + data = dict(call.data) + + # Call interceptor + await intercept_func(call, data) + + # Convert data back to read-only + call.data = ReadOnlyDict(data) + + # Call original service handler with processed data + await existing_service.job.target(call) + + hass.services.async_register( + domain, service, service_func_proxy, existing_service.schema + ) + + def remove(): + # Remove the interceptor by reinstalling the original service handler + hass.services.async_register( + domain, service, existing_service.job.target, existing_service.schema + ) + + return remove diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index d6580942..fcf644cd 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,12 +1,12 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", - "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", + "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], "config_flow": true, "dependencies": [], - "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.4.1", - "requirements": [], - "iot_class": "calculated" + "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", + "iot_class": "calculated", + "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", + "requirements": ["ulid-transform"], + "version": "1.16.3" } diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 8f449a77..cd25811b 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -1,36 +1,251 @@ +# This file is auto-generated by .github/update-services.py. apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. - example: switch.adaptive_lighting_default + description: The `entity_id` of the switch with the settings to apply. 📝 + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false lights: - description: "entity_id(s) of lights, default: lights of the switch" - example: light.bedroom_ceiling + description: A light (or list of lights) to apply the settings to. 💡 + selector: + entity: + domain: light + multiple: true transition: - description: Transition of the lights. + description: Duration of transition when lights change, in seconds. 🕑 example: 10 + selector: + text: null adapt_brightness: - description: "Adapt the 'brightness', default: true" + description: Whether to adapt the brightness of the light. 🌞 example: true + selector: + boolean: null adapt_color: - description: "Adapt the color_temp/color_rgb, default: true" + description: Whether to adapt the color on supporting lights. 🌈 example: true + selector: + boolean: null prefer_rgb_color: - description: "Prefer to use color_rgb over color_temp if possible, default: false" + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 example: false + selector: + boolean: null turn_on_lights: - description: "Turn on the lights that are off, default: false" + description: Whether to turn on lights that are currently off. 🔆 example: false + selector: + boolean: null set_manual_control: description: Mark whether a light is 'manually controlled'. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. - example: switch.adaptive_lighting_default + description: The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false + lights: + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 + selector: + entity: + domain: light + multiple: true manual_control: - description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" + description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 example: true - lights: - description: entity_id(s) of lights, if not specified, all lights in the switch are selected. - example: light.bedroom_ceiling + default: true + selector: + boolean: null +change_switch_settings: + description: Change any settings you'd like in the switch. All options here are the same as in the config flow. + fields: + entity_id: + description: Entity ID of the switch. 📝 + required: true + selector: + entity: + domain: switch + use_defaults: + description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' + example: current + required: false + default: current + selector: + select: + options: + - current + - configuration + - factory + include_config_in_attributes: + description: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 + required: false + selector: + boolean: null + turn_on_lights: + description: Whether to turn on lights that are currently off. 🔆 + example: false + required: false + selector: + boolean: null + initial_transition: + description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ + example: 1 + required: false + selector: + text: null + sleep_transition: + description: Duration of transition when "sleep mode" is toggled in seconds. 😴 + example: 1 + required: false + selector: + text: null + max_brightness: + description: Maximum brightness percentage. 💡 + required: false + example: 100 + selector: + text: null + max_color_temp: + description: Coldest color temperature in Kelvin. ❄️ + required: false + example: 5500 + selector: + text: null + min_brightness: + description: Minimum brightness percentage. 💡 + required: false + example: 1 + selector: + text: null + min_color_temp: + description: Warmest color temperature in Kelvin. 🔥 + required: false + example: 2000 + selector: + text: null + only_once: + description: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 + example: false + required: false + selector: + boolean: null + prefer_rgb_color: + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 + required: false + example: false + selector: + boolean: null + separate_turn_on_commands: + description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 + required: false + example: false + selector: + boolean: null + send_split_delay: + description: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ + required: false + example: 0 + selector: + boolean: null + sleep_brightness: + description: Brightness percentage of lights in sleep mode. 😴 + required: false + example: 1 + selector: + text: null + sleep_rgb_or_color_temp: + description: Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 + required: false + example: color_temp + selector: + select: + options: + - rgb_color + - color_temp + sleep_rgb_color: + description: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 + required: false + selector: + color_rgb: null + sleep_color_temp: + description: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 + required: false + example: 1000 + selector: + text: null + sunrise_offset: + description: Adjust sunrise time with a positive or negative offset in seconds. ⏰ + required: false + example: 0 + selector: + number: + min: 0 + max: 86300 + sunrise_time: + description: Set a fixed time (HH:MM:SS) for sunrise. 🌅 + required: false + example: '' + selector: + time: null + sunset_offset: + description: Adjust sunset time with a positive or negative offset in seconds. ⏰ + required: false + example: '' + selector: + number: + min: 0 + max: 86300 + sunset_time: + description: Set a fixed time (HH:MM:SS) for sunset. 🌇 + example: '' + required: false + selector: + time: null + max_sunrise_time: + description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 + example: '' + required: false + selector: + time: null + min_sunset_time: + description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 + example: '' + required: false + selector: + time: null + take_over_control: + description: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 + required: false + example: true + selector: + boolean: null + detect_non_ha_changes: + description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ + required: false + example: false + selector: + boolean: null + transition: + description: Duration of transition when lights change, in seconds. 🕑 + required: false + example: 45 + selector: + text: null + adapt_delay: + description: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ + required: false + example: 0 + selector: + text: null + autoreset_control_seconds: + description: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ + required: false + example: 0 + selector: + text: null diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 74099754..7ad84741 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Choose a name for the Adaptive Lighting", + "title": "Choose a name for the Adaptive Lighting instance", "description": "Every instance can contain multiple lights!", "data": { "name": "Name" @@ -19,32 +19,36 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { - "lights": "lights", - "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", - "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", - "interval": "interval: Time between switch updates. (seconds)", - "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", - "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", - "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", - "only_once": "only_once: Only adapt the lights when turning them on.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", - "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", - "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", - "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ac1acc96..fbf6c79c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,7 +4,7 @@ import asyncio import base64 import bisect -from collections import defaultdict +from collections.abc import Callable, Coroutine from copy import deepcopy from dataclasses import dataclass import datetime @@ -17,14 +17,7 @@ import astral from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS_STEP_PCT, - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, - ATTR_HS_COLOR, - ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -34,6 +27,7 @@ COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, COLOR_MODE_XY, ) from homeassistant.components.light import ( @@ -41,8 +35,8 @@ SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - VALID_TRANSITION, is_on, + preprocess_turn_on_alternatives, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -56,9 +50,11 @@ ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, CONF_NAME, + CONF_PARAMS, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -74,7 +70,7 @@ State, callback, ) -from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_platform, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change_event, @@ -88,10 +84,19 @@ color_RGB_to_xy, color_temperature_to_rgb, color_xy_to_hs, + color_xy_to_RGB, ) import homeassistant.util.dt as dt_util +import ulid_transform import voluptuous as vol +from .adaptation_utils import ( + BRIGHTNESS_ATTRS, + COLOR_ATTRS, + AdaptationData, + ServiceData, + prepare_adaptation_data, +) from .const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, @@ -99,7 +104,10 @@ ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, CONF_ADAPT_DELAY, + CONF_ADAPT_UNTIL_SLEEP, + CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, + CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, @@ -114,6 +122,7 @@ CONF_PREFER_RGB_COLOR, CONF_SEND_SPLIT_DELAY, CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SKIP_REDUNDANT_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_RGB_COLOR, @@ -126,18 +135,26 @@ CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, + CONF_USE_DEFAULTS, DOMAIN, EXTRA_VALIDATION, - ICON, + ICON_BRIGHTNESS, + ICON_COLOR_TEMP, + ICON_MAIN, + ICON_SLEEP, SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, VALIDATION_TUPLES, + apply_service_schema, replace_none_str, ) +from .hass_utils import setup_service_call_interceptor _SUPPORT_OPTS = { "brightness": SUPPORT_BRIGHTNESS, @@ -153,44 +170,70 @@ SCAN_INTERVAL = timedelta(seconds=10) +# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode. +# This exists to disable the proactive adaptation in the unit tests and enable it +# only for specific unit tests and when running as integration.""" +INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation" + # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) RGB_REDMEAN_CHANGE = 80 # ≈10% of total range -COLOR_ATTRS = { # Should ATTR_PROFILE be in here? - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, - ATTR_COLOR_TEMP_KELVIN, - ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_RGB_COLOR, - ATTR_XY_COLOR, -} - -BRIGHTNESS_ATTRS = { - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS_STEP_PCT, -} # Keep a short domain version for the context instances (which can only be 36 chars) -_DOMAIN_SHORT = "adapt_lgt" +_DOMAIN_SHORT = "al" + + +def _int_to_base36(num: int) -> str: + """ + Convert an integer to its base-36 representation using numbers and uppercase letters. + + Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive + alphanumeric representation. The function takes an integer `num` as input and returns + its base-36 representation as a string. + + Parameters + ---------- + num + The integer to convert to base-36. + + Returns + ------- + str + The base-36 representation of the input integer. + + Examples + -------- + >>> num = 123456 + >>> base36_num = int_to_base36(num) + >>> print(base36_num) + '2N9' + """ + ALPHANUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + if num == 0: + return ALPHANUMERIC_CHARS[0] + + base36_str = "" + base = len(ALPHANUMERIC_CHARS) + while num: + num, remainder = divmod(num, base) + base36_str = ALPHANUMERIC_CHARS[remainder] + base36_str -def _int_to_bytes(i: int, signed: bool = False) -> bytes: - bits = i.bit_length() - if signed: - # Make room for the sign bit. - bits += 1 - return i.to_bytes((bits + 7) // 8, "little", signed=signed) + return base36_str def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" - str_hash_bytes = _int_to_bytes(hash(string), signed=True) - return base64.b85encode(str_hash_bytes)[:length] + return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length] + + +def _remove_vowels(input_str: str, length: int = 4) -> str: + vowels = "aeiouAEIOU" + output_str = "".join([char for char in input_str if char not in vowels]) + return output_str.zfill(length)[:length] def create_context( @@ -198,96 +241,163 @@ def create_context( ) -> Context: """Create a context that can identify this integration.""" # Use a hash for the name because otherwise the context might become - # too long (max len == 36) to fit in the database. - name_hash = _short_hash(name) + # too long (max len == 26) to fit in the database. # Pack index with base85 to maximize the number of contexts we can create - # before we exceed the 36-character limit and are forced to wrap. - index_packed = base64.b85encode(_int_to_bytes(index, signed=False)) - context_id = f"{_DOMAIN_SHORT}:{name_hash}:{which}:{index_packed}"[:36] + # before we exceed the 26-character limit and are forced to wrap. + time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID + name_hash = _short_hash(name) + which_short = _remove_vowels(which) + context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:" + chars_left = 26 - len(context_id_start) + index_packed = _int_to_base36(index).zfill(chars_left)[-chars_left:] + context_id = context_id_start + index_packed parent_id = parent.id if parent else None return Context(id=context_id, parent_id=parent_id) +def is_our_context_id(context_id: str | None) -> bool: + if context_id is None: + return False + return f":{_DOMAIN_SHORT}:" in context_id + + def is_our_context(context: Context | None) -> bool: """Check whether this integration created 'context'.""" if context is None: return False - return context.id.startswith(_DOMAIN_SHORT) + return is_our_context_id(context.id) -def _split_service_data(service_data, adapt_brightness, adapt_color): - """Split service_data into two dictionaries (for color and brightness).""" - transition = service_data.get(ATTR_TRANSITION) - if transition is not None: - # Split the transition over both commands - service_data[ATTR_TRANSITION] /= 2 - service_datas = [] - if adapt_color: - service_data_color = service_data.copy() - service_data_color.pop(ATTR_BRIGHTNESS, None) - service_datas.append(service_data_color) - if adapt_brightness: - service_data_brightness = service_data.copy() - service_data_brightness.pop(ATTR_RGB_COLOR, None) - service_data_brightness.pop(ATTR_COLOR_TEMP_KELVIN, None) - service_datas.append(service_data_brightness) - - if not service_datas: # neither adapt_brightness nor adapt_color - return [service_data] - return service_datas - - -async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): - """Handle the entity service apply.""" - hass = switch.hass +def _get_switches_with_lights( + hass: HomeAssistant, lights: list[str] +) -> list[AdaptiveSwitch]: + """Get all switches that control at least one of the lights passed.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + data = hass.data[DOMAIN] + switches = [] + for config in config_entries: + entry = data.get(config.entry_id) + if entry is None: # entry might be disabled and therefore missing + continue + switch = data[config.entry_id]["instance"] + all_check_lights = _expand_light_groups(hass, lights) + switch._expand_light_groups() + # Check if any of the lights are in the switch's lights + if set(switch.lights) & set(all_check_lights): + switches.append(switch) + return switches + + +class NoSwitchFoundError(ValueError): + """No switches found for lights.""" + + +def find_switch_for_lights( + hass: HomeAssistant, + lights: list[str], +) -> AdaptiveSwitch: + """Find the switch that controls the lights in 'lights'.""" + switches = _get_switches_with_lights(hass, lights) + if len(switches) == 1: + return switches[0] + elif len(switches) > 1: + on_switches = [s for s in switches if s.is_on] + if len(on_switches) == 1: + # Of the multiple switches, only one is on + return on_switches[0] + raise NoSwitchFoundError( + f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs" + f" ({[s.entity_id for s in switches]}). You must pass a switch under" + f" 'entity_id'." + ) + else: + raise NoSwitchFoundError( + f"find_switch_for_lights: Light(s) {lights} not found in any switch's" + f" configuration. You must either include the light(s) that is/are" + f" in the integration config, or pass a switch under 'entity_id'." + ) + + +# For documentation on this function, see integration_entities() from HomeAssistant Core: +# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 +def _get_switches_from_service_call( + hass: HomeAssistant, service_call: ServiceCall +) -> list[AdaptiveSwitch]: data = service_call.data - all_lights = data[CONF_LIGHTS] - if not all_lights: - all_lights = switch._lights - all_lights = _expand_light_groups(hass, all_lights) - switch.turn_on_off_listener.lights.update(all_lights) - _LOGGER.debug( - "Called 'adaptive_lighting.apply' service with '%s'", - data, - ) - for light in all_lights: - if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): - await switch._adapt_light( # pylint: disable=protected-access - light, - data[CONF_TRANSITION], - data[ATTR_ADAPT_BRIGHTNESS], - data[ATTR_ADAPT_COLOR], - data[CONF_PREFER_RGB_COLOR], - force=True, - context=switch.create_context("service", parent=service_call.context), + lights = data[CONF_LIGHTS] + switch_entity_ids: list[str] | None = data.get("entity_id") + + if not lights and not switch_entity_ids: + raise ValueError( + "adaptive-lighting: Neither a switch nor a light was provided in the service call." + " If you intend to adapt all lights on all switches, please inform the developers at" + " https://github.com/basnijholt/adaptive-lighting about your use case." + " Currently, you must pass either an adaptive-lighting switch or the lights to an" + " `adaptive_lighting` service call." + ) + + if switch_entity_ids is not None: + if len(switch_entity_ids) > 1 and lights: + raise ValueError( + f"adaptive-lighting: Cannot pass multiple switches with lights argument." + f" Invalid service data received: {service_call.data}" ) + switches = [] + ent_reg = entity_registry.async_get(hass) + for entity_id in switch_entity_ids: + ent_entry = ent_reg.async_get(entity_id) + config_id = ent_entry.config_entry_id + switches.append(hass.data[DOMAIN][config_id]["instance"]) + return switches + + if lights: + switch = find_switch_for_lights(hass, lights) + return [switch] + + raise ValueError( + f"adaptive-lighting: Incorrect data provided in service call." + f" Entities not found in the integration. Service data: {service_call.data}" + ) -async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): - """Set or unset lights as 'manually controlled'.""" - lights = service_call.data[CONF_LIGHTS] - if not lights: - all_lights = switch._lights # pylint: disable=protected-access +async def handle_change_switch_settings( + switch: AdaptiveSwitch, service_call: ServiceCall +) -> None: + """Allows HASS to change config values via a service call.""" + data = service_call.data + + which = data.get(CONF_USE_DEFAULTS, "current") + if which == "current": # use whatever we're already using. + defaults = switch._current_settings # pylint: disable=protected-access + elif which == "factory": # use actual defaults listed in the documentation + defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + elif which == "configuration": + # use whatever's in the config flow or configuration.yaml + defaults = switch._config_backup # pylint: disable=protected-access else: - all_lights = _expand_light_groups(switch.hass, lights) + defaults = None + + switch._set_changeable_settings( + data=data, + defaults=defaults, + ) + + switch._update_time_interval_listener() + _LOGGER.debug( - "Called 'adaptive_lighting.set_manual_control' service with '%s'", - service_call.data, + "Called 'adaptive_lighting.change_switch_settings' service with '%s'", + data, ) - if service_call.data[CONF_MANUAL_CONTROL]: - for light in all_lights: - switch.turn_on_off_listener.manual_control[light] = True - _fire_manual_control_event(switch, light, service_call.context) - else: - switch.turn_on_off_listener.reset(*all_lights) - # pylint: disable=protected-access - if switch.is_on: - await switch._update_attrs_and_maybe_adapt_lights( - all_lights, - transition=switch._initial_transition, - force=True, - context=switch.create_context("service", parent=service_call.context), - ) + + all_lights = switch.lights # pylint: disable=protected-access + switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) + if switch.is_on: + await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access + all_lights, + transition=switch.initial_transition, + force=True, + context=switch.create_context("service", parent=service_call.context), + ) @callback @@ -302,6 +412,7 @@ def _fire_manual_control_event( switch.entity_id, light, ) + switch.turn_on_off_listener.mark_as_manual_control(light) fire( f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, @@ -317,12 +428,17 @@ async def async_setup_entry( assert config_entry.entry_id in data if ATTR_TURN_ON_OFF_LISTENER not in data: - data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) - turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] - - sleep_mode_switch = SimpleSwitch("Sleep Mode", False, hass, config_entry) - adapt_color_switch = SimpleSwitch("Adapt Color", True, hass, config_entry) - adapt_brightness_switch = SimpleSwitch("Adapt Brightness", True, hass, config_entry) + data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass, config_entry) + turn_on_off_listener: TurnOnOffListener = data[ATTR_TURN_ON_OFF_LISTENER] + sleep_mode_switch = SimpleSwitch( + "Sleep Mode", False, hass, config_entry, ICON_SLEEP + ) + adapt_color_switch = SimpleSwitch( + "Adapt Color", True, hass, config_entry, ICON_COLOR_TEMP + ) + adapt_brightness_switch = SimpleSwitch( + "Adapt Brightness", True, hass, config_entry, ICON_BRIGHTNESS + ) switch = AdaptiveSwitch( hass, config_entry, @@ -332,52 +448,130 @@ async def async_setup_entry( adapt_brightness_switch, ) + # save our switch instance, allows us to make switch's entity_id optional in service calls. + hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch + data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch async_add_entities( - [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch], + [sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, switch], update_before_add=True, ) + @callback + async def handle_apply(service_call: ServiceCall): + """Handle the entity service apply.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.apply' service with '%s'", + data, + ) + switches = _get_switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + if not lights: + all_lights = switch.lights + else: + all_lights = _expand_light_groups(switch.hass, lights) + switch.turn_on_off_listener.lights.update(all_lights) + for light in all_lights: + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + await switch._adapt_light( # pylint: disable=protected-access + light, + data[CONF_TRANSITION], + data[ATTR_ADAPT_BRIGHTNESS], + data[ATTR_ADAPT_COLOR], + data[CONF_PREFER_RGB_COLOR], + context=switch.create_context( + "service", parent=service_call.context + ), + ) + + @callback + async def handle_set_manual_control(service_call: ServiceCall): + """Set or unset lights as 'manually controlled'.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.set_manual_control' service with '%s'", + data, + ) + switches = _get_switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + if not lights: + all_lights = switch.lights + else: + all_lights = _expand_light_groups(switch.hass, lights) + if service_call.data[CONF_MANUAL_CONTROL]: + for light in all_lights: + _fire_manual_control_event(switch, light, service_call.context) + else: + switch.turn_on_off_listener.reset(*all_lights) + if switch.is_on: + # pylint: disable=protected-access + await switch._update_attrs_and_maybe_adapt_lights( + all_lights, + transition=switch.initial_transition, + force=True, + context=switch.create_context( + "service", parent=service_call.context + ), + ) + # Register `apply` service - platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_APPLY, - { - vol.Optional( - CONF_LIGHTS, default=[] - ): cv.entity_ids, # pylint: disable=protected-access - vol.Optional( - CONF_TRANSITION, - default=switch._initial_transition, # pylint: disable=protected-access - ): VALID_TRANSITION, - vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, - vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, - vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, - vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, - }, - handle_apply, + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_APPLY, + service_func=handle_apply, + schema=apply_service_schema( + switch.initial_transition + ), # pylint: disable=protected-access ) + # Register `set_manual_control` service + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_MANUAL_CONTROL, + service_func=handle_set_manual_control, + schema=SET_MANUAL_CONTROL_SCHEMA, + ) + + args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} + # Modifying these after init isn't possible + skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) + for k, _, valid in VALIDATION_TUPLES: + if k not in skip: + args[vol.Optional(k)] = valid + platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_SET_MANUAL_CONTROL, - { - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, - vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, - }, - handle_set_manual_control, + SERVICE_CHANGE_SWITCH_SETTINGS, + args, + handle_change_switch_settings, ) -def validate(config_entry: ConfigEntry): +def validate( + config_entry: ConfigEntry, + service_data: dict[str, Any] | None = None, + defaults: dict[str, Any] | None = None, +): """Get the options and data from the config_entry and add defaults.""" - defaults = {key: default for key, default, _ in VALIDATION_TUPLES} - data = deepcopy(defaults) - data.update(config_entry.options) # come from options flow - data.update(config_entry.data) # all yaml settings come from data + if defaults is None: + data = {key: default for key, default, _ in VALIDATION_TUPLES} + else: + data = defaults + + if config_entry is not None: + assert service_data is None + assert defaults is None + data.update(config_entry.options) # come from options flow + data.update(config_entry.data) # all yaml settings come from data + else: + assert service_data is not None + data.update(service_data) data = {key: replace_none_str(value) for key, value in data.items()} for key, (validate_value, _) in EXTRA_VALIDATION.items(): value = data.get(key) @@ -418,7 +612,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = { key for key, value in _SUPPORT_OPTS.items() if supported_features & value } @@ -431,6 +625,9 @@ def _supported_features(hass: HomeAssistant, light: str): if COLOR_MODE_RGBW in supported_color_modes: supported.add("color") supported.add("brightness") # see above url + if COLOR_MODE_RGBWW: + supported.add("color") + supported.add("brightness") # see above url if COLOR_MODE_XY in supported_color_modes: supported.add("color") supported.add("brightness") # see above url @@ -464,6 +661,41 @@ def color_difference_redmean( return math.sqrt(red_term + green_term + blue_term) +# All comparisons should be done with RGB since +# converting anything to color temp is inaccurate. +def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: + if ATTR_RGB_COLOR in attributes: + return attributes + + rgb = None + if ATTR_COLOR_TEMP_KELVIN in attributes: + rgb = color_temperature_to_rgb(attributes[ATTR_COLOR_TEMP_KELVIN]) + elif ATTR_XY_COLOR in attributes: + rgb = color_xy_to_RGB(*attributes[ATTR_XY_COLOR]) + + if rgb is not None: + attributes[ATTR_RGB_COLOR] = rgb + _LOGGER.debug(f"Converted {attributes} to rgb {rgb}") + else: + _LOGGER.debug("No suitable conversion found") + + return attributes + + +def _add_missing_attributes( + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], +) -> dict[str, Any]: + if not any( + attr in old_attributes and attr in new_attributes + for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR] + ): + old_attributes = _convert_attributes(old_attributes) + new_attributes = _convert_attributes(new_attributes) + + return old_attributes, new_attributes + + def _attributes_have_changed( light: str, old_attributes: dict[str, Any], @@ -472,6 +704,11 @@ def _attributes_have_changed( adapt_color: bool, context: Context, ) -> bool: + if adapt_color: + old_attributes, new_attributes = _add_missing_attributes( + old_attributes, new_attributes + ) + if ( adapt_brightness and ATTR_BRIGHTNESS in old_attributes @@ -526,21 +763,6 @@ def _attributes_have_changed( context.id, ) return True - - switched_color_temp = ( - ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes - ) - switched_to_rgb_color = ( - ATTR_COLOR_TEMP_KELVIN in old_attributes - and ATTR_COLOR_TEMP_KELVIN not in new_attributes - ) - if switched_color_temp or switched_to_rgb_color: - # Light switched from RGB mode to color_temp or visa versa - _LOGGER.debug( - "'%s' switched from RGB mode to color_temp or visa versa", - light, - ) - return True return False @@ -557,6 +779,7 @@ def __init__( adapt_brightness_switch: SimpleSwitch, ): """Initialize the Adaptive Lighting switch.""" + # Set attributes that can't be modified during runtime self.hass = hass self.turn_on_off_listener = turn_on_off_listener self.sleep_mode_switch = sleep_mode_switch @@ -564,20 +787,97 @@ def __init__( self.adapt_brightness_switch = adapt_brightness_switch data = validate(config_entry) + self._name = data[CONF_NAME] - self._lights = data[CONF_LIGHTS] + self._interval: timedelta = data[CONF_INTERVAL] + self.lights: list[str] = data[CONF_LIGHTS] + + # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS + self._config_backup = deepcopy(data) + self._set_changeable_settings( + data=data, + defaults=None, + ) + + # Set other attributes + self._icon = ICON_MAIN + self._state = None + + # Tracks 'off' → 'on' state changes + self._on_to_off_event: dict[str, Event] = {} + # Tracks 'on' → 'off' state changes + self._off_to_on_event: dict[str, Event] = {} + # Locks that prevent light adjusting when waiting for a light to 'turn_off' + self._locks: dict[str, asyncio.Lock] = {} + # To count the number of `Context` instances + self._context_cnt: int = 0 + + # Set in self._update_attrs_and_maybe_adapt_lights + self._settings: dict[str, Any] = {} + + # Set and unset tracker in async_turn_on and async_turn_off + self.remove_listeners = [] + self.remove_interval: Callable[[], None] = lambda: None + + _LOGGER.debug( + "%s: Setting up with '%s'," + " config_entry.data: '%s'," + " config_entry.options: '%s', converted to '%s'.", + self._name, + self.lights, + config_entry.data, + config_entry.options, + data, + ) + + def _set_changeable_settings( + self, + data: dict, + defaults: dict, + ): + # Only pass settings users can change during runtime + data = validate( + config_entry=None, + service_data=data, + defaults=defaults, + ) + + # backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS + self._current_settings = data self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] - self._initial_transition = data[CONF_INITIAL_TRANSITION] + self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] + self._config: dict[str, Any] = {} + if self._include_config_in_attributes: + attrdata = deepcopy(data) + for k, v in attrdata.items(): + if isinstance(v, (datetime.date, datetime.datetime)): + attrdata[k] = v.isoformat() + if isinstance(v, (datetime.timedelta)): + attrdata[k] = v.total_seconds() + self._config.update(attrdata) + + self.initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] - self._interval = data[CONF_INTERVAL] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] - self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = data[CONF_TRANSITION] self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] + self._take_over_control = data[CONF_TAKE_OVER_CONTROL] + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]: + _LOGGER.warning( + "%s: Config mismatch: 'detect_non_ha_changes: true' " + "requires 'take_over_control' to be enabled. Adjusting config " + "and continuing setup with `take_over_control: true`.", + self._name, + ) + self._take_over_control = True + self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] + self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] + self._expand_light_groups() # updates manual control timers _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -589,6 +889,7 @@ def __init__( self._sun_light_settings = SunLightSettings( name=self._name, astral_location=location, + adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP], max_brightness=data[CONF_MAX_BRIGHTNESS], max_color_temp=data[CONF_MAX_COLOR_TEMP], min_brightness=data[CONF_MIN_BRIGHTNESS], @@ -606,33 +907,10 @@ def __init__( time_zone=self.hass.config.time_zone, transition=data[CONF_TRANSITION], ) - - # Set other attributes - self._icon = ICON - self._state = None - - # Tracks 'off' → 'on' state changes - self._on_to_off_event: dict[str, Event] = {} - # Tracks 'on' → 'off' state changes - self._off_to_on_event: dict[str, Event] = {} - # Locks that prevent light adjusting when waiting for a light to 'turn_off' - self._locks: dict[str, asyncio.Lock] = {} - # To count the number of `Context` instances - self._context_cnt: int = 0 - - # Set in self._update_attrs_and_maybe_adapt_lights - self._settings: dict[str, Any] = {} - - # Set and unset tracker in async_turn_on and async_turn_off - self.remove_listeners = [] _LOGGER.debug( - "%s: Setting up with '%s'," - " config_entry.data: '%s'," - " config_entry.options: '%s', converted to '%s'.", + "%s: Set switch settings for lights '%s'. now using data: '%s'", self._name, - self._lights, - config_entry.data, - config_entry.options, + self.lights, data, ) @@ -672,9 +950,12 @@ async def async_will_remove_from_hass(self): self._remove_listeners() def _expand_light_groups(self) -> None: - all_lights = _expand_light_groups(self.hass, self._lights) + all_lights = _expand_light_groups(self.hass, self.lights) self.turn_on_off_listener.lights.update(all_lights) - self._lights = list(all_lights) + self.turn_on_off_listener.set_auto_reset_manual_control_times( + all_lights, self._auto_reset_manual_control_time + ) + self.lights = list(all_lights) async def _setup_listeners(self, _=None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) @@ -684,25 +965,53 @@ async def _setup_listeners(self, _=None) -> None: assert not self.remove_listeners - remove_interval = async_track_time_interval( - self.hass, self._async_update_at_interval, self._interval - ) + self._update_time_interval_listener() + remove_sleep = async_track_state_change_event( self.hass, self.sleep_mode_switch.entity_id, self._sleep_mode_switch_state_event, ) - self.remove_listeners.extend([remove_interval, remove_sleep]) + self.remove_listeners.append(remove_sleep) - if self._lights: + if self.lights: self._expand_light_groups() remove_state = async_track_state_change_event( - self.hass, self._lights, self._light_event + self.hass, self.lights, self._light_event ) self.remove_listeners.append(remove_state) + def _update_time_interval_listener(self) -> None: + """Create or recreate the adaptation interval listener. + + Recreation is necessary when the configuration has changed (e.g., `send_split_delay`). + """ + self._remove_interval_listener() + + # An adaptation takes a little longer than its nominal duration due processing overhead, + # so we factor this in to avoid overlapping adaptations. Since this is a constant value, + # it might not cover all cases, but if large enough, it covers most. + # Ideally, the interval and adaptation are a coupled process where a finished adaptation + # triggers the next, but that requires a larger architectural change. + processing_overhead_time = 0.5 + + adaptation_interval = ( + self._interval + + timedelta(milliseconds=self._send_split_delay) + + timedelta(seconds=processing_overhead_time) + ) + + self.remove_interval = async_track_time_interval( + self.hass, self._async_update_at_interval, adaptation_interval + ) + + def _remove_interval_listener(self) -> None: + self.remove_interval() + def _remove_listeners(self) -> None: + self._remove_interval_listener() + while self.remove_listeners: remove_listener = self.remove_listeners.pop() remove_listener() @@ -715,14 +1024,24 @@ def icon(self) -> str: @property def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the switch.""" + extra_state_attributes = {"configuration": self._config} if not self.is_on: - return {key: None for key in self._settings} - manual_control = [ + for key in self._settings: + extra_state_attributes[key] = None + return extra_state_attributes + extra_state_attributes["manual_control"] = [ light - for light in self._lights + for light in self.lights if self.turn_on_off_listener.manual_control.get(light) ] - return dict(self._settings, manual_control=manual_control) + extra_state_attributes.update(self._settings) + timers = self.turn_on_off_listener.auto_reset_manual_control_timers + extra_state_attributes["autoreset_time_remaining"] = { + light: time + for light in self.lights + if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0 + } + return extra_state_attributes def create_context( self, which: str = "default", parent: Context | None = None @@ -752,11 +1071,11 @@ async def async_turn_on( # pylint: disable=arguments-differ if self.is_on: return self._state = True - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, + transition=self.initial_transition, force=True, context=self.create_context("turn_on"), ) @@ -767,7 +1086,7 @@ async def async_turn_off(self, **kwargs) -> None: return self._state = False self._remove_listeners() - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( @@ -776,23 +1095,15 @@ async def _async_update_at_interval(self, now=None) -> None: context=self.create_context("interval"), ) - async def _adapt_light( + async def prepare_adaptation_data( self, light: str, transition: int | None = None, adapt_brightness: bool | None = None, adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, - force: bool = False, context: Context | None = None, - ) -> None: - lock = self._locks.get(light) - if lock is not None and lock.locked(): - _LOGGER.debug("%s: '%s' is locked", self._name, light) - return - service_data = {ATTR_ENTITY_ID: light} - features = _supported_features(self.hass, light) - + ) -> AdaptationData | None: if transition is None: transition = self._transition if adapt_brightness is None: @@ -802,14 +1113,28 @@ async def _adapt_light( if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color - if "transition" in features: - service_data[ATTR_TRANSITION] = transition + if not adapt_color and not adapt_brightness: + _LOGGER.debug( + "%s: Skipping adaptation of %s because both adapt_brightness and" + " adapt_color are False", + self._name, + light, + ) + return None # The switch might be off and not have _settings set. self._settings = self._sun_light_settings.get_settings( self.sleep_mode_switch.is_on, transition ) + # Build service data. + service_data = {ATTR_ENTITY_ID: light} + features = _supported_features(self.hass, light) + + # Check transition == 0 to fix #378 + use_transition = "transition" in features and transition > 0 + if use_transition: + service_data[ATTR_TRANSITION] = transition if "brightness" in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness @@ -836,50 +1161,119 @@ async def _adapt_light( service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] context = context or self.create_context("adapt_lights") - if ( - self._take_over_control - and self._detect_non_ha_changes - and not force - and await self.turn_on_off_listener.significant_change( - self, - light, - adapt_brightness, - adapt_color, - context, + + self.turn_on_off_listener.last_service_data[light] = service_data + + return prepare_adaptation_data( + self.hass, + light, + context, + transition if use_transition else 0, + self._send_split_delay / 1000.0, + service_data, + split=self._separate_turn_on_commands, + filter_by_state=self._skip_redundant_commands, + ) + + async def _adapt_light( # noqa: C901 + self, + light: str, + transition: int | None = None, + adapt_brightness: bool | None = None, + adapt_color: bool | None = None, + prefer_rgb_color: bool | None = None, + context: Context | None = None, + ) -> None: + lock = self._locks.get(light) + if lock is not None and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) + return + + if self.turn_on_off_listener.is_proactively_adapting(context.parent_id): + # Skip if adaptation was already executed by the service call interceptor + _LOGGER.debug( + "%s: Skipping reactive adaptation of %s", self._name, context.parent_id ) - ): return - self.turn_on_off_listener.last_service_data[light] = service_data - async def turn_on(service_data): + data = await self.prepare_adaptation_data( + light, + transition, + adapt_brightness, + adapt_color, + prefer_rgb_color, + context, + ) + if data is None: + return None # nothing to adapt + + await self.execute_cancellable_adaptation_calls(data) + + async def _execute_adaptation_calls(self, data: AdaptationData): + """Executes a sequence of adaptation service calls for the given service datas.""" + + for index in range(data.max_length): + is_first_call = index == 0 + + # Sleep between multiple service calls. + if not is_first_call or data.initial_sleep: + await asyncio.sleep(data.sleep_time) + + # Instead of directly iterating the generator in the while-loop, we get + # the next item here after the sleep to make sure it incorporates state + # changes which happened during the sleep. + service_data = await data.next_service_call_data() + + if not service_data: + # All service datas processed + break + _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" " with context.id='%s'", self._name, service_data, - context.id, + data.context.id, ) await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=context, + context=data.context, ) - if not self._separate_turn_on_commands: - await turn_on(service_data) - else: - # Could be a list of length 1 or 2 - service_datas = _split_service_data( - service_data, adapt_brightness, adapt_color + async def execute_cancellable_adaptation_calls( + self, + data: AdaptationData, + ): + """Executes a cancellable sequence of adaptation service calls for the given service datas. + + Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g., + to cancel an ongoing adaptation when a light is turned off. + """ + # Prevent overlap of multiple adaptation sequences + listener = self.turn_on_off_listener + listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) + _LOGGER.debug( + "%s: execute_cancellable_adaptation_calls with data: %s", + self._name, + data, + ) + # Execute adaptation calls within a task + try: + task = asyncio.ensure_future(self._execute_adaptation_calls(data)) + if data.which in ("both", "brightness"): + listener.adaptation_tasks_brightness[data.entity_id] = task + if data.which in ("both", "color"): + listener.adaptation_tasks_color[data.entity_id] = task + await task + except asyncio.CancelledError: + _LOGGER.debug( + "%s: Ongoing adaptation of %s cancelled, with AdaptationData: %s", + self._name, + data.entity_id, + data, ) - await turn_on(service_datas[0]) - if len(service_datas) == 2: - transition = service_datas[0].get(ATTR_TRANSITION) - if transition is not None: - await asyncio.sleep(transition) - await asyncio.sleep(self._send_split_delay / 1000.0) - await turn_on(service_datas[1]) async def _update_attrs_and_maybe_adapt_lights( self, @@ -890,22 +1284,51 @@ async def _update_attrs_and_maybe_adapt_lights( ) -> None: assert context is not None _LOGGER.debug( - "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'", + "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'" + " lights: '%s', transition: '%s', force: '%s'", self._name, context.id, + lights, + transition, + force, ) assert self.is_on - self._settings = self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on, transition + self._settings.update( + self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on, transition + ) ) self.async_write_ha_state() + if lights is None: - lights = self._lights - if (self._only_once and not force) or not lights: + lights = self.lights + + filtered_lights = [] + if not force: + if self._only_once: + return + for light in lights: + # Don't adapt lights that haven't finished prior transitions. + timer = self.turn_on_off_listener.transition_timers.get(light) + if timer is not None and timer.is_running(): + _LOGGER.debug( + "%s: Light '%s' is still transitioning", + self._name, + light, + ) + else: + filtered_lights.append(light) + else: + filtered_lights = lights + + if not filtered_lights: return - await self._adapt_lights(lights, transition, force, context) - async def _adapt_lights( + await self._update_manual_control_and_maybe_adapt( + filtered_lights, transition, force, context + ) + + async def _update_manual_control_and_maybe_adapt( self, lights: list[str], transition: int | None, @@ -914,34 +1337,53 @@ async def _adapt_lights( ) -> None: assert context is not None _LOGGER.debug( - "%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called", + "%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called", self.name, lights, transition, force, context.id, ) + + adapt_brightness = self.adapt_brightness_switch.is_on + adapt_color = self.adapt_color_switch.is_on + for light in lights: if not is_on(self.hass, light): continue - if ( - self._take_over_control - and self.turn_on_off_listener.is_manually_controlled( + + manually_controlled = self.turn_on_off_listener.is_manually_controlled( + self, + light, + force, + adapt_brightness, + adapt_color, + ) + + significant_change = ( + self._detect_non_ha_changes + and not force + and await self.turn_on_off_listener.significant_change( self, light, - force, - self.adapt_brightness_switch.is_on, - self.adapt_color_switch.is_on, - ) - ): - _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", - self._name, - light, - context.id, + adapt_brightness, + adapt_color, + context, ) - continue - await self._adapt_light(light, transition, force=force, context=context) + ) + + if self._take_over_control and (manually_controlled or significant_change): + if manually_controlled: + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", + self._name, + light, + context.id, + ) + else: + _fire_manual_control_event(self, light, context) + else: + await self._adapt_light(light, transition, context=context) async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): @@ -951,7 +1393,7 @@ async def _sleep_mode_switch_state_event(self, event: Event) -> None: "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event ) # Reset the manually controlled status when the "sleep mode" changes - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._sleep_transition, force=True, @@ -974,7 +1416,15 @@ async def _light_event(self, event: Event) -> None: entity_id, event.context.id, ) - self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) + + if ( + event.context.parent_id + and not self.turn_on_off_listener.is_proactively_adapting( + event.context.id + ) + ): + self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) + # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -1009,7 +1459,7 @@ async def _light_event(self, event: Event) -> None: await self._update_attrs_and_maybe_adapt_lights( lights=[entity_id], - transition=self._initial_transition, + transition=self.initial_transition, force=True, context=self.create_context("light_event", parent=event.context), ) @@ -1028,12 +1478,17 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" def __init__( - self, which: str, initial_state: bool, hass: HomeAssistant, config_entry + self, + which: str, + initial_state: bool, + hass: HomeAssistant, + config_entry: ConfigEntry, + icon: str, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) - self._icon = ICON + self._icon = icon self._state = None self._which = which name = data[CONF_NAME] @@ -1089,6 +1544,7 @@ class SunLightSettings: name: str astral_location: astral.Location + adapt_until_sleep: bool max_brightness: int max_color_temp: int min_brightness: int @@ -1238,7 +1694,12 @@ def calc_color_temp_kelvin(self, percent: float) -> int: delta = self.max_color_temp - self.min_color_temp ct = (delta * percent) + self.min_color_temp return 5 * round(ct / 5) # round to nearest 5 - return self.min_color_temp + if percent == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and percent < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + percent)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 def get_settings( self, is_sleep, transition @@ -1261,11 +1722,14 @@ def get_settings( rgb_color: tuple[float, float, float] = color_temperature_to_rgb( color_temp_kelvin ) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) return { "brightness_pct": brightness_pct, "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, "rgb_color": rgb_color, "xy_color": xy_color, "hs_color": hs_color, @@ -1276,9 +1740,10 @@ def get_settings( class TurnOnOffListener: """Track 'light.turn_off' and 'light.turn_on' service calls.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass + data = validate(config_entry) self.lights = set() # Tracks 'light.turn_off' service calls @@ -1289,46 +1754,335 @@ def __init__(self, hass: HomeAssistant): self.sleep_tasks: dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manual_control: dict[str, bool] = {} - # Counts the number of times (in a row) a light had a changed state. - self.cnt_significant_changes: dict[str, int] = defaultdict(int) # Track 'state_changed' events of self.lights resulting from this integration self.last_state_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} + # Track ongoing split adaptations to be able to cancel them + self.adaptation_tasks_brightness: dict[str, asyncio.Task] = {} + self.adaptation_tasks_color: dict[str, asyncio.Task] = {} + + # Track auto reset of manual_control + self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} + self.auto_reset_manual_control_times: dict[str, float] = {} + + # Track light transitions + self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} + + self.listener_removers = [] + + self.listener_removers.append( + self.hass.bus.async_listen( + EVENT_CALL_SERVICE, self.turn_on_off_event_listener + ) + ) + self.listener_removers.append( + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, self.state_changed_event_listener + ) + ) + + self._proactively_adapting_contexts: dict[str, str] = {} + + is_proactive_adaptation_enabled = ( + data.get(INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True) is not False + ) + + if is_proactive_adaptation_enabled: + try: + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + self._service_interceptor_turn_on_handler, + ) + ) + + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TOGGLE, + self._service_interceptor_turn_on_handler, + ) + ) + + _LOGGER.debug("Proactive adaptation enabled") + except RuntimeError: + _LOGGER.warning( + "Failed to set up service call interceptors, " + "falling back to event-reactive mode", + exc_info=True, + ) + + def disable(self): + """Disable the listener by removing all subscribed handlers.""" + for remove in self.listener_removers: + remove() + + def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: + """Declare the adaptation with the given context ID as proactively adapting, + and associate it to an entity ID.""" + self._proactively_adapting_contexts[context_id] = entity_id + + def is_proactively_adapting(self, context_id: str) -> bool: + """Determine whether an adaptation with the given context ID is proactive.""" + is_proactively_adapting_context = ( + context_id in self._proactively_adapting_contexts + ) + + _LOGGER.debug( + "is_proactively_adapting_context %s %s", + context_id, + is_proactively_adapting_context, + ) + + return is_proactively_adapting_context + + def clear_proactively_adapting(self, entity_id: str) -> None: + """Clear all context IDs associated with the given entity ID. + + Call this method to clear past context IDs and avoid a memory leak.""" + keys = [ + k for k, v in self._proactively_adapting_contexts.items() if v == entity_id + ] + + for key in keys: + self._proactively_adapting_contexts.pop(key) + + async def _service_interceptor_turn_on_handler( + self, call: ServiceCall, data: ServiceData + ): + # Don't adapt our own service calls + if is_our_context(call.context): + return + + entity_ids = self._get_entity_list(data) + + # For simplicity, only service calls affecting a single entity are currently handled. + # + # To add support for adapting multiple entities, the following properties + # need to hold for _all_ entities: + # - managed by this AL instance + # - not manually controlled + # - supporting the same relevant feature set + # - off state + if len(entity_ids) != 1: + return + + entity_id = entity_ids[0] + try: + adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) + except NoSwitchFoundError: + # This might be a light that is not managed by this AL instance. + _LOGGER.debug( + "No (or multiple) adaptive switch(es) found for entity %s," + " skipping adaptation by intercepting service call", + entity_id, + ) + return + + if not adaptive_switch.is_on: + return + + if entity_id not in adaptive_switch.lights: + return + + if self.manual_control.get(entity_id, False): + return + + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + if self.hass.states.is_state(entity_id, STATE_ON): + return + + _LOGGER.debug( + "Intercepted TURN_ON call with data %s (%s)", data, call.context.id + ) + + self.reset(entity_id, reset_manual_control=False) + self.clear_proactively_adapting(entity_id) + + adapt_brightness = adaptive_switch.adapt_brightness_switch.is_on or False + adapt_color = adaptive_switch.adapt_color_switch.is_on or False + transition = ( + data[CONF_PARAMS].get(ATTR_TRANSITION, None) + or adaptive_switch.initial_transition + ) + + adaptation_data = await adaptive_switch.prepare_adaptation_data( + entity_id, + transition, + adapt_brightness, + adapt_color, + ) + if adaptation_data is None: + return + + # Take first adaptation item to apply it to this service call + first_service_data = await adaptation_data.next_service_call_data() - # When a state is different `max_cnt_significant_changes` times in a row, - # mark it as manually_controlled. - self.max_cnt_significant_changes = 2 + if not first_service_data: + return - self.remove_listener = self.hass.bus.async_listen( - EVENT_CALL_SERVICE, self.turn_on_off_event_listener + # Update/adapt service call data + first_service_data.pop(ATTR_ENTITY_ID, None) + # This is called as a preprocessing step by the schema validation of the original + # service call and needs to be repeated here to also process the added adaptation data. + # (A more generic alternative would be re-executing the validation, but that is more + # complicated and unstable because it requires transformation of the data object back + # into its original service call structure which cannot be reliably done due to the + # lack of a bijective mapping.) + preprocess_turn_on_alternatives(self.hass, first_service_data) + data[CONF_PARAMS].update(first_service_data) + + # Schedule additional service calls for the remaining adaptation data. + # We cannot know here whether there is another call to follow (since the + # state can change until the next call), so we just schedule it and let + # it sort out by itself. + self.set_proactively_adapting(call.context.id, entity_id) + self.set_proactively_adapting(adaptation_data.context.id, entity_id) + adaptation_data.initial_sleep = True + asyncio.create_task( # Don't await to avoid blocking the service call + adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data) ) - self.remove_listener2 = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self.state_changed_event_listener + + def _handle_timer( + self, + light: str, + timers_dict: dict[str, _AsyncSingleShotTimer], + delay: float | None, + reset_coroutine: Callable[[], Coroutine[Any, Any, None]], + ) -> None: + timer = timers_dict.get(light) + if timer is not None: + if delay is None: # Timer object exists, but should not anymore + timer.cancel() + timers_dict.pop(light) + else: # Timer object already exists, just update the delay and restart it + timer.delay = delay + timer.start() + elif delay is not None: # Timer object does not exist, create it + timer = _AsyncSingleShotTimer(delay, reset_coroutine) + timers_dict[light] = timer + timer.start() + + def start_transition_timer(self, light: str) -> None: + """Mark a light as manually controlled.""" + last_service_data = self.last_service_data.get(light) + if not last_service_data: + _LOGGER.debug("This should not ever happen. Please report to the devs.") + return + last_transition = last_service_data.get(ATTR_TRANSITION) + if not last_transition: + _LOGGER.debug( + "No transition in last adapt for light %s, continuing...", light + ) + return + _LOGGER.debug( + "Start transition timer of %s seconds for light %s", last_transition, light ) + async def reset(): + ValueError("TEST") + _LOGGER.debug( + "Transition finished for light %s", + light, + ) + + self._handle_timer(light, self.transition_timers, last_transition, reset) + + def set_auto_reset_manual_control_times(self, lights: list[str], time: float): + """Set the time after which the lights are automatically reset.""" + if time == 0: + return + for light in lights: + old_time = self.auto_reset_manual_control_times.get(light) + if (old_time is not None) and (old_time != time): + _LOGGER.info( + "Setting auto_reset_manual_control for '%s' from %s seconds to %s seconds." + " This might happen because the light is in multiple swiches" + " or because of a config change.", + light, + old_time, + time, + ) + self.auto_reset_manual_control_times[light] = time + + def mark_as_manual_control(self, light: str) -> None: + """Mark a light as manually controlled.""" + _LOGGER.debug("Marking '%s' as manually controlled.", light) + self.manual_control[light] = True + delay = self.auto_reset_manual_control_times.get(light) + + async def reset(): + self.reset(light) + switches = _get_switches_with_lights(self.hass, [light]) + for switch in switches: + if not switch.is_on: + continue + await switch._update_attrs_and_maybe_adapt_lights( + [light], + transition=switch.initial_transition, + force=True, + context=switch.create_context("autoreset"), + ) + _LOGGER.debug( + "Auto resetting 'manual_control' status of '%s' because" + " it was not manually controlled for %s seconds.", + light, + delay, + ) + assert not self.manual_control[light] + + self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) + + def cancel_ongoing_adaptation_calls( + self, light_id: str, which: Literal["color", "brightness", "both"] = "both" + ): + """Cancel ongoing adaptation service calls for a specific light entity.""" + brightness_task = self.adaptation_tasks_brightness.get(light_id) + color_task = self.adaptation_tasks_color.get(light_id) + if which in ("both", "brightness") and brightness_task is not None: + _LOGGER.debug( + "Cancelled ongoing brightness adaptation calls (%s) for '%s'", + brightness_task, + light_id, + ) + brightness_task.cancel() + if ( + which in ("both", "color") + and color_task is not None + and color_task is not brightness_task + ): + _LOGGER.debug( + "Cancelled ongoing color adaptation calls (%s) for '%s'", + color_task, + light_id, + ) + # color_task might be the same as brightness_task + color_task.cancel() + def reset(self, *lights, reset_manual_control=True) -> None: """Reset the 'manual_control' status of the lights.""" for light in lights: if reset_manual_control: self.manual_control[light] = False + timer = self.auto_reset_manual_control_timers.pop(light, None) + if timer is not None: + timer.cancel() self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) - self.cnt_significant_changes[light] = 0 + self.cancel_ongoing_adaptation_calls(light) - async def turn_on_off_event_listener(self, event: Event) -> None: - """Track 'light.turn_off' and 'light.turn_on' service calls.""" - domain = event.data.get(ATTR_DOMAIN) - if domain != LIGHT_DOMAIN: - return + def _get_entity_list(self, service_data: ServiceData) -> list[str]: + entity_ids = [] - service = event.data[ATTR_SERVICE] - service_data = event.data[ATTR_SERVICE_DATA] if ATTR_ENTITY_ID in service_data: entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) elif ATTR_AREA_ID in service_data: area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) - entity_ids = [] for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) for entity_id in area_entity_ids: @@ -1341,8 +2095,19 @@ async def turn_on_off_event_listener(self, event: Event) -> None: _LOGGER.debug( "No entity_ids or area_ids found in service_data: %s", service_data ) + + return entity_ids + + async def turn_on_off_event_listener(self, event: Event) -> None: + """Track 'light.turn_off' and 'light.turn_on' service calls.""" + domain = event.data.get(ATTR_DOMAIN) + if domain != LIGHT_DOMAIN: return + service = event.data[ATTR_SERVICE] + service_data = event.data[ATTR_SERVICE_DATA] + entity_ids = self._get_entity_list(service_data) + if not any(eid in self.lights for eid in entity_ids): return @@ -1369,11 +2134,19 @@ async def turn_on_off_event_listener(self, event: Event) -> None: if task is not None: task.cancel() self.turn_on_event[eid] = event + timer = self.auto_reset_manual_control_timers.get(eid) + if ( + timer is not None + and timer.is_running() + and event.time_fired > timer.start_time + ): + # Restart the auto reset timer + timer.start() async def state_changed_event_listener(self, event: Event) -> None: """Track 'state_changed' events.""" entity_id = event.data.get(ATTR_ENTITY_ID, "") - if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN: + if entity_id not in self.lights: return new_state = event.data.get("new_state") @@ -1385,11 +2158,7 @@ async def state_changed_event_listener(self, event: Event) -> None: new_state.context.id, ) - if ( - new_state is not None - and new_state.state == STATE_ON - and is_our_context(new_state.context) - ): + if new_state is not None and new_state.state == STATE_ON: # It is possible to have multiple state change events with the same context. # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` # event leads to an instant state change of @@ -1402,21 +2171,29 @@ async def state_changed_event_listener(self, event: Event) -> None: # incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). old_state: list[State] | None = self.last_state_change.get(entity_id) - if ( - old_state is not None - and old_state[0].context.id == new_state.context.id - ): - # If there is already a state change event from this event (with this - # context) then append it to the already existing list. - _LOGGER.debug( - "State change event of '%s' is already in 'self.last_state_change' (%s)" - " adding this state also", - entity_id, - new_state.context.id, - ) + if is_our_context(new_state.context): + if ( + old_state is not None + and old_state[0].context.id == new_state.context.id + ): + _LOGGER.debug( + "TurnOnOffListener: State change event of '%s' is already" + " in 'self.last_state_change' (%s)" + " adding this state also", + entity_id, + new_state.context.id, + ) + self.last_state_change[entity_id].append(new_state) + else: + _LOGGER.debug( + "TurnOnOffListener: New adapt '%s' found for %s", + new_state, + entity_id, + ) + self.last_state_change[entity_id] = [new_state] + self.start_transition_timer(entity_id) + elif old_state is not None: self.last_state_change[entity_id].append(new_state) - else: - self.last_state_change[entity_id] = [new_state] def is_manually_controlled( self, @@ -1444,7 +2221,7 @@ def is_manually_controlled( ): # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. - manual_control = self.manual_control[light] = True + manual_control = True _fire_manual_control_event(switch, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" @@ -1471,64 +2248,56 @@ async def significant_change( detected, we mark the light as 'manually controlled' until the light or switch is turned 'off' and 'on' again. """ - if light not in self.last_state_change: - return False - old_states: list[State] = self.last_state_change[light] - await self.hass.helpers.entity_component.async_update_entity(light) - new_state = self.hass.states.get(light) + last_service_data = self.last_service_data.get(light) + if last_service_data is None: + return compare_to = functools.partial( _attributes_have_changed, light=light, - new_attributes=new_state.attributes, adapt_brightness=adapt_brightness, adapt_color=adapt_color, context=context, ) - for index, old_state in enumerate(old_states): - changed = compare_to(old_attributes=old_state.attributes) - if not changed: - _LOGGER.debug( - "State of '%s' didn't change wrt change event nr. %s (context.id=%s)", - light, - index, - context.id, - ) - break - - last_service_data = self.last_service_data.get(light) - if changed and last_service_data is not None: - # It can happen that the state change events that are associated - # with the last 'light.turn_on' call by this integration were not - # final states. Possibly a later EVENT_STATE_CHANGED happened, where - # the correct target brightness/color was reached. - changed = compare_to(old_attributes=last_service_data) - if not changed: + # Update state and check for a manual change not done in HA. + # Ensure HASS is correctly updating your light's state with + # light.turn_on calls if any problems arise. This + # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. + if switch._detect_non_ha_changes: + _LOGGER.debug( + "%s: 'detect_non_ha_changes: true', calling update_entity(%s)" + " and check if it's last adapt succeeded.", + switch._name, + light, + ) + # This update_entity probably isn't necessary now that we're checking + # if transitions finished from our last adapt. + await self.hass.helpers.entity_component.async_update_entity(light) + refreshed_state = self.hass.states.get(light) + _LOGGER.debug( + "%s: Current state of %s: %s", + switch._name, + light, + refreshed_state, + ) + changed = compare_to( + old_attributes=last_service_data, + new_attributes=refreshed_state.attributes, + ) + if changed: _LOGGER.debug( "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)", light, context.id, ) - - n_changes = self.cnt_significant_changes[light] - if changed: - self.cnt_significant_changes[light] += 1 - if n_changes >= self.max_cnt_significant_changes: - # Only mark a light as significantly changing, if changed==True - # N times in a row. We do this because sometimes a state changes - # happens only *after* a new update interval has already started. - self.manual_control[light] = True - _fire_manual_control_event(switch, light, context, is_async=False) - else: - if n_changes > 1: - _LOGGER.debug( - "State of '%s' had 'cnt_significant_changes=%s' but the state" - " changed to the expected settings now", - light, - n_changes, - ) - self.cnt_significant_changes[light] = 0 - - return changed + return True + _LOGGER.debug( + "%s: Light '%s' correctly matches our last adapt's service data, continuing..." + " context.id=%s.", + switch._name, + light, + context.id, + ) + return False async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None @@ -1627,3 +2396,45 @@ async def maybe_cancel_adjusting( # other 'off' → 'on' state switches resulting from polling. That # would mean we 'return True' here. return False + + +class _AsyncSingleShotTimer: + def __init__(self, delay, callback): + """Initialize the timer.""" + self.delay = delay + self.callback = callback + self.task = None + self.start_time: int | None = None + + async def _run(self): + """Run the timer. Don't call this directly, use start() instead.""" + self.start_time = dt_util.utcnow() + await asyncio.sleep(self.delay) + if self.callback: + if asyncio.iscoroutinefunction(self.callback): + await self.callback() + else: + self.callback() + + def is_running(self): + """Return whether the timer is running.""" + return self.task is not None and not self.task.done() + + def start(self): + """Start the timer.""" + if self.task is not None and not self.task.done(): + self.task.cancel() + self.task = asyncio.create_task(self._run()) + + def cancel(self): + """Cancel the timer.""" + if self.task: + self.task.cancel() + self.callback = None + + def remaining_time(self): + """Return the remaining time before the timer expires.""" + if self.start_time is not None: + elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds() + return max(0, self.delay - elapsed_time) + return 0 diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json new file mode 100644 index 00000000..58f8fead --- /dev/null +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -0,0 +1,57 @@ +{ + "title": "Adaptivní osvětlení", + "config": { + "step": { + "user": { + "title": "Vyberte název instance Adaptivního osvětlení", + "description": "Vyberte název pro tuto instanci. Můžete spustit několik instancí Adaptivního osvětlení, každá z nich může obsahovat více světel!", + "data": { + "name": "Název" + } + } + }, + "abort": { + "already_configured": "Toto zařízení je již nakonfigurováno" + } + }, + "options": { + "step": { + "init": { + "title": "Nastavení adaptivního osvětlení", + "description": "Všechna nastavení komponenty Adaptivního osvětlení. Názvy možností odpovídají nastavení YAML. Pokud máte v konfiguraci YAML definovánu položku 'adaptive_lighting', nezobrazí se žádné možnosti.", + "data": { + "lights": "osvětlení", + "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", + "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", + "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", + "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", + "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", + "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", + "min_color_temp": "min_color_temp, Nejteplejší odstín cyklu teploty barev. (Kelvin)", + "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", + "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", + "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", + "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, v RGB", + "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)", + "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", + "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", + "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", + "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", + "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", + "transition": "transition: doba přechodu při změně osvětlení (sekundy)", + "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání." + } + } + }, + "error": { + "option_error": "Neplatná možnost", + "entity_missing": "V aplikaci Home Assistant chybí jedna nebo více vybraných entit osvětlení" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index cc427138..79eb0df8 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -45,7 +45,8 @@ "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden." + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index be556ec0..e6d1f9a9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Choose a name for the Adaptive Lighting instance", - "description": "Pick a name for this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!", + "description": "Every instance can contain multiple lights!", "data": { "name": "Name" } @@ -20,32 +20,36 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", "data": { - "lights": "lights", - "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", - "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", - "interval": "interval: Time between switch updates. (seconds)", - "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", - "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", - "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", - "only_once": "only_once: Only adapt the lights when turning them on.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", - "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", - "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", - "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 80cc8fdd..99a97e3c 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -36,7 +36,7 @@ "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (sekund)" diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index bd321dda..929dbea2 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -11,7 +11,7 @@ from json import JSONDecodeError import logging import time -from typing import Optional, Text +from typing import Optional from alexapy import ( AlexaAPI, @@ -182,6 +182,7 @@ async def async_setup(hass, config, discovery_info=None): # @retry_async(limit=5, delay=5, catch_exceptions=True) async def async_setup_entry(hass, config_entry): + # noqa: MC0001 """Set up Alexa Media Player as config entry.""" async def close_alexa_media(event=None) -> None: @@ -192,6 +193,7 @@ async def close_alexa_media(event=None) -> None: await close_connections(hass, email) async def complete_startup(event=None) -> None: + # pylint: disable=unused-argument """Run final tasks after startup.""" _LOGGER.debug("Completing remaining startup tasks.") await asyncio.sleep(10) @@ -315,6 +317,7 @@ async def login_success(event=None) -> None: ), ) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_push_activity"] = 0 if not hass.data[DATA_ALEXAMEDIA]["accounts"][email]["second_account_index"]: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_alexa_media) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, complete_startup) @@ -331,9 +334,11 @@ async def login_success(event=None) -> None: async def setup_alexa(hass, config_entry, login_obj: AlexaLogin): + # pylint: disable=too-many-statements,too-many-locals """Set up a alexa api based on host parameter.""" async def async_update_data() -> Optional[AlexaEntityData]: + # noqa pylint: disable=too-many-branches """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables @@ -399,11 +404,17 @@ async def async_update_data() -> Optional[AlexaEntityData]: if temp and temp.enabled: entities_to_monitor.add(temp.alexa_entity_id) + temp = sensor.get("Air_Quality") + if temp and temp.enabled: + entities_to_monitor.add(temp.alexa_entity_id) + for light in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"]["light"]: if light.enabled: entities_to_monitor.add(light.alexa_entity_id) - for binary_sensor in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"]["binary_sensor"]: + for binary_sensor in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ + "binary_sensor" + ]: if binary_sensor.enabled: entities_to_monitor.add(binary_sensor.alexa_entity_id) @@ -445,8 +456,8 @@ async def async_update_data() -> Optional[AlexaEntityData]: # First run is a special case. Get the state of all entities(including disabled) # This ensures all entities have state during startup without needing to request coordinator refresh - for typeOfEntity, entities in alexa_entities.items(): - if typeOfEntity == "guard" or extended_entity_discovery: + for type_of_entity, entities in alexa_entities.items(): + if type_of_entity == "guard" or extended_entity_discovery: for entity in entities: entities_to_monitor.add(entity.get("id")) entity_state = await get_entity_data( @@ -482,7 +493,7 @@ async def async_update_data() -> Optional[AlexaEntityData]: ) return except BaseException as err: - raise UpdateFailed(f"Error communicating with API: {err}") + raise UpdateFailed(f"Error communicating with API: {err}") from err new_alexa_clients = [] # list of newly discovered device names exclude_filter = [] @@ -530,13 +541,13 @@ async def async_update_data() -> Optional[AlexaEntityData]: _LOGGER.debug("Excluding %s for lacking capability", dev_name) continue - if "bluetoothStates" in bluetooth: + if bluetooth is not None and "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if serial == b_state["deviceSerialNumber"]: device["bluetooth_state"] = b_state break - if "devicePreferences" in preferences: + if preferences is not None and "devicePreferences" in preferences: for dev in preferences["devicePreferences"]: if dev["deviceSerialNumber"] == serial: device["locale"] = dev["locale"] @@ -549,7 +560,7 @@ async def async_update_data() -> Optional[AlexaEntityData]: ) break - if "doNotDisturbDeviceStatusList" in dnd: + if dnd is not None and "doNotDisturbDeviceStatusList" in dnd: for dev in dnd["doNotDisturbDeviceStatusList"]: if dev["deviceSerialNumber"] == serial: device["dnd"] = dev["enabled"] @@ -626,7 +637,7 @@ async def async_update_data() -> Optional[AlexaEntityData]: for device_entry in dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ): - for (_, identifier) in device_entry.identifiers: + for _, identifier in device_entry.identifiers: if identifier in hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "devices" ]["media_player"].keys() or identifier in map( @@ -662,6 +673,7 @@ async def async_update_data() -> Optional[AlexaEntityData]: async def process_notifications(login_obj, raw_notifications=None): """Process raw notifications json.""" if not raw_notifications: + await asyncio.sleep(4) raw_notifications = await AlexaAPI.get_notifications(login_obj) email: str = login_obj.email previous = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( @@ -687,7 +699,9 @@ async def process_notifications(login_obj, raw_notifications=None): notification["date_time"] = ( f"{n_date} {n_time}" if n_date and n_time else None ) - previous_alarm = previous.get(n_dev_id, {}).get("Alarm", {}).get(n_id) + previous_alarm = ( + previous.get(n_dev_id, {}).get("Alarm", {}).get(n_id) + ) if previous_alarm and alarm_just_dismissed( notification, previous_alarm.get("status"), @@ -695,7 +709,10 @@ async def process_notifications(login_obj, raw_notifications=None): ): hass.bus.async_fire( "alexa_media_alarm_dismissal_event", - event_data={"device": {"id": n_dev_id}, "event": notification}, + event_data={ + "device": {"id": n_dev_id}, + "event": notification, + }, ) if n_dev_id not in notifications: @@ -840,6 +857,7 @@ async def ws_connect() -> WebsocketEchoClient: return websocket async def ws_handler(message_obj): + # pylint: disable=too-many-branches """Handle websocket messages. This allows push notifications from Alexa to update last_called @@ -864,7 +882,6 @@ async def ws_handler(message_obj): ] coord = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["coordinator"] if command and json_payload: - _LOGGER.debug( "%s: Received websocket command: %s : %s", hide_email(email), @@ -892,23 +909,36 @@ async def ws_handler(message_obj): else: serial = None if command == "PUSH_ACTIVITY": - # Last_Alexa Updated - last_called = { - "serialNumber": serial, - "timestamp": json_payload["timestamp"], - } - try: - await coord.async_request_refresh() - if serial and serial in existing_serials: - await update_last_called(login_obj, last_called) - async_dispatcher_send( - hass, - f"{DOMAIN}_{hide_email(email)}"[0:32], - {"push_activity": json_payload}, - ) - except (AlexapyConnectionError): - # Catch case where activities doesn't report valid json - pass + if ( + datetime.now().timestamp() * 1000 + - hass.data[DATA_ALEXAMEDIA]["accounts"][email][ + "last_push_activity" + ] + > 100 + ): + # Last_Alexa Updated + last_called = { + "serialNumber": serial, + "timestamp": json_payload["timestamp"], + } + try: + await coord.async_request_refresh() + if serial and serial in existing_serials: + await update_last_called(login_obj, last_called) + async_dispatcher_send( + hass, + f"{DOMAIN}_{hide_email(email)}"[0:32], + {"push_activity": json_payload}, + ) + except AlexapyConnectionError: + # Catch case where activities doesn't report valid json + pass + else: + # Duplicate PUSH_ACTIVITY message + _LOGGER.debug("Skipped processing of double PUSH_ACTIVITY message") + hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_push_activity"] = ( + datetime.now().timestamp() * 1000 + ) elif command in ( "PUSH_AUDIO_PLAYER_STATE", "PUSH_MEDIA_CHANGE", diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 7841d1e1..c5ae861d 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -8,7 +8,7 @@ """ from asyncio import sleep import logging -from typing import Dict, List, Optional, Text # noqa pylint: disable=unused-import +from typing import List, Optional from alexapy import hide_email, hide_serial from homeassistant.const import ( @@ -50,7 +50,13 @@ async def async_setup_platform( ) -> bool: """Set up the Alexa alarm control panel platform.""" devices = [] # type: List[AlexaAlarmControlPanel] - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] @@ -132,7 +138,6 @@ class AlexaAlarmControlPanel(AlarmControlPanel, AlexaMedia, CoordinatorEntity): """Implementation of Alexa Media Player alarm control panel.""" def __init__(self, login, coordinator, guard_entity, media_players=None) -> None: - # pylint: disable=unexpected-keyword-arg """Initialize the Alexa device.""" AlexaMedia.__init__(self, None, login) CoordinatorEntity.__init__(self, coordinator) @@ -144,7 +149,7 @@ def __init__(self, login, coordinator, guard_entity, media_players=None) -> None self._guard_entity_id = guard_entity["id"] self._friendly_name = "Alexa Guard " + self._appliance_id[-5:] self._media_players = {} or media_players - self._attrs: Dict[Text, Text] = {} + self._attrs: dict[str, str] = {} _LOGGER.debug( "%s: Guard Discovered %s: %s %s", self.account, @@ -154,8 +159,9 @@ def __init__(self, login, coordinator, guard_entity, media_players=None) -> None ) @_catch_login_errors - async def _async_alarm_set(self, command: Text = "", code=None) -> None: - # pylint: disable=unexpected-keyword-arg + async def _async_alarm_set( + self, command: str = "", code=None # pylint: disable=unused-argument + ) -> None: """Send command.""" try: if not self.enabled: @@ -187,14 +193,16 @@ async def _async_alarm_set(self, command: Text = "", code=None) -> None: ) await self.coordinator.async_request_refresh() - async def async_alarm_disarm(self, code=None) -> None: - # pylint: disable=unexpected-keyword-arg + async def async_alarm_disarm( + self, code=None # pylint:disable=unused-argument + ) -> None: """Send disarm command.""" await self._async_alarm_set(STATE_ALARM_DISARMED) - async def async_alarm_arm_away(self, code=None) -> None: + async def async_alarm_arm_away( + self, code=None # pylint:disable=unused-argument + ) -> None: """Send arm away command.""" - # pylint: disable=unexpected-keyword-arg await self._async_alarm_set(STATE_ALARM_ARMED_AWAY) @property @@ -215,14 +223,14 @@ def state(self): ) if _state == "ARMED_AWAY": return STATE_ALARM_ARMED_AWAY - elif _state == "ARMED_STAY": - return STATE_ALARM_DISARMED - else: + if _state == "ARMED_STAY": return STATE_ALARM_DISARMED + return STATE_ALARM_DISARMED @property def supported_features(self) -> int: """Return the list of supported features.""" + # pylint: disable=import-outside-toplevel try: from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_AWAY, @@ -233,6 +241,12 @@ def supported_features(self) -> int: @property def assumed_state(self) -> bool: + """Return assumed state. + + Returns + bool: Whether the state is assumed + + """ last_refresh_success = ( self.coordinator.data and self._guard_entity_id in self.coordinator.data ) diff --git a/custom_components/alexa_media/alexa_entity.py b/custom_components/alexa_media/alexa_entity.py index d86e835b..00fc70ab 100644 --- a/custom_components/alexa_media/alexa_entity.py +++ b/custom_components/alexa_media/alexa_entity.py @@ -10,7 +10,7 @@ import json import logging import re -from typing import Any, Dict, List, Optional, Text, Tuple, TypedDict, Union +from typing import Any, Optional, TypedDict, Union from alexapy import AlexaAPI, AlexaLogin, hide_serial from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -19,14 +19,14 @@ def has_capability( - appliance: Dict[Text, Any], interface_name: Text, property_name: Text + appliance: dict[str, Any], interface_name: str, property_name: str ) -> bool: """Determine if an appliance from the Alexa network details offers a particular interface with enough support that is worth adding to Home Assistant. Args: - appliance(Dict[Text, Any]): An appliance from a call to AlexaAPI.get_network_details - interface_name(Text): One of the interfaces documented by the Alexa Smart Home Skills API - property_name(Text): The property that matches the interface name. + appliance(dict[str, Any]): An appliance from a call to AlexaAPI.get_network_details + interface_name(str): One of the interfaces documented by the Alexa Smart Home Skills API + property_name(str): The property that matches the interface name. """ for cap in appliance["capabilities"]: @@ -42,7 +42,7 @@ def has_capability( return False -def is_hue_v1(appliance: Dict[Text, Any]) -> bool: +def is_hue_v1(appliance: dict[str, Any]) -> bool: """Determine if an appliance is managed via the Philips Hue v1 Hub. This check catches old Philips Hue bulbs and hubs, but critically, it also catches things pretending to be older @@ -51,7 +51,12 @@ def is_hue_v1(appliance: Dict[Text, Any]) -> bool: return appliance.get("manufacturerName") == "Royal Philips Electronics" -def is_local(appliance: Dict[Text, Any]) -> bool: +def is_skill(appliance: dict[str, Any]) -> bool: + namespace = appliance.get("driverIdentity", {}).get("namespace", "") + return namespace and namespace == "SKILL" + + +def is_local(appliance: dict[str, Any]) -> bool: """Test whether locally connected. This is mainly present to prevent loops with the official Alexa integration. @@ -66,8 +71,12 @@ def is_local(appliance: Dict[Text, Any]) -> bool: # This catches the Echo/AVS devices. connectedVia isn't reliable in this case. # Only the first appears to get that set. if "ALEXA_VOICE_ENABLED" in appliance.get("applianceTypes", []): - namespace = appliance.get("driverIdentity", {}).get("namespace", "") - return namespace and namespace != "SKILL" + return not is_skill(appliance) + + # Ledvance bulbs connected via bluetooth are hard to detect as locally connected + # There is probably a better way, but this works for now. + if appliance.get("manufacturerName") == "Ledvance": + return not is_skill(appliance) # Zigbee devices are guaranteed to be local and have a particular pattern of id zigbee_pattern = re.compile( @@ -76,21 +85,32 @@ def is_local(appliance: Dict[Text, Any]) -> bool: return zigbee_pattern.fullmatch(appliance.get("applianceId", "")) is not None -def is_alexa_guard(appliance: Dict[Text, Any]) -> bool: +def is_alexa_guard(appliance: dict[str, Any]) -> bool: """Is the given appliance the guard alarm system of an echo.""" return appliance["modelName"] == "REDROCK_GUARD_PANEL" and has_capability( appliance, "Alexa.SecurityPanelController", "armState" ) -def is_temperature_sensor(appliance: Dict[Text, Any]) -> bool: +def is_temperature_sensor(appliance: dict[str, Any]) -> bool: """Is the given appliance the temperature sensor of an Echo.""" return is_local(appliance) and has_capability( appliance, "Alexa.TemperatureSensor", "temperature" ) -def is_light(appliance: Dict[Text, Any]) -> bool: +# Checks if air quality sensor +def is_air_quality_sensor(appliance: dict[str, Any]) -> bool: + """Is the given appliance the Air Quality Sensor.""" + return ( + appliance["friendlyDescription"] == "Amazon Indoor Air Quality Monitor" + and "AIR_QUALITY_MONITOR" in appliance.get("applianceTypes", []) + and has_capability(appliance, "Alexa.TemperatureSensor", "temperature") + and has_capability(appliance, "Alexa.RangeController", "rangeValue") + ) + + +def is_light(appliance: dict[str, Any]) -> bool: """Is the given appliance a light controlled locally by an Echo.""" return ( is_local(appliance) @@ -98,7 +118,8 @@ def is_light(appliance: Dict[Text, Any]) -> bool: and has_capability(appliance, "Alexa.PowerController", "powerState") ) -def is_contact_sensor(appliance: Dict[Text, Any]) -> bool: + +def is_contact_sensor(appliance: dict[str, Any]) -> bool: """Is the given appliance a contact sensor controlled locally by an Echo.""" return ( is_local(appliance) @@ -106,7 +127,8 @@ def is_contact_sensor(appliance: Dict[Text, Any]) -> bool: and has_capability(appliance, "Alexa.ContactSensor", "detectionState") ) -def get_friendliest_name(appliance: Dict[Text, Any]) -> Text: + +def get_friendliest_name(appliance: dict[str, Any]) -> str: """Find the best friendly name. Alexa seems to store manual renames in aliases. Prefer that one.""" aliases = appliance.get("aliases", []) for alias in aliases: @@ -116,7 +138,7 @@ def get_friendliest_name(appliance: Dict[Text, Any]) -> Text: return appliance["friendlyName"] -def get_device_serial(appliance: Dict[Text, Any]) -> Optional[Text]: +def get_device_serial(appliance: dict[str, Any]) -> Optional[str]: """Find the device serial id if it is present.""" alexa_device_id_list = appliance.get("alexaDeviceIdentifierList", []) for alexa_device_id in alexa_device_id_list: @@ -128,9 +150,9 @@ def get_device_serial(appliance: Dict[Text, Any]) -> Optional[Text]: class AlexaEntity(TypedDict): """Class for Alexaentity.""" - id: Text - appliance_id: Text - name: Text + id: str + appliance_id: str + name: str is_hue_v1: bool @@ -145,7 +167,14 @@ class AlexaLightEntity(AlexaEntity): class AlexaTemperatureEntity(AlexaEntity): """Class for AlexaTemperatureEntity.""" - device_serial: Text + device_serial: str + + +class AlexaAirQualityEntity(AlexaEntity): + """Class for AlexaAirQualityEntity.""" + + device_serial: str + class AlexaBinaryEntity(AlexaEntity): """Class for AlexaBinaryEntity.""" @@ -156,19 +185,23 @@ class AlexaBinaryEntity(AlexaEntity): class AlexaEntities(TypedDict): """Class for holding entities.""" - light: List[AlexaLightEntity] - guard: List[AlexaEntity] - temperature: List[AlexaTemperatureEntity] - binary_sensor: List[AlexaBinaryEntity] + light: list[AlexaLightEntity] + guard: list[AlexaEntity] + temperature: list[AlexaTemperatureEntity] + air_quality: list[AlexaAirQualityEntity] + binary_sensor: list[AlexaBinaryEntity] -def parse_alexa_entities(network_details: Optional[Dict[Text, Any]]) -> AlexaEntities: +def parse_alexa_entities(network_details: Optional[dict[str, Any]]) -> AlexaEntities: + # pylint: disable=too-many-locals """Turn the network details into a list of useful entities with the important details extracted.""" lights = [] guards = [] temperature_sensors = [] + air_quality_sensors = [] contact_sensors = [] location_details = network_details["locationDetails"]["locationDetails"] + # pylint: disable=too-many-nested-blocks for location in location_details.values(): amazon_bridge_details = location["amazonBridgeDetails"]["amazonBridgeDetails"] for bridge in amazon_bridge_details.values(): @@ -188,6 +221,44 @@ def parse_alexa_entities(network_details: Optional[Dict[Text, Any]]) -> AlexaEnt serial if serial else appliance["entityId"] ) temperature_sensors.append(processed_appliance) + # Code for Amazon Smart Air Quality Monitor + elif is_air_quality_sensor(appliance): + serial = get_device_serial(appliance) + processed_appliance["device_serial"] = ( + serial if serial else appliance["entityId"] + ) + # create array of air quality sensors. We must store the instance id against + # the assetId so we know which sensors are which. + sensors = [] + if ( + appliance["friendlyDescription"] + == "Amazon Indoor Air Quality Monitor" + ): + for cap in appliance["capabilities"]: + instance = cap.get("instance") + if instance: + friendlyName = cap["resources"].get("friendlyNames") + for entry in friendlyName: + assetId = entry["value"].get("assetId") + if assetId and assetId.startswith( + "Alexa.AirQuality" + ): + unit = cap["configuration"]["unitOfMeasure"] + sensor = { + "sensorType": assetId, + "instance": instance, + "unit": unit, + } + sensors.append(sensor) + _LOGGER.debug( + "AIAQM sensor detected %s", sensor + ) + processed_appliance["sensors"] = sensors + + # Add as both temperature and air quality sensor + temperature_sensors.append(processed_appliance) + air_quality_sensors.append(processed_appliance) + elif is_light(appliance): processed_appliance["brightness"] = has_capability( appliance, "Alexa.BrightnessController", "brightness" @@ -209,22 +280,28 @@ def parse_alexa_entities(network_details: Optional[Dict[Text, Any]]) -> AlexaEnt else: _LOGGER.debug("Found unsupported device %s", appliance) - return {"light": lights, "guard": guards, "temperature": temperature_sensors, "binary_sensor": contact_sensors} + return { + "light": lights, + "guard": guards, + "temperature": temperature_sensors, + "air_quality": air_quality_sensors, + "binary_sensor": contact_sensors, + } class AlexaCapabilityState(TypedDict): """Class for AlexaCapabilityState.""" - name: Text - namespace: Text - value: Union[int, Text, TypedDict] + name: str + namespace: str + value: Union[int, str, TypedDict] -AlexaEntityData = Dict[Text, List[AlexaCapabilityState]] +AlexaEntityData = dict[str, list[AlexaCapabilityState]] async def get_entity_data( - login_obj: AlexaLogin, entity_ids: List[Text] + login_obj: AlexaLogin, entity_ids: list[str] ) -> AlexaEntityData: """Get and process the entity data into a more usable format.""" @@ -244,8 +321,8 @@ async def get_entity_data( def parse_temperature_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text -) -> Optional[Text]: + coordinator: DataUpdateCoordinator, entity_id: str +) -> Optional[str]: """Get the temperature of an entity from the coordinator data.""" value = parse_value_from_coordinator( coordinator, entity_id, "Alexa.TemperatureSensor", "temperature" @@ -253,8 +330,22 @@ def parse_temperature_from_coordinator( return value.get("value") if value and "value" in value else None +def parse_air_quality_from_coordinator( + coordinator: DataUpdateCoordinator, entity_id: str, instance_id: str +) -> Optional[str]: + """Get the air quality of an entity from the coordinator data.""" + value = parse_value_from_coordinator( + coordinator, + entity_id, + "Alexa.RangeController", + "rangeValue", + instance=instance_id, + ) + return value + + def parse_brightness_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text, since: datetime + coordinator: DataUpdateCoordinator, entity_id: str, since: datetime ) -> Optional[int]: """Get the brightness in the range 0-100.""" return parse_value_from_coordinator( @@ -263,9 +354,9 @@ def parse_brightness_from_coordinator( def parse_color_temp_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text, since: datetime + coordinator: DataUpdateCoordinator, entity_id: str, since: datetime ) -> Optional[int]: - """Get the color temperature in kelvin""" + """Get the color temperature in kelvin.""" return parse_value_from_coordinator( coordinator, entity_id, @@ -276,9 +367,9 @@ def parse_color_temp_from_coordinator( def parse_color_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text, since: datetime -) -> Optional[Tuple[float, float, float]]: - """Get the color as a tuple of (hue, saturation, brightness)""" + coordinator: DataUpdateCoordinator, entity_id: str, since: datetime +) -> Optional[tuple[float, float, float]]: + """Get the color as a tuple of (hue, saturation, brightness).""" value = parse_value_from_coordinator( coordinator, entity_id, "Alexa.ColorController", "color", since ) @@ -290,8 +381,8 @@ def parse_color_from_coordinator( def parse_power_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text, since: datetime -) -> Optional[Text]: + coordinator: DataUpdateCoordinator, entity_id: str, since: datetime +) -> Optional[str]: """Get the power state of the entity.""" return parse_value_from_coordinator( coordinator, entity_id, "Alexa.PowerController", "powerState", since @@ -299,8 +390,8 @@ def parse_power_from_coordinator( def parse_guard_state_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text -) -> Optional[Text]: + coordinator: DataUpdateCoordinator, entity_id: str +) -> Optional[str]: """Get the guard state from the coordinator data.""" return parse_value_from_coordinator( coordinator, entity_id, "Alexa.SecurityPanelController", "armState" @@ -308,19 +399,21 @@ def parse_guard_state_from_coordinator( def parse_detection_state_from_coordinator( - coordinator: DataUpdateCoordinator, entity_id: Text + coordinator: DataUpdateCoordinator, entity_id: str ) -> Optional[bool]: """Get the detection state from the coordinator data.""" return parse_value_from_coordinator( coordinator, entity_id, "Alexa.ContactSensor", "detectionState" ) + def parse_value_from_coordinator( coordinator: DataUpdateCoordinator, - entity_id: Text, - namespace: Text, - name: Text, + entity_id: str, + namespace: str, + name: str, since: Optional[datetime] = None, + instance: str = None, ) -> Any: """Parse out values from coordinator for Alexa Entities.""" if coordinator.data and entity_id in coordinator.data: @@ -328,22 +421,22 @@ def parse_value_from_coordinator( if ( cap_state.get("namespace") == namespace and cap_state.get("name") == name + and (cap_state.get("instance") == instance or instance is None) ): if is_cap_state_still_acceptable(cap_state, since): return cap_state.get("value") - else: - _LOGGER.debug( - "Coordinator data for %s is too old to be returned.", - hide_serial(entity_id), - ) - return None + _LOGGER.debug( + "Coordinator data for %s is too old to be returned.", + hide_serial(entity_id), + ) + return None else: _LOGGER.debug("Coordinator has no data for %s", hide_serial(entity_id)) return None def is_cap_state_still_acceptable( - cap_state: Dict[Text, Any], since: Optional[datetime] + cap_state: dict[str, Any], since: Optional[datetime] ) -> bool: """Determine if a particular capability state is still usable given its age.""" if since is not None: diff --git a/custom_components/alexa_media/alexa_media.py b/custom_components/alexa_media/alexa_media.py index 16b9c625..7c96ac96 100644 --- a/custom_components/alexa_media/alexa_media.py +++ b/custom_components/alexa_media/alexa_media.py @@ -8,7 +8,6 @@ """ import logging -from typing import Dict, Text # noqa pylint: disable=unused-import from alexapy import AlexaAPI, hide_email @@ -21,7 +20,6 @@ class AlexaMedia: """Implementation of Alexa Media Base object.""" def __init__(self, device, login) -> None: - # pylint: disable=unexpected-keyword-arg """Initialize the Alexa device.""" # Class info diff --git a/custom_components/alexa_media/binary_sensor.py b/custom_components/alexa_media/binary_sensor.py index 45e545b5..73296b7f 100644 --- a/custom_components/alexa_media/binary_sensor.py +++ b/custom_components/alexa_media/binary_sensor.py @@ -8,13 +8,13 @@ """ import logging -from typing import List # noqa pylint: disable=unused-import from alexapy import hide_serial from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ( @@ -30,23 +30,30 @@ _LOGGER = logging.getLogger(__name__) + async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa sensor platform.""" - devices: List[BinarySensorEntity] = [] - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + devices: list[BinarySensorEntity] = [] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) coordinator = account_dict["coordinator"] binary_entities = account_dict.get("devices", {}).get("binary_sensor", []) if binary_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY): - for be in binary_entities: + for binary_entity in binary_entities: _LOGGER.debug( "Creating entity %s for a binary_sensor with name %s", - hide_serial(be["id"]), - be["name"], + hide_serial(binary_entity["id"]), + binary_entity["name"], ) - contact_sensor = AlexaContact(coordinator, be) + contact_sensor = AlexaContact(coordinator, binary_entity) account_dict["entities"]["binary_sensor"].append(contact_sensor) devices.append(contact_sensor) @@ -75,34 +82,46 @@ async def async_unload_entry(hass, entry) -> bool: await binary_sensor.async_remove() return True + class AlexaContact(CoordinatorEntity, BinarySensorEntity): """A contact sensor controlled by an Echo.""" _attr_device_class = BinarySensorDeviceClass.DOOR - def __init__(self, coordinator, details): + def __init__(self, coordinator: CoordinatorEntity, details: dict): + """Initialize alexa contact sensor. + + Args + coordinator (CoordinatorEntity): Coordinator + details (dict): Details dictionary + + """ super().__init__(coordinator) self.alexa_entity_id = details["id"] self._name = details["name"] @property def name(self): + """Return name.""" return self._name @property def unique_id(self): + """Return unique id.""" return self.alexa_entity_id @property def is_on(self): + """Return whether on.""" detection = parse_detection_state_from_coordinator( self.coordinator, self.alexa_entity_id ) - return detection == 'DETECTED' if detection is not None else None + return detection == "DETECTED" if detection is not None else None @property def assumed_state(self) -> bool: + """Return assumed state.""" last_refresh_success = ( self.coordinator.data and self.alexa_entity_id in self.coordinator.data ) diff --git a/custom_components/alexa_media/config_flow.py b/custom_components/alexa_media/config_flow.py index cde4da2f..585776cc 100644 --- a/custom_components/alexa_media/config_flow.py +++ b/custom_components/alexa_media/config_flow.py @@ -12,7 +12,7 @@ from datetime import timedelta from functools import reduce import logging -from typing import Any, Dict, List, Optional, Text +from typing import Any, Optional from aiohttp import ClientConnectionError, ClientSession, InvalidURL, web, web_response from aiohttp.web_exceptions import HTTPBadRequest @@ -31,7 +31,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import UnknownFlow from homeassistant.exceptions import Unauthorized -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.util import slugify import httpx @@ -89,7 +88,7 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow): def _update_ord_dict(self, old_dict: OrderedDict, new_dict: dict) -> OrderedDict: result: OrderedDict = OrderedDict() - for k, v in old_dict.items(): + for k, v in old_dict.items(): # pylint: disable=invalid-name for key, value in new_dict.items(): if k == key: result.update([(key, value)]) @@ -133,6 +132,7 @@ async def async_step_import(self, import_config): return await self.async_step_user_legacy(import_config) async def async_step_user(self, user_input=None): + # pylint: disable=too-many-branches """Provide a proxy for login.""" self._save_user_input_to_config(user_input=user_input) try: @@ -263,10 +263,10 @@ async def async_step_user(self, user_input=None): try: async with session.get(hass_url) as resp: hass_url_valid = resp.status == 200 - except (ClientConnectionError) as err: + except ClientConnectionError as err: hass_url_valid = False hass_url_error = str(err) - except (InvalidURL) as err: + except InvalidURL as err: hass_url_valid = False hass_url_error = str(err.__cause__) if not hass_url_valid: @@ -305,6 +305,7 @@ async def async_step_user(self, user_input=None): async def async_step_start_proxy(self, user_input=None): """Start proxy for login.""" + # pylint: disable=unused-argument _LOGGER.debug( "Starting proxy for %s - %s", hide_email(self.login.email), @@ -340,10 +341,11 @@ async def async_step_start_proxy(self, user_input=None): proxy_url = self.proxy.access_url().with_query( {"config_flow_id": self.flow_id, "callback_url": str(callback_url)} ) - self.login._session.cookie_jar.clear() + self.login._session.cookie_jar.clear() # pylint: disable=protected-access return self.async_external_step(step_id="check_proxy", url=str(proxy_url)) async def async_step_check_proxy(self, user_input=None): + # pylint: disable=unused-argument """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", @@ -354,6 +356,7 @@ async def async_step_check_proxy(self, user_input=None): return self.async_external_step_done(next_step_id="finish_proxy") async def async_step_finish_proxy(self, user_input=None): + # pylint: disable=unused-argument """Finish auth.""" if await self.login.test_loggedin(): await self.login.finalize_login() @@ -462,7 +465,7 @@ async def async_step_user_legacy(self, user_input=None): errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) - except BaseException as ex: # pylyint: disable=broad-except + except BaseException as ex: # pylint: disable=broad-except _LOGGER.warning("Unknown error: %s", ex) if self.config[CONF_DEBUG]: raise @@ -555,7 +558,6 @@ async def async_step_reauth(self, user_input=None): return await self.async_step_user_legacy(self.config) async def _test_login(self): - # pylint: disable=too-many-statements, too-many-return-statements login = self.login email = login.email _LOGGER.debug("Testing login status: %s", login.status) @@ -632,10 +634,10 @@ async def _test_login(self): self.automatic_steps += 1 await sleep(5) if generated_securitycode: - return await self.async_step_twofactor( + return await self.async_step_user_legacy( user_input={CONF_SECURITYCODE: generated_securitycode} ) - return await self.async_step_twofactor( + return await self.async_step_user_legacy( user_input={CONF_SECURITYCODE: self.securitycode} ) if login.status and (login.status.get("login_failed")): @@ -675,6 +677,7 @@ async def _test_login(self): ) def _save_user_input_to_config(self, user_input=None) -> None: + # pylint: disable=too-many-branches """Process user_input to save to self.config. user_input can be a dictionary of strings or an internally @@ -833,11 +836,11 @@ class AlexaMediaAuthorizationProxyView(HomeAssistantView): """Handle proxy connections.""" url: str = AUTH_PROXY_PATH - extra_urls: List[str] = [f"{AUTH_PROXY_PATH}/{{tail:.*}}"] + extra_urls: list[str] = [f"{AUTH_PROXY_PATH}/{{tail:.*}}"] name: str = AUTH_PROXY_NAME requires_auth: bool = False handler: web.RequestHandler = None - known_ips: Dict[str, datetime.datetime] = {} + known_ips: dict[str, datetime.datetime] = {} auth_seconds: int = 300 def __init__(self, handler: web.RequestHandler): @@ -885,7 +888,9 @@ async def wrapped(request, **kwargs): _LOGGER.warning("Detected Connection error: %s", ex) return web_response.Response( headers={"content-type": "text/html"}, - text=f"Connection Error! Please try refreshing. If this persists, please report this error to here:
{ex}
", + text="Connection Error! Please try refreshing. " + + "If this persists, please report this error to " + + f"here:
{ex}
", ) return wrapped diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 8a8df879..c8bf4237 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -8,7 +8,13 @@ """ from datetime import timedelta -__version__ = "4.4.0" +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, +) + +__version__ = "4.6.5" PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" ISSUE_URL = f"{PROJECT_URL}issues" NOTIFY_URL = f"{PROJECT_URL}wiki/Configuration%3A-Notification-Component#use-the-notifyalexa_media-service" @@ -30,7 +36,7 @@ "sensor", "alarm_control_panel", "light", - "binary_sensor" + "binary_sensor", ] HTTP_COOKIE_HEADER = "# HTTP Cookie File" @@ -98,19 +104,30 @@ ATTR_MESSAGE = "message" ATTR_EMAIL = "email" ATTR_NUM_ENTRIES = "entries" -STARTUP = """ +STARTUP = f""" ------------------------------------------------------------------- -{} -Version: {} +{DOMAIN} +Version: {__version__} This is a custom component If you have any issues with this you need to open an issue here: -{} +{ISSUE_URL} ------------------------------------------------------------------- -""".format( - DOMAIN, __version__, ISSUE_URL -) +""" AUTH_CALLBACK_PATH = "/auth/alexamedia/callback" AUTH_CALLBACK_NAME = "auth:alexamedia:callback" AUTH_PROXY_PATH = "/auth/alexamedia/proxy" AUTH_PROXY_NAME = "auth:alexamedia:proxy" + +ALEXA_UNIT_CONVERSION = { + "Alexa.Unit.Percent": PERCENTAGE, + "Alexa.Unit.PartsPerMillion": CONCENTRATION_PARTS_PER_MILLION, + "Alexa.Unit.Density.MicroGramsPerCubicMeter": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, +} + +ALEXA_ICON_CONVERSION = { + "Alexa.AirQuality.CarbonMonoxide": "mdi:molecule-co", + "Alexa.AirQuality.Humidity": "mdi:water-percent", + "Alexa.AirQuality.IndoorAirQuality": "mdi:numeric", +} +ALEXA_ICON_DEFAULT = "mdi:molecule" diff --git a/custom_components/alexa_media/helpers.py b/custom_components/alexa_media/helpers.py index 9964178a..69434af8 100644 --- a/custom_components/alexa_media/helpers.py +++ b/custom_components/alexa_media/helpers.py @@ -6,14 +6,16 @@ For more details about this platform, please refer to the documentation at https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ +import asyncio +import functools import hashlib import logging -from typing import Any, Callable, Dict, List, Optional, Text +from typing import Any, Callable, Optional from alexapy import AlexapyLoginCloseRequested, AlexapyLoginError, hide_email from alexapy.alexalogin import AlexaLogin from homeassistant.const import CONF_EMAIL, CONF_URL -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConditionErrorMessage from homeassistant.helpers.entity_component import EntityComponent import wrapt @@ -23,11 +25,11 @@ async def add_devices( - account: Text, - devices: List[EntityComponent], + account: str, + devices: list[EntityComponent], add_devices_callback: Callable, - include_filter: Optional[List[Text]] = None, - exclude_filter: Optional[List[Text]] = None, + include_filter: Optional[list[str]] = None, + exclude_filter: Optional[list[str]] = None, ) -> bool: """Add devices using add_devices_callback.""" include_filter = [] or include_filter @@ -49,8 +51,8 @@ async def add_devices( try: add_devices_callback(devices, False) return True - except HomeAssistantError as exception_: - message = exception_.message # type: str + except ConditionErrorMessage as exception_: + message: str = exception_.message if message.startswith("Entity id already exists"): _LOGGER.debug("%s: Device already added: %s", account, message) else: @@ -84,6 +86,7 @@ def retry_async( The delay in seconds between retries. catch_exceptions : bool Whether exceptions should be caught and treated as failures or thrown. + Returns ------- def @@ -92,9 +95,6 @@ def retry_async( """ def wrap(func) -> Callable: - import asyncio - import functools - @functools.wraps(func) async def wrapper(*args, **kwargs) -> Any: _LOGGER.debug( @@ -110,7 +110,7 @@ async def wrapper(*args, **kwargs) -> Any: next_try: int = 0 while not result and retries < limit: if retries != 0: - next_try = delay * 2 ** retries + next_try = delay * 2**retries await asyncio.sleep(next_try) retries += 1 try: @@ -168,7 +168,7 @@ async def _catch_login_errors(func, instance, args, kwargs) -> Any: # _LOGGER.debug("Func %s instance %s %s %s", func, instance, args, kwargs) if instance: if hasattr(instance, "_login"): - login = instance._login + login = instance._login # pylint: disable=protected-access hass = instance.hass else: for arg in all_args: @@ -222,8 +222,8 @@ def report_relogin_required(hass, login, email) -> bool: return False -def _existing_serials(hass, login_obj) -> List: - email: Text = login_obj.email +def _existing_serials(hass, login_obj) -> list: + email: str = login_obj.email existing_serials = ( list( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][ @@ -250,13 +250,13 @@ def _existing_serials(hass, login_obj) -> List: return existing_serials -async def calculate_uuid(hass, email: Text, url: Text) -> dict: +async def calculate_uuid(hass, email: str, url: str) -> dict: """Return uuid and index of email/url. Args hass (bool): Hass entity - url (Text): url for account - email (Text): email for account + url (str): url for account + email (str): email for account Returns dict: dictionary with uuid and index @@ -285,33 +285,25 @@ async def calculate_uuid(hass, email: Text, url: Text) -> dict: def alarm_just_dismissed( - alarm: Dict[Text, Any], - previous_status: Optional[Text], - previous_version: Optional[Text], + alarm: dict[str, Any], + previous_status: Optional[str], + previous_version: Optional[str], ) -> bool: """Given the previous state of an alarm, determine if it has just been dismissed.""" - if previous_status not in ("SNOOZED", "ON"): + if ( + previous_status not in ("SNOOZED", "ON") # The alarm had to be in a status that supported being dismissed - return False - - if previous_version is None: + or previous_version is None # The alarm was probably just created - return False - - if not alarm: + or not alarm # The alarm that was probably just deleted. - return False - - if alarm.get("status") not in ("OFF", "ON"): + or alarm.get("status") not in ("OFF", "ON") # A dismissed alarm is guaranteed to be turned off(one-off alarm) or left on(recurring alarm) - return False - - if previous_version == alarm.get("version"): + or previous_version == alarm.get("version") # A dismissal always has a changed version. - return False - - if int(alarm.get("version", "0")) > 1 + int(previous_version): + or int(alarm.get("version", "0")) > 1 + int(previous_version) + ): # This is an absurd thing to check, but it solves many, many edge cases. # Experimentally, when an alarm is dismissed, the version always increases by 1 # When an alarm is edited either via app or voice, its version always increases by 2+ diff --git a/custom_components/alexa_media/light.py b/custom_components/alexa_media/light.py index 369cf071..b8bd529c 100644 --- a/custom_components/alexa_media/light.py +++ b/custom_components/alexa_media/light.py @@ -9,13 +9,7 @@ import datetime import logging from math import sqrt -from typing import ( # noqa pylint: disable=unused-import - Callable, - List, - Optional, - Text, - Tuple, -) +from typing import Optional from alexapy import AlexaAPI, hide_serial from homeassistant.components.light import ( @@ -27,6 +21,7 @@ SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.exceptions import ConfigEntryNotReady try: from homeassistant.components.light import ( @@ -74,8 +69,14 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa sensor platform.""" - devices: List[LightEntity] = [] - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + devices: list[LightEntity] = [] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) @@ -85,20 +86,20 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf ) light_entities = account_dict.get("devices", {}).get("light", []) if light_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY): - for le in light_entities: - if not (le["is_hue_v1"] and hue_emulated_enabled): + for light_entity in light_entities: + if not (light_entity["is_hue_v1"] and hue_emulated_enabled): _LOGGER.debug( "Creating entity %s for a light with name %s", - hide_serial(le["id"]), - le["name"], + hide_serial(light_entity["id"]), + light_entity["name"], ) - light = AlexaLight(coordinator, account_dict["login_obj"], le) + light = AlexaLight(coordinator, account_dict["login_obj"], light_entity) account_dict["entities"]["light"].append(light) devices.append(light) else: _LOGGER.debug( "Light '%s' has not been added because it may originate from emulated_hue", - le["name"], + light_entity["name"], ) return await add_devices( @@ -127,23 +128,24 @@ async def async_unload_entry(hass, entry) -> bool: return True -def color_modes(details): +def color_modes(details) -> list: + """Return list of color modes.""" if details["color"] and details["color_temperature"]: return [COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP] - elif details["color"]: + if details["color"]: return [COLOR_MODE_HS] - elif details["color_temperature"]: + if details["color_temperature"]: return [COLOR_MODE_COLOR_TEMP] - elif details["brightness"]: + if details["brightness"]: return [COLOR_MODE_BRIGHTNESS] - else: - return [COLOR_MODE_ONOFF] + return [COLOR_MODE_ONOFF] class AlexaLight(CoordinatorEntity, LightEntity): """A light controlled by an Echo.""" def __init__(self, coordinator, login, details): + """Initialize alexa light entity.""" super().__init__(coordinator) self.alexa_entity_id = details["id"] self._name = details["name"] @@ -163,14 +165,17 @@ def __init__(self, coordinator, login, details): @property def name(self): + """Return name.""" return self._name @property def unique_id(self): + """Return unique id.""" return self.alexa_entity_id @property def supported_features(self): + """Return supported features.""" # The HA documentation marks every single feature that Alexa lights can support as deprecated. # The new alternative is the supported_color_modes and color_mode properties(HA 2021.4) # This SHOULD just need to return 0 according to the light entity docs. @@ -178,95 +183,99 @@ def supported_features(self): # So, continue to provide a backwards compatible method here until HA is fixed and the min HA version is raised. if COLOR_MODE_BRIGHTNESS in self._color_modes: return SUPPORT_BRIGHTNESS - elif ( + if ( COLOR_MODE_HS in self._color_modes and COLOR_MODE_COLOR_TEMP in self._color_modes ): return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP - elif COLOR_MODE_HS in self._color_modes: + if COLOR_MODE_HS in self._color_modes: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - elif COLOR_MODE_COLOR_TEMP in self._color_modes: + if COLOR_MODE_COLOR_TEMP in self._color_modes: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - else: - - return 0 + return 0 @property def color_mode(self): + """Return color mode.""" if ( COLOR_MODE_HS in self._color_modes and COLOR_MODE_COLOR_TEMP in self._color_modes ): - hs = self.hs_color - if hs is None or (hs[0] == 0 and hs[1] == 0): + hs_color = self.hs_color + if hs_color is None or (hs_color[0] == 0 and hs_color[1] == 0): # (0,0) is white. When white, color temp is the better plan. return COLOR_MODE_COLOR_TEMP - else: - return COLOR_MODE_HS - else: - return self._color_modes[0] + return COLOR_MODE_HS + return self._color_modes[0] @property def supported_color_modes(self): + """Return supported color modes.""" return self._color_modes @property def is_on(self): + """Return whether on.""" power = parse_power_from_coordinator( self.coordinator, self.alexa_entity_id, self._requested_state_at ) if power is None: return self._requested_power if self._requested_power is not None else False - else: - return power == "ON" + return power == "ON" @property def brightness(self): + """Return brightness.""" bright = parse_brightness_from_coordinator( self.coordinator, self.alexa_entity_id, self._requested_state_at ) if bright is None: return self._requested_ha_brightness - else: - return alexa_brightness_to_ha(bright) + return alexa_brightness_to_ha(bright) @property def min_mireds(self): + """Return min mireds.""" return 143 @property def max_mireds(self): + """Return max mireds.""" return 454 @property def color_temp(self): + """Return color temperature.""" kelvin = parse_color_temp_from_coordinator( self.coordinator, self.alexa_entity_id, self._requested_state_at ) if kelvin is None: return self._requested_mired - else: - return alexa_kelvin_to_mired(kelvin) + return alexa_kelvin_to_mired(kelvin) @property def hs_color(self): + """Return hs color.""" hsb = parse_color_from_coordinator( self.coordinator, self.alexa_entity_id, self._requested_state_at ) if hsb is None: return self._requested_hs - else: - adjusted_hs, color_name = hsb_to_alexa_color(hsb) - return adjusted_hs + ( + adjusted_hs, + color_name, # pylint:disable=unused-variable + ) = hsb_to_alexa_color(hsb) + return adjusted_hs @property def assumed_state(self) -> bool: + """Return whether state is assumed.""" last_refresh_success = ( self.coordinator.data and self.alexa_entity_id in self.coordinator.data ) return not last_refresh_success - async def _set_state(self, power_on, brightness=None, mired=None, hs=None): + async def _set_state(self, power_on, brightness=None, mired=None, hs_color=None): # This is "rounding" on mired to the closest value Alexa is willing to acknowledge the existence of. # The alternative implementation would be to use effects instead. # That is far more non-standard, and would lock users out of things like the Flux integration. @@ -278,7 +287,7 @@ async def _set_state(self, power_on, brightness=None, mired=None, hs=None): # This is "rounding" on HS color to closest value Alexa supports. # The alexa color list is short, but covers a pretty broad spectrum. # Like for mired above, this sounds bad but works ok in practice. - adjusted_hs, color_name = hs_to_alexa_color(hs) + adjusted_hs, color_name = hs_to_alexa_color(hs_color) else: # If a color temperature is being set, it is not possible to also adjust the color. adjusted_hs = None @@ -317,35 +326,36 @@ async def _set_state(self, power_on, brightness=None, mired=None, hs=None): self.async_write_ha_state() async def async_turn_on(self, **kwargs): + """Turn on.""" brightness = None mired = None - hs = None + hs_color = None if COLOR_MODE_ONOFF not in self._color_modes and ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] if COLOR_MODE_COLOR_TEMP in self._color_modes and ATTR_COLOR_TEMP in kwargs: mired = kwargs[ATTR_COLOR_TEMP] if COLOR_MODE_HS in self._color_modes and ATTR_HS_COLOR in kwargs: - hs = kwargs[ATTR_HS_COLOR] - await self._set_state(True, brightness, mired, hs) + hs_color = kwargs[ATTR_HS_COLOR] + await self._set_state(True, brightness, mired, hs_color) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): # pylint:disable=unused-argument + """Turn off.""" await self._set_state(False) -def mired_to_alexa(mired: Optional[float]) -> Tuple[Optional[float], Optional[Text]]: +def mired_to_alexa(mired: Optional[float]) -> tuple[Optional[float], Optional[str]]: """Convert a given color temperature in mired to the closest available value that Alexa has support for.""" if mired is None: return None, None - elif mired <= 162.5: + if mired <= 162.5: return 143, "cool_white" - elif mired <= 216: + if mired <= 216: return 182, "daylight_white" - elif mired <= 310: + if mired <= 310: return 250, "white" - elif mired <= 412: + if mired <= 412: return 370, "soft_white" - else: - return 454, "warm_white" + return 454, "warm_white" def alexa_kelvin_to_mired(kelvin: float) -> float: @@ -354,11 +364,13 @@ def alexa_kelvin_to_mired(kelvin: float) -> float: return mired_to_alexa(raw_mired)[0] -def ha_brightness_to_alexa(ha: Optional[float]) -> Optional[float]: - return (ha / 255 * 100) if ha is not None else None +def ha_brightness_to_alexa(ha_brightness: Optional[float]) -> Optional[float]: + """Convert HA brightness to alexa brightness.""" + return (ha_brightness / 255 * 100) if ha_brightness is not None else None def alexa_brightness_to_ha(alexa: Optional[float]) -> Optional[float]: + """Convert Alexa brightness to HA brightness.""" return (alexa / 100 * 255) if alexa is not None else None @@ -508,8 +520,9 @@ def alexa_brightness_to_ha(alexa: Optional[float]) -> Optional[float]: } -def red_mean(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float: +def red_mean(color1: tuple[int, int, int], color2: tuple[int, int, int]) -> float: """Get an approximate 'distance' between two colors using red mean. + Wikipedia says this method is "one of the better low-cost approximations". """ r_avg = (color2[0] + color1[0]) / 2 @@ -522,14 +535,14 @@ def red_mean(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> floa return sqrt(r_term + g_term + b_term) -def alexa_color_name_to_rgb(color_name: Text) -> Tuple[int, int, int]: - """Convert an alexa color name into RGB""" +def alexa_color_name_to_rgb(color_name: str) -> tuple[int, int, int]: + """Convert an alexa color name into RGB.""" return color_name_to_rgb(color_name.replace("_", "")) def rgb_to_alexa_color( - rgb: Tuple[int, int, int] -) -> Tuple[Optional[Tuple[float, float]], Optional[Text]]: + rgb: tuple[int, int, int] +) -> tuple[Optional[tuple[float, float]], Optional[str]]: """Convert a given RGB value into the closest Alexa color.""" (name, alexa_rgb) = min( ALEXA_COLORS.items(), @@ -540,18 +553,18 @@ def rgb_to_alexa_color( def hs_to_alexa_color( - hs: Optional[Tuple[float, float]] -) -> Tuple[Optional[Tuple[float, float]], Optional[Text]]: + hs_color: Optional[tuple[float, float]] +) -> tuple[Optional[tuple[float, float]], Optional[str]]: """Convert a given hue/saturation value into the closest Alexa color.""" - if hs is None: + if hs_color is None: return None, None - hue, saturation = hs + hue, saturation = hs_color return rgb_to_alexa_color(color_hs_to_RGB(hue, saturation)) def hsb_to_alexa_color( - hsb: Optional[Tuple[float, float, float]] -) -> Tuple[Optional[Tuple[float, float]], Optional[Text]]: + hsb: Optional[tuple[float, float, float]] +) -> tuple[Optional[tuple[float, float]], Optional[str]]: """Convert a given hue/saturation/brightness value into the closest Alexa color.""" if hsb is None: return None, None diff --git a/custom_components/alexa_media/manifest.json b/custom_components/alexa_media/manifest.json index 59146a99..7700856b 100644 --- a/custom_components/alexa_media/manifest.json +++ b/custom_components/alexa_media/manifest.json @@ -1,13 +1,13 @@ { "domain": "alexa_media", "name": "Alexa Media Player", - "version": "4.4.0", + "codeowners": ["@alandtse", "@keatontaylor"], "config_flow": true, - "documentation": "https://github.com/custom-components/alexa_media_player/wiki", - "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", "dependencies": ["persistent_notification", "http"], - "codeowners": ["@alandtse", "@keatontaylor"], - "requirements": ["alexapy==1.26.4", "packaging>=20.3", "wrapt>=1.12.1"], + "documentation": "https://github.com/custom-components/alexa_media_player/wiki", "iot_class": "cloud_polling", - "loggers": ["alexapy", "authcaptureproxy"] + "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", + "loggers": ["alexapy", "authcaptureproxy"], + "requirements": ["alexapy==1.26.8", "packaging>=20.3", "wrapt>=1.12.1"], + "version": "4.6.5" } diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index 589fb1d7..31b42a13 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -9,9 +9,8 @@ import asyncio import logging import re -from typing import List, Optional, Text # noqa pylint: disable=unused-import +from typing import List, Optional -from alexapy import AlexaAPI from homeassistant import util from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, @@ -90,10 +89,15 @@ # @retry_async(limit=5, delay=2, catch_exceptions=True) async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): - # pylint: disable=unused-argument """Set up the Alexa media player platform.""" devices = [] # type: List[AlexaClient] - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] entry_setup = len(account_dict["entities"]["media_player"]) alexa_client = None @@ -188,7 +192,6 @@ class AlexaClient(MediaPlayerDevice, AlexaMedia): """Representation of a Alexa device.""" def __init__(self, device, login, second_account_index=0): - # pylint: disable=unused-argument """Initialize the Alexa device.""" super().__init__(self, login) @@ -283,6 +286,7 @@ async def async_will_remove_from_hass(self): pass # ignore missing listener async def _handle_event(self, event): + # pylint: disable=too-many-branches,too-many-statements """Handle events. This will update last_called and player_state events. @@ -378,7 +382,6 @@ async def _refresh_if_no_audiopush(already_refreshed=False): and self._last_called_timestamp != event["last_called_change"]["timestamp"] ): - _LOGGER.debug( "%s: %s is last_called: %s", hide_email(self._login.email), @@ -520,6 +523,7 @@ def _set_authentication_details(self, auth): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @_catch_login_errors async def refresh(self, device=None, skip_api: bool = False): + # pylint: disable=too-many-branches,too-many-statements """Refresh device data. This is a per device refresh and for many Alexa devices can result in @@ -594,7 +598,7 @@ async def refresh(self, device=None, skip_api: bool = False): if playing_parents: if len(playing_parents) > 1: _LOGGER.warning( - "Found multiple playing parents " "please file an issue" + "Found multiple playing parents please file an issue" ) parent = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self._login.email @@ -1312,7 +1316,7 @@ async def async_send_dropin_notification(self, message, **kwargs): @_catch_login_errors async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): - # pylint: disable=unused-argument + # pylint: disable=unused-argument,too-many-branches """Send the play_media command to the media player.""" queue_delay = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email][ "options" diff --git a/custom_components/alexa_media/notify.py b/custom_components/alexa_media/notify.py index 17186bdc..d220b9a5 100644 --- a/custom_components/alexa_media/notify.py +++ b/custom_components/alexa_media/notify.py @@ -10,6 +10,7 @@ import json import logging +from alexapy.helpers import hide_email, hide_serial from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -18,18 +19,15 @@ SERVICE_NOTIFY, BaseNotificationService, ) +from homeassistant.const import CONF_EMAIL import voluptuous as vol -from custom_components.alexa_media.const import NOTIFY_URL - -from . import ( - CONF_EMAIL, +from .const import ( CONF_QUEUE_DELAY, DATA_ALEXAMEDIA, DEFAULT_QUEUE_DELAY, DOMAIN, - hide_email, - hide_serial, + NOTIFY_URL, ) from .helpers import retry_async @@ -151,6 +149,8 @@ def targets(self): return devices last_called_entity = None for _, entity in account_dict["entities"]["media_player"].items(): + if entity is None or entity.entity_id is None: + continue entity_name = (entity.entity_id).split(".")[1] devices[entity_name] = entity.unique_id if self.last_called and entity.extra_state_attributes.get( @@ -205,6 +205,7 @@ def devices(self): return devices async def async_send_message(self, message="", **kwargs): + # pylint: disable=too-many-branches """Send a message to a Alexa device.""" _LOGGER.debug("Message: %s, kwargs: %s", message, kwargs) _LOGGER.debug("Target type: %s", type(kwargs.get(ATTR_TARGET))) @@ -268,7 +269,7 @@ async def async_send_message(self, message="", **kwargs): # ) if alexa.device_serial_number in targets and alexa.available: _LOGGER.debug( - ("%s: Announce by %s to " "targets: %s: %s"), + ("%s: Announce by %s to targets: %s: %s"), hide_email(account), alexa, list(map(hide_serial, targets)), @@ -322,7 +323,7 @@ async def async_send_message(self, message="", **kwargs): errormessage = ( f"{account}: Data value `type={data_type}` is not implemented. " f"See {NOTIFY_URL}" - ) + ) _LOGGER.debug(errormessage) raise vol.Invalid(errormessage) await asyncio.gather(*tasks) diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py index cdfd195c..c1910ec3 100644 --- a/custom_components/alexa_media/sensor.py +++ b/custom_components/alexa_media/sensor.py @@ -7,16 +7,17 @@ https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ import datetime +import json import logging -from typing import Callable, List, Optional, Text # noqa pylint: disable=unused-import - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_TIMESTAMP, - STATE_UNAVAILABLE, - TEMP_CELSIUS, - __version__ as HA_VERSION, +from typing import Callable, Optional + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.const import UnitOfTemperature, __version__ as HA_VERSION +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, NoEntitySpecifiedError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time @@ -34,8 +35,14 @@ hide_email, hide_serial, ) -from .alexa_entity import parse_temperature_from_coordinator +from .alexa_entity import ( + parse_air_quality_from_coordinator, + parse_temperature_from_coordinator, +) from .const import ( + ALEXA_ICON_CONVERSION, + ALEXA_ICON_DEFAULT, + ALEXA_UNIT_CONVERSION, CONF_EXTENDED_ENTITY_DISCOVERY, RECURRING_DAY, RECURRING_PATTERN, @@ -49,14 +56,21 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): + # pylint: disable=too-many-locals """Set up the Alexa sensor platform.""" - devices: List[AlexaMediaNotificationSensor] = [] - SENSOR_TYPES = { + devices: list[AlexaMediaNotificationSensor] = [] + SENSOR_TYPES = { # pylint: disable=invalid-name "Alarm": AlarmSensor, "Timer": TimerSensor, "Reminder": ReminderSensor, } - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] @@ -73,7 +87,7 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf raise ConfigEntryNotReady if key not in (account_dict["entities"]["sensor"]): (account_dict["entities"]["sensor"][key]) = {} - for (n_type, class_) in SENSOR_TYPES.items(): + for n_type, class_ in SENSOR_TYPES.items(): n_type_dict = ( account_dict["notifications"][key][n_type] if key in account_dict["notifications"] @@ -81,7 +95,7 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf else {} ) if ( - n_type in ("Alarm, Timer") + n_type in ("Alarm", "Timer") and "TIMERS_AND_ALARMS" in device["capabilities"] ): alexa_client = class_( @@ -124,9 +138,19 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf account_dict, temperature_entities ) + # AIAQM Sensors + air_quality_sensors = [] + air_quality_entities = account_dict.get("devices", {}).get("air_quality", []) + if air_quality_entities and account_dict["options"].get( + CONF_EXTENDED_ENTITY_DISCOVERY + ): + air_quality_sensors = await create_air_quality_sensors( + account_dict, air_quality_entities + ) + return await add_devices( hide_email(account), - devices + temperature_sensors, + devices + temperature_sensors + air_quality_sensors, add_devices_callback, include_filter, exclude_filter, @@ -153,6 +177,7 @@ async def async_unload_entry(hass, entry) -> bool: async def create_temperature_sensors(account_dict, temperature_entities): + """Create temperature sensors.""" devices = [] coordinator = account_dict["coordinator"] for temp in temperature_entities: @@ -170,14 +195,54 @@ async def create_temperature_sensors(account_dict, temperature_entities): return devices +async def create_air_quality_sensors(account_dict, air_quality_entities): + devices = [] + coordinator = account_dict["coordinator"] + + for temp in air_quality_entities: + _LOGGER.debug( + "Creating entity %s for a air quality sensor with name %s", + temp["id"], + temp["name"], + ) + # Each AIAQM has 5 different sensors. + for subsensor in temp["sensors"]: + sensor_type = subsensor["sensorType"] + instance = subsensor["instance"] + unit = subsensor["unit"] + serial = temp["device_serial"] + device_info = lookup_device_info(account_dict, serial) + sensor = AirQualitySensor( + coordinator, + temp["id"], + temp["name"], + device_info, + sensor_type, + instance, + unit, + ) + _LOGGER.debug("Create air quality sensors %s", sensor) + account_dict["entities"]["sensor"].setdefault(serial, {}) + account_dict["entities"]["sensor"][serial].setdefault(sensor_type, {}) + account_dict["entities"]["sensor"][serial][sensor_type][ + "Air_Quality" + ] = sensor + devices.append(sensor) + return devices + + def lookup_device_info(account_dict, device_serial): """Get the device to use for a given Echo based on a given device serial id. This may return nothing as there is no guarantee that a given temperature sensor is actually attached to an Echo. """ - for key, mp in account_dict["entities"]["media_player"].items(): - if key == device_serial and mp.device_info and "identifiers" in mp.device_info: - for ident in mp.device_info["identifiers"]: + for key, mediaplayer in account_dict["entities"]["media_player"].items(): + if ( + key == device_serial + and mediaplayer.device_info + and "identifiers" in mediaplayer.device_info + ): + for ident in mediaplayer.device_info["identifiers"]: return ident return None @@ -186,44 +251,86 @@ class TemperatureSensor(SensorEntity, CoordinatorEntity): """A temperature sensor reported by an Echo.""" def __init__(self, coordinator, entity_id, name, media_player_device_id): + """Initialize temperature sensor.""" super().__init__(coordinator) self.alexa_entity_id = entity_id - self._name = name - self._media_player_device_id = media_player_device_id - - @property - def name(self): - return self._name + " Temperature" - - @property - def device_info(self): - """Return the device_info of the device.""" - if self._media_player_device_id: - return { - "identifiers": {self._media_player_device_id}, - "via_device": self._media_player_device_id, + self._attr_name = name + " Temperature" + self._attr_device_class = SensorDeviceClass.TEMPERATURE + self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_native_value: Optional[ + datetime.datetime + ] = parse_temperature_from_coordinator(coordinator, entity_id) + self._attr_native_unit_of_measurement: Optional[str] = UnitOfTemperature.CELSIUS + # This includes "_temperature" because the Alexa entityId is for a physical device + # A single physical device could have multiple HA entities + self._attr_unique_id = entity_id + "_temperature" + self._attr_device_info = ( + { + "identifiers": {media_player_device_id}, + "via_device": media_player_device_id, } - return None - - @property - def native_unit_of_measurement(self): - return TEMP_CELSIUS + if media_player_device_id + else None + ) - @property - def native_value(self): - return parse_temperature_from_coordinator( + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = parse_temperature_from_coordinator( self.coordinator, self.alexa_entity_id ) + super()._handle_coordinator_update() - @property - def device_class(self): - return "temperature" - @property - def unique_id(self): - # This includes "_temperature" because the Alexa entityId is for a physical device - # A single physical device could have multiple HA entities - return self.alexa_entity_id + "_temperature" +class AirQualitySensor(SensorEntity, CoordinatorEntity): + """A air quality sensor reported by an Amazon indoor air quality monitor.""" + + def __init__( + self, + coordinator, + entity_id, + name, + media_player_device_id, + sensor_name, + instance, + unit, + ): + super().__init__(coordinator) + self.alexa_entity_id = entity_id + self._sensor_name = sensor_name + # tidy up name + self._sensor_name = self._sensor_name.replace("Alexa.AirQuality.", "") + self._sensor_name = "".join( + " " + char if char.isupper() else char.strip() for char in self._sensor_name + ).strip() + self._attr_name = name + " " + self._sensor_name + self._attr_device_class = self._sensor_name + self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_native_value: Optional[ + datetime.datetime + ] = parse_air_quality_from_coordinator(coordinator, entity_id, instance) + self._attr_native_unit_of_measurement: Optional[ + str + ] = ALEXA_UNIT_CONVERSION.get(unit) + self._attr_unique_id = entity_id + " " + self._sensor_name + self._attr_icon = ALEXA_ICON_CONVERSION.get(sensor_name, ALEXA_ICON_DEFAULT) + self._attr_device_info = ( + { + "identifiers": {media_player_device_id}, + "via_device": media_player_device_id, + } + if media_player_device_id + else None + ) + self._instance = instance + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = parse_air_quality_from_coordinator( + self.coordinator, self.alexa_entity_id, self._instance + ) + super()._handle_coordinator_update() class AlexaMediaNotificationSensor(SensorEntity): @@ -240,22 +347,29 @@ def __init__( ): """Initialize the Alexa sensor device.""" # Class info + self._attr_device_class = SensorDeviceClass.TIMESTAMP + self._attr_state_class = None + self._attr_native_value: Optional[datetime.datetime] = None + self._attr_name = f"{client.name} {name}" + self._attr_unique_id = f"{client.unique_id}_{name}" + self._attr_icon = icon + self._attr_device_info = { + "identifiers": {(ALEXA_DOMAIN, client.unique_id)}, + "via_device": (ALEXA_DOMAIN, client.unique_id), + } + self._attr_assumed_state = client.assumed_state + self._attr_available = client.available self._client = client self._n_dict = n_dict self._sensor_property = sensor_property self._account = account - self._dev_id = client.unique_id - self._name = name - self._unit = None - self._device_class = DEVICE_CLASS_TIMESTAMP - self._icon = icon + self._type = "" if not self._type else self._type self._all = [] self._active = [] - self._next = None + self._next: Optional[dict] = None self._prior_value = None self._timestamp: Optional[datetime.datetime] = None self._tracker: Optional[Callable] = None - self._state: Optional[datetime.datetime] = None self._dismissed: Optional[datetime.datetime] = None self._status: Optional[str] = None self._amz_id: Optional[str] = None @@ -282,12 +396,12 @@ def _process_raw_notifications(self): ) if alarm_just_dismissed(alarm, self._status, self._version): self._dismissed = dt.now().isoformat() - self._state = self._process_state(self._next) + self._attr_native_value = self._process_state(self._next) self._status = self._next.get("status", "OFF") if self._next else "OFF" self._version = self._next.get("version", "0") if self._next else None self._amz_id = self._next.get("id") if self._next else None - if self._state == STATE_UNAVAILABLE or self._next != self._prior_value: + if self._attr_native_value is None or self._next != self._prior_value: # cancel any event triggers if self._tracker: _LOGGER.debug( @@ -295,16 +409,16 @@ def _process_raw_notifications(self): self, ) self._tracker() - if self._state != STATE_UNAVAILABLE and self._status != "SNOOZED": + if self._attr_native_value is not None and self._status != "SNOOZED": _LOGGER.debug( "%s: Scheduling event in %s", self, - dt.as_utc(dt.parse_datetime(self._state)) - dt.utcnow(), + dt.as_utc(self._attr_native_value) - dt.utcnow(), ) self._tracker = async_track_point_in_utc_time( self.hass, self._trigger_event, - dt.as_utc(dt.parse_datetime(self._state)), + dt.as_utc(self._attr_native_value), ) def _trigger_event(self, time_date) -> None: @@ -331,7 +445,9 @@ def _fix_alarm_date_time(self, value): ): return value naive_time = dt.parse_datetime(value[1][self._sensor_property]) - timezone = pytz.timezone(self._client._timezone) + timezone = pytz.timezone( + self._client._timezone # pylint: disable=protected-access + ) if timezone and naive_time: value[1][self._sensor_property] = timezone.localize(naive_time) elif not naive_time: @@ -355,7 +471,7 @@ def _fix_alarm_date_time(self, value): self._client.name, value[1], naive_time, - self._client._timezone, + self._client._timezone, # pylint: disable=protected-access ) return value @@ -374,7 +490,9 @@ def _update_recurring_alarm(self, value): ) alarm_on = next_item["status"] == "ON" r_rule_data = next_item.get("rRuleData") - if r_rule_data: # the new recurrence pattern; https://github.com/custom-components/alexa_media_player/issues/1608 + if ( + r_rule_data + ): # the new recurrence pattern; https://github.com/custom-components/alexa_media_player/issues/1608 next_trigger_times = r_rule_data.get("nextTriggerTimes") weekdays = r_rule_data.get("byWeekDays") if next_trigger_times: @@ -437,7 +555,7 @@ async def async_will_remove_from_hass(self): def _handle_event(self, event): """Handle events. - This will update PUSH_NOTIFICATION_CHANGE events to see if the sensor + This will update PUSH_ACTIVITY events to see if the sensor should be updated. """ try: @@ -445,38 +563,18 @@ def _handle_event(self, event): return except AttributeError: pass - if "notification_update" in event: + if "push_activity" in event: if ( - event["notification_update"]["dopplerId"]["deviceSerialNumber"] + event["push_activity"]["key"]["serialNumber"] == self._client.device_serial_number ): _LOGGER.debug("Updating sensor %s", self) - self.async_schedule_update_ha_state(True) - - @property - def available(self): - """Return the availability of the sensor.""" - return self._client.available - - @property - def assumed_state(self): - """Return whether the state is an assumed_state.""" - return self._client.assumed_state + self.schedule_update_ha_state(True) @property def hidden(self): """Return whether the sensor should be hidden.""" - return self.state == STATE_UNAVAILABLE - - @property - def unique_id(self): - """Return the unique ID.""" - return f"{self._client.unique_id}_{self._name}" - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._client.name} {self._name}" + return self.state is None @property def should_poll(self): @@ -485,27 +583,8 @@ def should_poll(self): self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._account]["websocket"] ) - @property - def state(self) -> datetime.datetime: - """Return the state of the sensor.""" - return self._state - - def _process_state(self, value): - return ( - dt.as_local(value[self._sensor_property]).isoformat() - if value - else STATE_UNAVAILABLE - ) - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return self._unit - - @property - def device_class(self): - """Return the device_class of the device.""" - return self._device_class + def _process_state(self, value) -> Optional[datetime.datetime]: + return dt.as_local(value[self._sensor_property]) if value else None async def async_update(self): """Update state.""" @@ -528,19 +607,6 @@ async def async_update(self): except NoEntitySpecifiedError: pass # we ignore this due to a harmless startup race condition - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(ALEXA_DOMAIN, self._dev_id)}, - "via_device": (ALEXA_DOMAIN, self._dev_id), - } - - @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon - @property def recurrence(self): """Return the recurrence pattern of the sensor.""" @@ -553,8 +619,6 @@ def recurrence(self): @property def extra_state_attributes(self): """Return additional attributes.""" - import json - attr = { "recurrence": self.recurrence, "process_timestamp": dt.as_local(self._timestamp).isoformat(), @@ -568,6 +632,11 @@ def extra_state_attributes(self): } return attr + @callback + def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude sorted_active and sorted_all from being recorded in the database.""" + return {"sorted_active", "sorted_all"} + class AlarmSensor(AlexaMediaNotificationSensor): """Representation of a Alexa Alarm sensor.""" @@ -599,22 +668,22 @@ def __init__(self, client, n_json, account): else "mdi:timer", ) - def _process_state(self, value): + def _process_state(self, value) -> Optional[datetime.datetime]: return ( dt.as_local( super()._round_time( self._timestamp + datetime.timedelta(milliseconds=value[self._sensor_property]) ) - ).isoformat() + ) if value and self._timestamp - else STATE_UNAVAILABLE + else None ) @property def paused(self) -> Optional[bool]: """Return the paused state of the sensor.""" - return self._next["status"] == "PAUSED" if self._next else None + return self._next.get("status") == "PAUSED" if self._next else None @property def icon(self): @@ -624,7 +693,7 @@ def icon(self): if (version.parse(HA_VERSION) >= version.parse("0.113.0")) else "mdi:timer-off" ) - return self._icon if not self.paused else off_icon + return self._attr_icon if not self.paused else off_icon class ReminderSensor(AlexaMediaNotificationSensor): @@ -638,7 +707,7 @@ def __init__(self, client, n_json, account): client, n_json, "alarmTime", account, f"next {self._type}", "mdi:reminder" ) - def _process_state(self, value): + def _process_state(self, value) -> Optional[datetime.datetime]: return ( dt.as_local( super()._round_time( @@ -646,15 +715,15 @@ def _process_state(self, value): value[self._sensor_property] / 1000, tz=LOCAL_TIMEZONE ) ) - ).isoformat() + ) if value - else STATE_UNAVAILABLE + else None ) @property def reminder(self): """Return the reminder of the sensor.""" - return self._next["reminderLabel"] if self._next else None + return self._next.get("reminderLabel") if self._next else None @property def extra_state_attributes(self): diff --git a/custom_components/alexa_media/services.py b/custom_components/alexa_media/services.py index 418b09ca..3f85e723 100644 --- a/custom_components/alexa_media/services.py +++ b/custom_components/alexa_media/services.py @@ -8,7 +8,7 @@ """ import logging -from typing import Callable, Dict, Text +from typing import Callable from alexapy import AlexaAPI, AlexapyLoginError, hide_email from alexapy.errors import AlexapyConnectionError @@ -49,10 +49,10 @@ class AlexaMediaServices: """Class that holds our services that should be published to hass.""" - def __init__(self, hass, functions: Dict[Text, Callable]): + def __init__(self, hass, functions: dict[str, Callable]): """Initialize with self.hass.""" self.hass = hass - self.functions: Dict[Text, Callable] = functions + self.functions: dict[str, Callable] = functions async def register(self): """Register services to hass.""" @@ -158,8 +158,8 @@ async def force_logout(self, call) -> bool: async def last_call_handler(self, call): """Handle last call service request. - Args: - call.ATTR_EMAIL: List of case-sensitive Alexa email addresses. If None + Args + call: List of case-sensitive Alexa email addresses. If None all accounts are updated. """ diff --git a/custom_components/alexa_media/strings.json b/custom_components/alexa_media/strings.json index 55e880b0..e4328ddd 100644 --- a/custom_components/alexa_media/strings.json +++ b/custom_components/alexa_media/strings.json @@ -16,7 +16,7 @@ "email": "Email Address", "url": "Amazon region domain (e.g., amazon.co.uk)", "hass_url": "Url to access Home Assistant", - "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This not six digits long.", + "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This is not six digits long.", "include_devices": "Included device (comma separated)", "exclude_devices": "Excluded device (comma separated)", "debug": "Advanced debugging", diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index 5a2e95a3..9a572057 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -7,7 +7,7 @@ https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ import logging -from typing import List, Text # noqa pylint: disable=unused-import +from typing import List from homeassistant.exceptions import ConfigEntryNotReady, NoEntitySpecifiedError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -36,12 +36,18 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa switch platform.""" devices = [] # type: List[DNDSwitch] - SWITCH_TYPES = [ + SWITCH_TYPES = [ # pylint: disable=invalid-name ("dnd", DNDSwitch), ("shuffle", ShuffleSwitch), ("repeat", RepeatSwitch), ] - account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] + account = None + if config: + account = config.get(CONF_EMAIL) + if account is None and discovery_info: + account = discovery_info.get("config", {}).get(CONF_EMAIL) + if account is None: + raise ConfigEntryNotReady include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] @@ -62,7 +68,7 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"][ key ] = {} - for (switch_key, class_) in SWITCH_TYPES: + for switch_key, class_ in SWITCH_TYPES: if ( switch_key == "dnd" and not account_dict["devices"]["switch"].get(key, {}).get("dnd") @@ -139,8 +145,8 @@ class AlexaMediaSwitch(SwitchDevice, AlexaMedia): def __init__( self, client, - switch_property: Text, - switch_function: Text, + switch_property: str, + switch_function: str, name="Alexa", ): """Initialize the Alexa Switch device.""" @@ -188,6 +194,7 @@ def _handle_event(self, event): @_catch_login_errors async def _set_switch(self, state, **kwargs): + # pylint: disable=unused-argument try: if not self.enabled: return @@ -290,7 +297,7 @@ def icon(self): """Return the icon of the switch.""" return self._icon() - def _icon(self, on=None, off=None): + def _icon(self, on=None, off=None): # pylint: disable=invalid-name return on if self.is_on else off diff --git a/custom_components/alexa_media/translations/ar.json b/custom_components/alexa_media/translations/ar.json index 5d5a1b8d..3108c59d 100644 --- a/custom_components/alexa_media/translations/ar.json +++ b/custom_components/alexa_media/translations/ar.json @@ -36,7 +36,7 @@ "exclude_devices": "Excluded device (comma separated)", "hass_url": "Url to access Home Assistant", "include_devices": "Included device (comma separated)", - "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This not six digits long.", + "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This is not six digits long.", "password": "Password", "scan_interval": "Seconds between scans", "securitycode": "[%key_id:55616596%]", diff --git a/custom_components/alexa_media/translations/de.json b/custom_components/alexa_media/translations/de.json index bdaf9246..88ada4cc 100644 --- a/custom_components/alexa_media/translations/de.json +++ b/custom_components/alexa_media/translations/de.json @@ -6,12 +6,12 @@ "reauth_successful": "Alexa Media Player erfolgreich authentifiziert" }, "error": { - "2fa_key_invalid": "Invalid Built-In 2FA key", + "2fa_key_invalid": "Ungültiger 2-Faktor Schlüssel", "connection_error": "Verbindungsfehler; Netzwerk prüfen und erneut versuchen", - "identifier_exists": "Diese Email ist bereits registriert", + "identifier_exists": "Diese E-Mail-Adresse ist bereits registriert", "invalid_credentials": "Falsche Zugangsdaten", "invalid_url": "URL ist ungültig: {message}", - "unable_to_connect_hass_url": "Es kann keine Verbindung zur Home Assistant-URL hergestellt werden. Bitte überprüfen Sie die externe URL unter Konfiguration - > Allgemein", + "unable_to_connect_hass_url": "Es kann keine Verbindung zur Home Assistant-URL hergestellt werden. Bitte überprüfen Sie die externe URL unter Konfiguration -> Allgemein", "unknown_error": "Unbekannter Fehler, bitte Log-Info melden" }, "step": { @@ -20,21 +20,21 @@ "proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning." }, "description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your **HA server** can reach {hass_url}. This field is from the External Url under Configuration -> General but you can try your internal url.\n\nIf you are **certain** your client can reach this url, you can bypass this warning.", - "title": "Alexa Media Player - Unable to Connect to HA URL" + "title": "Alexa Media Player - Keine Verbindung zur Home Assistant-URL möglich" }, "totp_register": { "data": { "registered": "OTP from the Built-in 2FA App Key confirmed successfully." }, "description": "**{email} - alexa.{url}** \nHave you successfully confirmed an OTP from the Built-in 2FA App Key with Amazon? \n >OTP Code {message}", - "title": "Alexa Media Player - OTP Confirmation" + "title": "Alexa Media Player - OTP Bestätigung" }, "user": { "data": { - "debug": "Erweitertes debugging", - "email": "Email Adresse", + "debug": "Erweitertes Debugging", + "email": "E-Mail Adresse", "exclude_devices": "Ausgeschlossene Geräte (komma getrennnt)", - "hass_url": "Url to access Home Assistant", + "hass_url": "Home Assistant-URL", "include_devices": "Eingebundene Geräte (komma getrennnt)", "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes)", "password": "Passwort", diff --git a/custom_components/alexa_media/translations/en.json b/custom_components/alexa_media/translations/en.json index 651baef5..8a6108fb 100644 --- a/custom_components/alexa_media/translations/en.json +++ b/custom_components/alexa_media/translations/en.json @@ -36,7 +36,7 @@ "exclude_devices": "Excluded device (comma separated)", "hass_url": "Url to access Home Assistant", "include_devices": "Included device (comma separated)", - "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This not six digits long.", + "otp_secret": "Built-in 2FA App Key (automatically generate 2FA Codes). This is not six digits long.", "password": "Password", "scan_interval": "Seconds between scans", "securitycode": "[%key_id:55616596%]", diff --git a/custom_components/alexa_media/translations/pt-BR.json b/custom_components/alexa_media/translations/pt-BR.json index 88d892fa..28ef6896 100644 --- a/custom_components/alexa_media/translations/pt-BR.json +++ b/custom_components/alexa_media/translations/pt-BR.json @@ -36,7 +36,7 @@ "exclude_devices": "Dispositivos excluídos (separado por vírgula)", "hass_url": "Url para acesso ao Home Assistant", "include_devices": "Dispositivos incluídos (separado por vírgula)", - "otp_secret": "Chave de aplicativo 2FA integrada (gera automaticamente códigos 2FA). Essa não tem seis dígitos.", + "otp_secret": "Chave de aplicativo 2FA integrada (gera códigos 2FA automaticamente). Isso não tem seis dígitos.", "password": "Senha", "scan_interval": "Segundos entre varreduras", "securitycode": "[%key_id:55616596%]", diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py new file mode 100644 index 00000000..a1acec04 --- /dev/null +++ b/custom_components/better_thermostat/__init__.py @@ -0,0 +1,91 @@ +"""The better_thermostat component.""" +import logging +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, Config +from homeassistant.config_entries import ConfigEntry +import voluptuous as vol + +from .const import ( + CONF_FIX_CALIBRATION, + CONF_CALIBRATION_MODE, + CONF_HEATER, + CONF_NO_SYSTEM_MODE_OFF, + CONF_WINDOW_TIMEOUT, +) + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "better_thermostat" +PLATFORMS = [Platform.CLIMATE] +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, config: Config): + """Set up this integration using YAML is not supported.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + hass.data[DOMAIN] = {} + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + await async_unload_entry(hass, config_entry) + await async_setup_entry(hass, config_entry) + + +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + if config_entry.version == 1: + new = {**config_entry.data} + for trv in new[CONF_HEATER]: + trv["advanced"].update({CONF_FIX_CALIBRATION: False}) + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=new) + + if config_entry.version == 2: + new = {**config_entry.data} + new[CONF_WINDOW_TIMEOUT] = 0 + config_entry.version = 3 + hass.config_entries.async_update_entry(config_entry, data=new) + + if config_entry.version == 3: + new = {**config_entry.data} + for trv in new[CONF_HEATER]: + if ( + CONF_FIX_CALIBRATION in trv["advanced"] + and trv["advanced"][CONF_FIX_CALIBRATION] + ): + trv["advanced"].update({CONF_CALIBRATION_MODE: CONF_FIX_CALIBRATION}) + else: + trv["advanced"].update({CONF_CALIBRATION_MODE: "default"}) + config_entry.version = 4 + hass.config_entries.async_update_entry(config_entry, data=new) + + if config_entry.version == 4: + new = {**config_entry.data} + for trv in new[CONF_HEATER]: + trv["advanced"].update({CONF_NO_SYSTEM_MODE_OFF: False}) + config_entry.version = 5 + hass.config_entries.async_update_entry(config_entry, data=new) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/custom_components/better_thermostat/adapters/deconz.py b/custom_components/better_thermostat/adapters/deconz.py new file mode 100644 index 00000000..c6055439 --- /dev/null +++ b/custom_components/better_thermostat/adapters/deconz.py @@ -0,0 +1,66 @@ +import logging +from .generic import ( + set_temperature as generic_set_temperature, + set_hvac_mode as generic_set_hvac_mode, +) + +_LOGGER = logging.getLogger(__name__) + + +async def get_info(self, entity_id): + """Get info from TRV.""" + _offset = self.hass.states.get(entity_id).attributes.get("offset", None) + if _offset is None: + return {"support_offset": False, "support_valve": False} + return {"support_offset": True, "support_valve": False} + + +async def init(self, entity_id): + return None + + +async def set_temperature(self, entity_id, temperature): + """Set new target temperature.""" + return await generic_set_temperature(self, entity_id, temperature) + + +async def set_hvac_mode(self, entity_id, hvac_mode): + """Set new target hvac mode.""" + return await generic_set_hvac_mode(self, entity_id, hvac_mode) + + +async def get_current_offset(self, entity_id): + """Get current offset.""" + return float(str(self.hass.states.get(entity_id).attributes.get("offset", 0))) + + +async def get_offset_steps(self, entity_id): + """Get offset steps.""" + return float(1.0) + + +async def get_min_offset(self, entity_id): + """Get min offset.""" + return -6 + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + return 6 + + +async def set_offset(self, entity_id, offset): + """Set new target offset.""" + await self.hass.services.async_call( + "deconz", + "configure", + {"entity": entity_id, "field": "/config", "data": {"offset": offset}}, + blocking=True, + context=self.context, + ) + self.real_trvs[entity_id]["last_calibration"] = offset + + +async def set_valve(self, entity_id, valve): + """Set new target valve.""" + return None diff --git a/custom_components/better_thermostat/adapters/generic.py b/custom_components/better_thermostat/adapters/generic.py new file mode 100644 index 00000000..fb7a48e6 --- /dev/null +++ b/custom_components/better_thermostat/adapters/generic.py @@ -0,0 +1,64 @@ +import logging + +_LOGGER = logging.getLogger(__name__) + + +async def get_info(self, entity_id): + """Get info from TRV.""" + return {"support_offset": False, "support_valve": False} + + +async def init(self, entity_id): + return None + + +async def get_current_offset(self, entity_id): + """Get current offset.""" + return None + + +async def get_offset_steps(self, entity_id): + """Get offset steps.""" + return None + + +async def get_min_offset(self, entity_id): + """Get min offset.""" + return -6 + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + return 6 + + +async def set_temperature(self, entity_id, temperature): + """Set new target temperature.""" + await self.hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": temperature}, + blocking=True, + context=self.context, + ) + + +async def set_hvac_mode(self, entity_id, hvac_mode): + """Set new target hvac mode.""" + await self.hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": hvac_mode}, + blocking=True, + context=self.context, + ) + + +async def set_offset(self, entity_id, offset): + """Set new target offset.""" + return # Not supported + + +async def set_valve(self, entity_id, valve): + """Set new target valve.""" + return # Not supported diff --git a/custom_components/better_thermostat/adapters/mqtt.py b/custom_components/better_thermostat/adapters/mqtt.py new file mode 100644 index 00000000..0dc59f11 --- /dev/null +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -0,0 +1,173 @@ +import asyncio +import logging + +from homeassistant.components.number.const import SERVICE_SET_VALUE + +from ..utils.helpers import find_local_calibration_entity, find_valve_entity +from .generic import ( + set_hvac_mode as generic_set_hvac_mode, + set_temperature as generic_set_temperature, +) +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + +_LOGGER = logging.getLogger(__name__) + + +async def get_info(self, entity_id): + """Get info from TRV.""" + support_offset = False + support_valve = False + offset = await find_local_calibration_entity(self, entity_id) + if offset is not None: + support_offset = True + valve = await find_valve_entity(self, entity_id) + if valve is not None: + support_valve = True + return {"support_offset": support_offset, "support_valve": support_valve} + + +async def init(self, entity_id): + if ( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] is None + and self.real_trvs[entity_id]["calibration"] == 0 + ): + self.real_trvs[entity_id][ + "local_temperature_calibration_entity" + ] = await find_local_calibration_entity(self, entity_id) + _LOGGER.debug( + "better_thermostat %s: uses local calibration entity %s", + self.name, + self.real_trvs[entity_id]["local_temperature_calibration_entity"], + ) + # Wait for the entity to be available + _ready = True + while _ready: + if self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + _LOGGER.info( + "better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...", + self.name, + self.real_trvs[entity_id]["local_temperature_calibration_entity"], + ) + await asyncio.sleep(5) + continue + _ready = False + return + + _has_preset = self.hass.states.get(entity_id).attributes.get( + "preset_modes", None + ) + if _has_preset is not None: + await self.hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "manual"}, + blocking=True, + context=self.context, + ) + + +async def set_temperature(self, entity_id, temperature): + """Set new target temperature.""" + return await generic_set_temperature(self, entity_id, temperature) + + +async def set_hvac_mode(self, entity_id, hvac_mode): + """Set new target hvac mode.""" + await generic_set_hvac_mode(self, entity_id, hvac_mode) + await asyncio.sleep(3) + + +async def get_current_offset(self, entity_id): + """Get current offset.""" + return float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).state + ) + ) + + +async def get_offset_steps(self, entity_id): + """Get offset steps.""" + return float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).attributes.get("step", 1) + ) + ) + + +async def get_min_offset(self, entity_id): + """Get min offset.""" + return float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).attributes.get("min", -10) + ) + ) + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + return float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).attributes.get("max", 10) + ) + ) + + +async def set_offset(self, entity_id, offset): + """Set new target offset.""" + max_calibration = await get_max_offset(self, entity_id) + min_calibration = await get_min_offset(self, entity_id) + + if offset >= max_calibration: + offset = max_calibration + if offset <= min_calibration: + offset = min_calibration + + await self.hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + "entity_id": self.real_trvs[entity_id][ + "local_temperature_calibration_entity" + ], + "value": offset, + }, + blocking=True, + context=self.context, + ) + self.real_trvs[entity_id]["last_calibration"] = offset + if ( + self.real_trvs[entity_id]["last_hvac_mode"] is not None + and self.real_trvs[entity_id]["last_hvac_mode"] != "off" + ): + await asyncio.sleep(3) + return await generic_set_hvac_mode( + self, entity_id, self.real_trvs[entity_id]["last_hvac_mode"] + ) + + +async def set_valve(self, entity_id, valve): + """Set new target valve.""" + _LOGGER.debug( + f"better_thermostat {self.name}: TO TRV {entity_id} set_valve: {valve}" + ) + await self.hass.services.async_call( + "number", + SERVICE_SET_VALUE, + { + "entity_id": self.real_trvs[entity_id]["valve_position_entity"], + "value": valve, + }, + blocking=True, + context=self.context, + ) diff --git a/custom_components/better_thermostat/adapters/tado.py b/custom_components/better_thermostat/adapters/tado.py new file mode 100644 index 00000000..ea13b2f8 --- /dev/null +++ b/custom_components/better_thermostat/adapters/tado.py @@ -0,0 +1,69 @@ +import logging +from .generic import ( + set_temperature as generic_set_temperature, + set_hvac_mode as generic_set_hvac_mode, +) + +_LOGGER = logging.getLogger(__name__) + + +async def get_info(self, entity_id): + """Get info from TRV.""" + return {"support_offset": True, "support_valve": False} + + +async def init(self, entity_id): + return None + + +async def set_temperature(self, entity_id, temperature): + """Set new target temperature.""" + return await generic_set_temperature(self, entity_id, temperature) + + +async def set_hvac_mode(self, entity_id, hvac_mode): + """Set new target hvac mode.""" + return await generic_set_hvac_mode(self, entity_id, hvac_mode) + + +async def get_current_offset(self, entity_id): + """Get current offset.""" + return float( + str(self.hass.states.get(entity_id).attributes.get("offset_celsius", 0)) + ) + + +async def get_offset_steps(self, entity_id): + """Get offset steps.""" + return float(0.01) + + +async def get_min_offset(self, entity_id): + """Get min offset.""" + return -10 + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + return 10 + + +async def set_offset(self, entity_id, offset): + """Set new target offset.""" + if offset >= 10: + offset = 10 + if offset <= -10: + offset = -10 + await self.hass.services.async_call( + "tado", + "set_climate_temperature_offset", + {"entity_id": entity_id, "offset": offset}, + blocking=True, + context=self.context, + ) + self.real_trvs[entity_id]["last_calibration"] = offset + + +async def set_valve(self, entity_id, valve): + """Set new target valve.""" + return None diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py new file mode 100644 index 00000000..4d5cee75 --- /dev/null +++ b/custom_components/better_thermostat/climate.py @@ -0,0 +1,1160 @@ +"""Better Thermostat""" + +import asyncio +import logging +from abc import ABC +from datetime import datetime, timedelta +from random import randint +from statistics import mean + +from .utils.watcher import check_all_entities + +from .utils.weather import check_ambient_air_temperature, check_weather +from .utils.bridge import ( + get_current_offset, + get_min_offset, + get_max_offset, + init, + load_adapter, +) + +from .utils.model_quirks import load_model_quirks + +from .utils.helpers import convert_to_float, find_battery_entity +from homeassistant.helpers import entity_platform +from homeassistant.core import callback, CoreState, Context, ServiceCall +import json +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_STEP, + HVACMode, + HVACAction, +) +from homeassistant.const import ( + CONF_NAME, + EVENT_HOMEASSISTANT_START, + ATTR_TEMPERATURE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.helpers.event import ( + async_track_state_change_event, + async_track_time_change, + async_track_time_interval, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from homeassistant.components.group.util import reduce_attribute + +from .const import ( + ATTR_STATE_BATTERIES, + ATTR_STATE_CALL_FOR_HEAT, + ATTR_STATE_ERRORS, + ATTR_STATE_HUMIDIY, + ATTR_STATE_LAST_CHANGE, + ATTR_STATE_MAIN_MODE, + ATTR_STATE_WINDOW_OPEN, + ATTR_STATE_SAVED_TEMPERATURE, + ATTR_STATE_HEATING_POWER, + CONF_HEATER, + CONF_HUMIDITY, + CONF_MODEL, + CONF_OFF_TEMPERATURE, + CONF_OUTDOOR_SENSOR, + CONF_SENSOR, + CONF_SENSOR_WINDOW, + CONF_WEATHER, + CONF_WINDOW_TIMEOUT, + SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, + SUPPORT_FLAGS, + VERSION, + SERVICE_SET_TEMP_TARGET_TEMPERATURE, + SERVICE_RESET_HEATING_POWER, + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, + BetterThermostatEntityFeature, +) + +from .utils.controlling import control_queue, control_trv +from .events.temperature import trigger_temperature_change +from .events.trv import trigger_trv_change +from .events.window import trigger_window_change, window_queue + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "better_thermostat" + + +class ContinueLoop(Exception): + pass + + +async def async_setup_entry(hass, entry, async_add_devices): + """Setup sensor platform.""" + + async def async_service_handler(self, data: ServiceCall): + _LOGGER.debug(f"Service call: {self} » {data.service}") + if data.service == SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE: + await self.restore_temp_temperature() + elif data.service == SERVICE_SET_TEMP_TARGET_TEMPERATURE: + await self.set_temp_temperature(data.data[ATTR_TEMPERATURE]) + elif data.service == SERVICE_RESET_HEATING_POWER: + await self.reset_heating_power + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_SET_TEMP_TARGET_TEMPERATURE, + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, # type: ignore + async_service_handler, + [ + BetterThermostatEntityFeature.TARGET_TEMPERATURE, + BetterThermostatEntityFeature.TARGET_TEMPERATURE_RANGE, + ], + ) + platform.async_register_entity_service( + SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, {}, async_service_handler + ) + platform.async_register_entity_service( + SERVICE_RESET_HEATING_POWER, {}, async_service_handler + ) + + async_add_devices( + [ + BetterThermostat( + entry.data.get(CONF_NAME), + entry.data.get(CONF_HEATER), + entry.data.get(CONF_SENSOR), + entry.data.get(CONF_HUMIDITY, None), + entry.data.get(CONF_SENSOR_WINDOW, None), + entry.data.get(CONF_WINDOW_TIMEOUT, None), + entry.data.get(CONF_WEATHER, None), + entry.data.get(CONF_OUTDOOR_SENSOR, None), + entry.data.get(CONF_OFF_TEMPERATURE, None), + entry.data.get(CONF_MODEL, None), + hass.config.units.temperature_unit, + entry.entry_id, + device_class="better_thermostat", + state_class="better_thermostat_state", + ) + ] + ) + + +class BetterThermostat(ClimateEntity, RestoreEntity, ABC): + """Representation of a Better Thermostat device.""" + + async def set_temp_temperature(self, temperature): + if self._saved_temperature is None: + self._saved_temperature = self.bt_target_temp + self.bt_target_temp = convert_to_float( + temperature, self.name, "service.settarget_temperature()" + ) + self.async_write_ha_state() + await self.control_queue_task.put(self) + else: + self.bt_target_temp = convert_to_float( + temperature, self.name, "service.settarget_temperature()" + ) + self.async_write_ha_state() + await self.control_queue_task.put(self) + + async def savetarget_temperature(self): + self._saved_temperature = self.bt_target_temp + self.async_write_ha_state() + + async def restore_temp_temperature(self): + if self._saved_temperature is not None: + self.bt_target_temp = convert_to_float( + self._saved_temperature, self.name, "service.restore_temp_temperature()" + ) + self._saved_temperature = None + self.async_write_ha_state() + await self.control_queue_task.put(self) + + async def reset_heating_power(self): + self.heating_power = 0.01 + self.async_write_ha_state() + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Better Thermostat", + "model": self.model, + "sw_version": VERSION, + } + + def __init__( + self, + name, + heater_entity_id, + sensor_entity_id, + humidity_sensor_entity_id, + window_id, + window_delay, + weather_entity, + outdoor_sensor, + off_temperature, + model, + unit, + unique_id, + device_class, + state_class, + ): + """Initialize the thermostat. + + Parameters + ---------- + TODO + """ + self._name = name + self.model = model + self.real_trvs = {} + self.entity_ids = [] + self.all_trvs = heater_entity_id + self.sensor_entity_id = sensor_entity_id + self.humidity_entity_id = humidity_sensor_entity_id + self.window_id = window_id or None + self.window_delay = window_delay or 0 + self.weather_entity = weather_entity or None + self.outdoor_sensor = outdoor_sensor or None + self.off_temperature = float(off_temperature) or None + self._unique_id = unique_id + self._unit = unit + self._device_class = device_class + self._state_class = state_class + self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] + self.next_valve_maintenance = datetime.now() + timedelta( + hours=randint(1, 24 * 5) + ) + self.cur_temp = None + self.cur_humidity = 0 + self.window_open = None + self.bt_target_temp_step = 1 + self.bt_min_temp = 0 + self.bt_max_temp = 30 + self.bt_target_temp = 5 + self._support_flags = SUPPORT_FLAGS + self.bt_hvac_mode = None + self.closed_window_triggered = False + self.call_for_heat = True + self.ignore_states = False + self.last_dampening_timestamp = None + self.version = VERSION + self.last_change = datetime.now() - timedelta(hours=2) + self.last_external_sensor_change = datetime.now() - timedelta(hours=2) + self.last_internal_sensor_change = datetime.now() - timedelta(hours=2) + self._temp_lock = asyncio.Lock() + self.startup_running = True + self._saved_temperature = None + self.last_avg_outdoor_temp = None + self.last_main_hvac_mode = None + self.last_window_state = None + self._last_call_for_heat = None + self._available = False + self.context = None + self.attr_hvac_action = None + self.old_attr_hvac_action = None + self.heating_start_temp = None + self.heating_start_timestamp = None + self.heating_end_temp = None + self.heating_end_timestamp = None + self._async_unsub_state_changed = None + self.old_external_temp = 0 + self.old_internal_temp = 0 + self.all_entities = [] + self.devices_states = {} + self.devices_errors = [] + self.control_queue_task = asyncio.Queue(maxsize=1) + if self.window_id is not None: + self.window_queue_task = asyncio.Queue(maxsize=1) + asyncio.create_task(control_queue(self)) + if self.window_id is not None: + asyncio.create_task(window_queue(self)) + self.heating_power = 0.01 + self.last_heating_power_stats = [] + + async def async_added_to_hass(self): + """Run when entity about to be added. + + Returns + ------- + None + """ + if isinstance(self.all_trvs, str): + return _LOGGER.error( + "You updated from version before 1.0.0-Beta36 of the Better Thermostat integration, you need to remove the BT devices (integration) and add it again." + ) + + self.entity_ids = [ + entity for trv in self.all_trvs if (entity := trv["trv"]) is not None + ] + + for trv in self.all_trvs: + _calibration = 1 + if trv["advanced"]["calibration"] == "local_calibration_based": + _calibration = 0 + _adapter = load_adapter(self, trv["integration"], trv["trv"]) + _model_quirks = load_model_quirks(self, trv["model"], trv["trv"]) + self.real_trvs[trv["trv"]] = { + "calibration": _calibration, + "integration": trv["integration"], + "adapter": _adapter, + "model_quirks": _model_quirks, + "model": trv["model"], + "advanced": trv["advanced"], + "ignore_trv_states": False, + "valve_position": None, + "max_temp": None, + "min_temp": None, + "target_temp_step": None, + "temperature": None, + "current_temperature": None, + "hvac_modes": None, + "hvac_mode": None, + "local_temperature_calibration_entity": None, + "local_calibration_min": None, + "local_calibration_max": None, + "calibration_received": True, + "target_temp_received": True, + "system_mode_received": True, + "last_temperature": None, + "last_valve_position": None, + "last_hvac_mode": None, + "last_current_temperature": None, + "last_calibration": None, + } + + await super().async_added_to_hass() + + _LOGGER.info( + "better_thermostat %s: Waiting for entity to be ready...", self.name + ) + + @callback + def _async_startup(*_): + """Init on startup. + + Parameters + ---------- + _ : + All parameters are piped. + """ + self.context = Context() + loop = asyncio.get_event_loop() + loop.create_task(self.startup()) + + if self.hass.state == CoreState.running: + _async_startup() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + + async def _trigger_check_weather(self, event=None): + _check = await check_all_entities(self) + if _check is False: + return + check_weather(self) + if self._last_call_for_heat != self.call_for_heat: + self._last_call_for_heat = self.call_for_heat + await self.async_update_ha_state(force_refresh=True) + self.async_write_ha_state() + if event is not None: + await self.control_queue_task.put(self) + + async def _trigger_time(self, event=None): + _check = await check_all_entities(self) + if _check is False: + return + _LOGGER.debug("better_thermostat %s: get last avg outdoor temps...", self.name) + await check_ambient_air_temperature(self) + self.async_write_ha_state() + if event is not None: + await self.control_queue_task.put(self) + + async def _trigger_temperature_change(self, event): + _check = await check_all_entities(self) + if _check is False: + return + self.async_set_context(event.context) + if (event.data.get("new_state")) is None: + return + self.hass.async_create_task(trigger_temperature_change(self, event)) + + async def _trigger_humidity_change(self, event): + _check = await check_all_entities(self) + if _check is False: + return + self.async_set_context(event.context) + if (event.data.get("new_state")) is None: + return + self.cur_humidity = convert_to_float( + str(self.hass.states.get(self.humidity_entity_id).state), + self.name, + "humidity_update", + ) + self.async_write_ha_state() + + async def _trigger_trv_change(self, event): + _check = await check_all_entities(self) + if _check is False: + return + self.async_set_context(event.context) + if self._async_unsub_state_changed is None: + return + + if (event.data.get("new_state")) is None: + return + + self.hass.async_create_task(trigger_trv_change(self, event)) + + async def _trigger_window_change(self, event): + _check = await check_all_entities(self) + if _check is False: + return + self.async_set_context(event.context) + if (event.data.get("new_state")) is None: + return + + self.hass.async_create_task(trigger_window_change(self, event)) + + async def startup(self): + """Run when entity about to be added. + + Returns + ------- + None + """ + while self.startup_running: + _LOGGER.info( + "better_thermostat %s: Starting version %s. Waiting for entity to be ready...", + self.name, + self.version, + ) + sensor_state = self.hass.states.get(self.sensor_entity_id) + if sensor_state is not None: + if sensor_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + _LOGGER.info( + "better_thermostat %s: waiting for sensor entity with id '%s' to become fully available...", + self.name, + self.sensor_entity_id, + ) + await asyncio.sleep(10) + continue + + try: + for trv in self.real_trvs.keys(): + trv_state = self.hass.states.get(trv) + if trv_state is None: + _LOGGER.info( + "better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...", + self.name, + trv, + ) + await asyncio.sleep(10) + raise ContinueLoop + if trv_state is not None: + if trv_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + _LOGGER.info( + "better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...", + self.name, + trv, + ) + await asyncio.sleep(10) + raise ContinueLoop + except ContinueLoop: + continue + + if self.window_id is not None: + if self.hass.states.get(self.window_id).state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + None, + ): + _LOGGER.info( + "better_thermostat %s: waiting for window sensor entity with id '%s' to become fully available...", + self.name, + self.window_id, + ) + await asyncio.sleep(10) + continue + + if self.humidity_entity_id is not None: + if self.hass.states.get(self.humidity_entity_id).state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + None, + ): + _LOGGER.info( + "better_thermostat %s: waiting for humidity sensor entity with id '%s' to become fully available...", + self.name, + self.humidity_entity_id, + ) + await asyncio.sleep(10) + continue + + if self.outdoor_sensor is not None: + if self.hass.states.get(self.outdoor_sensor).state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + None, + ): + _LOGGER.info( + "better_thermostat %s: waiting for outdoor sensor entity with id '%s' to become fully available...", + self.name, + self.outdoor_sensor, + ) + await asyncio.sleep(10) + continue + + if self.weather_entity is not None: + if self.hass.states.get(self.weather_entity).state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + None, + ): + _LOGGER.info( + "better_thermostat %s: waiting for weather entity with id '%s' to become fully available...", + self.name, + self.weather_entity, + ) + await asyncio.sleep(10) + continue + + states = [ + state + for entity_id in self.real_trvs + if (state := self.hass.states.get(entity_id)) is not None + ] + + self.bt_min_temp = reduce_attribute(states, ATTR_MIN_TEMP, reduce=max) + self.bt_max_temp = reduce_attribute(states, ATTR_MAX_TEMP, reduce=min) + self.bt_target_temp_step = reduce_attribute( + states, ATTR_TARGET_TEMP_STEP, reduce=max + ) + + self.all_entities.append(self.sensor_entity_id) + + self.cur_temp = convert_to_float( + str(sensor_state.state), self.name, "startup()" + ) + if self.humidity_entity_id is not None: + self.all_entities.append(self.humidity_entity_id) + self.cur_humidity = convert_to_float( + str(self.hass.states.get(self.humidity_entity_id).state), + self.name, + "startup()", + ) + if self.window_id is not None: + self.all_entities.append(self.window_id) + window = self.hass.states.get(self.window_id) + + check = window.state + if check in ("on", "open", "true"): + self.window_open = True + else: + self.window_open = False + _LOGGER.debug( + "better_thermostat %s: detected window state at startup: %s", + self.name, + "Open" if self.window_open else "Closed", + ) + else: + self.window_open = False + + # Check If we have an old state + old_state = await self.async_get_last_state() + if old_state is not None: + # If we have no initial temperature, restore + # If we have a previously saved temperature + if old_state.attributes.get(ATTR_TEMPERATURE) is None: + self.bt_target_temp = reduce_attribute( + states, ATTR_TEMPERATURE, reduce=lambda *data: mean(data) + ) + _LOGGER.debug( + "better_thermostat %s: Undefined target temperature, falling back to %s", + self.name, + self.bt_target_temp, + ) + else: + _oldtarget_temperature = float( + old_state.attributes.get(ATTR_TEMPERATURE) + ) + # if the saved temperature is lower than the min_temp, set it to min_temp + if _oldtarget_temperature < self.bt_min_temp: + _LOGGER.warning( + "better_thermostat %s: Saved target temperature %s is lower than min_temp %s, setting to min_temp", + self.name, + _oldtarget_temperature, + self.bt_min_temp, + ) + _oldtarget_temperature = self.bt_min_temp + # if the saved temperature is higher than the max_temp, set it to max_temp + elif _oldtarget_temperature > self.bt_max_temp: + _LOGGER.warning( + "better_thermostat %s: Saved target temperature %s is higher than max_temp %s, setting to max_temp", + self.name, + _oldtarget_temperature, + self.bt_min_temp, + ) + _oldtarget_temperature = self.bt_max_temp + self.bt_target_temp = convert_to_float( + str(_oldtarget_temperature), self.name, "startup()" + ) + if old_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + self.bt_hvac_mode = old_state.state + if old_state.attributes.get(ATTR_STATE_CALL_FOR_HEAT, None) is not None: + self.call_for_heat = old_state.attributes.get( + ATTR_STATE_CALL_FOR_HEAT + ) + if ( + old_state.attributes.get(ATTR_STATE_SAVED_TEMPERATURE, None) + is not None + ): + self._saved_temperature = convert_to_float( + str( + old_state.attributes.get(ATTR_STATE_SAVED_TEMPERATURE, None) + ), + self.name, + "startup()", + ) + if old_state.attributes.get(ATTR_STATE_HUMIDIY, None) is not None: + self.cur_humidity = old_state.attributes.get(ATTR_STATE_HUMIDIY) + if old_state.attributes.get(ATTR_STATE_MAIN_MODE, None) is not None: + self.last_main_hvac_mode = old_state.attributes.get( + ATTR_STATE_MAIN_MODE + ) + if old_state.attributes.get(ATTR_STATE_HEATING_POWER, None) is not None: + self.heating_power = float( + old_state.attributes.get(ATTR_STATE_HEATING_POWER) + ) + + else: + # No previous state, try and restore defaults + if self.bt_target_temp is None or type(self.bt_target_temp) != float: + _LOGGER.info( + "better_thermostat %s: No previously saved temperature found on startup, get it from the TRV", + self.name, + ) + self.bt_target_temp = reduce_attribute( + states, ATTR_TEMPERATURE, reduce=lambda *data: mean(data) + ) + + # if hvac mode could not be restored, turn heat off + if self.bt_hvac_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + current_hvac_modes = [ + x.state for x in states if x.state != HVACMode.OFF + ] + # return the most common hvac mode (what the thermostat is set to do) except OFF + if current_hvac_modes: + _temp_bt_hvac_mode = max( + set(current_hvac_modes), key=current_hvac_modes.count + ) + if _temp_bt_hvac_mode is not HVACMode.OFF: + self.bt_hvac_mode = HVACMode.HEAT + else: + self.bt_hvac_mode = HVACMode.OFF + _LOGGER.debug( + "better_thermostat %s: No previously hvac mode found on startup, turn bt to trv mode %s", + self.name, + self.bt_hvac_mode, + ) + # return off if all are off + elif all(x.state == HVACMode.OFF for x in states): + self.bt_hvac_mode = HVACMode.OFF + _LOGGER.debug( + "better_thermostat %s: No previously hvac mode found on startup, turn bt to trv mode %s", + self.name, + self.bt_hvac_mode, + ) + else: + _LOGGER.warning( + "better_thermostat %s: No previously hvac mode found on startup, turn heat off", + self.name, + ) + self.bt_hvac_mode = HVACMode.OFF + + _LOGGER.debug( + "better_thermostat %s: Startup config, BT hvac mode is %s, Target temp %s", + self.name, + self.bt_hvac_mode, + self.bt_target_temp, + ) + + if self.last_main_hvac_mode is None: + self.last_main_hvac_mode = self.bt_hvac_mode + + if self.humidity_entity_id is not None: + self.cur_humidity = convert_to_float( + str(self.hass.states.get(self.humidity_entity_id).state), + self.name, + "startup()", + ) + else: + self.cur_humidity = 0 + + self.last_window_state = self.window_open + if self.bt_hvac_mode not in (HVACMode.OFF, HVACMode.HEAT): + self.bt_hvac_mode = HVACMode.HEAT + + self.async_write_ha_state() + + for trv in self.real_trvs.keys(): + self.all_entities.append(trv) + await init(self, trv) + if self.real_trvs[trv]["calibration"] == 0: + self.real_trvs[trv]["last_calibration"] = await get_current_offset( + self, trv + ) + self.real_trvs[trv]["local_calibration_min"] = await get_min_offset( + self, trv + ) + self.real_trvs[trv]["local_calibration_max"] = await get_max_offset( + self, trv + ) + else: + self.real_trvs[trv]["last_calibration"] = 0 + + self.real_trvs[trv]["valve_position"] = convert_to_float( + str( + self.hass.states.get(trv).attributes.get("valve_position", None) + ), + self.name, + "startup", + ) + self.real_trvs[trv]["max_temp"] = convert_to_float( + str(self.hass.states.get(trv).attributes.get("max_temp", 30)), + self.name, + "startup", + ) + self.real_trvs[trv]["min_temp"] = convert_to_float( + str(self.hass.states.get(trv).attributes.get("min_temp", 5)), + self.name, + "startup", + ) + self.real_trvs[trv]["target_temp_step"] = convert_to_float( + str( + self.hass.states.get(trv).attributes.get("target_temp_step", 1) + ), + self.name, + "startup", + ) + self.real_trvs[trv]["temperature"] = convert_to_float( + str(self.hass.states.get(trv).attributes.get("temperature", 5)), + self.name, + "startup", + ) + self.real_trvs[trv]["hvac_modes"] = self.hass.states.get( + trv + ).attributes.get("hvac_modes", None) + self.real_trvs[trv]["hvac_mode"] = self.hass.states.get(trv).state + self.real_trvs[trv]["last_hvac_mode"] = self.hass.states.get(trv).state + self.real_trvs[trv]["last_temperature"] = convert_to_float( + str(self.hass.states.get(trv).attributes.get("temperature")), + self.name, + "startup()", + ) + self.real_trvs[trv]["current_temperature"] = convert_to_float( + str( + self.hass.states.get(trv).attributes.get("current_temperature") + or 5 + ), + self.name, + "startup()", + ) + await control_trv(self, trv) + + await self._trigger_time(None) + await self._trigger_check_weather(None) + self.startup_running = False + self._available = True + self.async_write_ha_state() + # + await asyncio.sleep(5) + + # try to find battery entities for all related entities + for entity in self.all_entities: + if entity is not None: + battery_id = await find_battery_entity(self, entity) + if battery_id is not None: + self.devices_states[entity] = { + "battery_id": battery_id, + "battery": None, + } + + # update_hvac_action(self) + # Add listener + if self.outdoor_sensor is not None: + self.all_entities.append(self.outdoor_sensor) + self.async_on_remove( + async_track_time_change(self.hass, self._trigger_time, 5, 0, 0) + ) + + await check_all_entities(self) + + self.async_on_remove( + async_track_time_interval( + self.hass, self._trigger_check_weather, timedelta(hours=1) + ) + ) + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.sensor_entity_id], self._trigger_temperature_change + ) + ) + if self.humidity_entity_id is not None: + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self.humidity_entity_id], + self._trigger_humidity_change, + ) + ) + if self._async_unsub_state_changed is None: + self._async_unsub_state_changed = async_track_state_change_event( + self.hass, self.entity_ids, self._trigger_trv_change + ) + self.async_on_remove(self._async_unsub_state_changed) + if self.window_id is not None: + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.window_id], self._trigger_window_change + ) + ) + _LOGGER.info("better_thermostat %s: startup completed.", self.name) + self.async_write_ha_state() + await self.async_update_ha_state(force_refresh=True) + break + + async def calculate_heating_power(self): + if ( + self.heating_start_temp is not None + and self.heating_end_temp is not None + and self.cur_temp < self.heating_end_temp + ): + _temp_diff = self.heating_end_temp - self.heating_start_temp + _time_diff_minutes = round( + (self.heating_end_timestamp - self.heating_start_timestamp).seconds + / 60.0, + 1, + ) + if _time_diff_minutes > 1: + _degrees_time = round(_temp_diff / _time_diff_minutes, 4) + self.heating_power = round( + (self.heating_power * 0.9 + _degrees_time * 0.1), 4 + ) + if len(self.last_heating_power_stats) >= 10: + self.last_heating_power_stats = self.last_heating_power_stats[ + len(self.last_heating_power_stats) - 9 : + ] + self.last_heating_power_stats.append( + { + "temp_diff": round(_temp_diff, 1), + "time": _time_diff_minutes, + "degrees_time": round(_degrees_time, 4), + "heating_power": round(self.heating_power, 4), + } + ) + _LOGGER.debug( + f"better_thermostat {self.name}: calculate_heating_power / temp_diff: {round(_temp_diff, 1)} - time: {_time_diff_minutes} - degrees_time: {round(_degrees_time, 4)} - heating_power: {round(self.heating_power, 4)} - heating_power_stats: {self.last_heating_power_stats}" + ) + # reset for the next heating start + self.heating_start_temp = None + self.heating_end_temp = None + + # heating starts + if ( + self.attr_hvac_action == HVACAction.HEATING + and self.old_attr_hvac_action != HVACAction.HEATING + ): + self.heating_start_temp = self.cur_temp + self.heating_start_timestamp = datetime.now() + # heating stops + elif ( + self.attr_hvac_action != HVACAction.HEATING + and self.old_attr_hvac_action == HVACAction.HEATING + and self.heating_start_temp is not None + and self.heating_end_temp is None + ): + self.heating_end_temp = self.cur_temp + self.heating_end_timestamp = datetime.now() + # check if the temp is still rising, after heating stopped + elif ( + self.attr_hvac_action != HVACAction.HEATING + and self.heating_start_temp is not None + and self.heating_end_temp is not None + and self.cur_temp > self.heating_end_temp + ): + self.heating_end_temp = self.cur_temp + + if self.attr_hvac_action != self.old_attr_hvac_action: + self.old_attr_hvac_action = self.attr_hvac_action + self.async_write_ha_state() + + @property + def extra_state_attributes(self): + """Return the device specific state attributes. + + Returns + ------- + dict + Attribute dictionary for the extra device specific state attributes. + """ + dev_specific = { + ATTR_STATE_WINDOW_OPEN: self.window_open, + ATTR_STATE_CALL_FOR_HEAT: self.call_for_heat, + ATTR_STATE_LAST_CHANGE: self.last_change, + ATTR_STATE_SAVED_TEMPERATURE: self._saved_temperature, + ATTR_STATE_HUMIDIY: self.cur_humidity, + ATTR_STATE_MAIN_MODE: self.last_main_hvac_mode, + ATTR_STATE_HEATING_POWER: self.heating_power, + ATTR_STATE_ERRORS: json.dumps(self.devices_errors), + ATTR_STATE_BATTERIES: json.dumps(self.devices_states), + } + + return dev_specific + + @property + def available(self): + """Return if thermostat is available. + + Returns + ------- + bool + True if the thermostat is available. + """ + return self._available + + @property + def should_poll(self): + """Return the polling state. + + Returns + ------- + bool + True if the thermostat uses polling. + """ + return False + + @property + def name(self): + """Return the name of the thermostat. + + Returns + ------- + string + The name of the thermostat. + """ + return self._name + + @property + def unique_id(self): + """Return the unique id of this thermostat. + + Returns + ------- + string + The unique id of this thermostat. + """ + return self._unique_id + + @property + def precision(self): + """Return the precision of the system. + + Returns + ------- + float + Precision of the thermostat. + """ + return super().precision + + @property + def target_temperature_step(self): + """Return the supported step of target temperature. + + Returns + ------- + float + Steps of target temperature. + """ + if self.bt_target_temp_step is not None: + return self.bt_target_temp_step + + return super().precision + + @property + def temperature_unit(self): + """Return the unit of measurement. + + Returns + ------- + string + The unit of measurement. + """ + return self._unit + + @property + def current_temperature(self): + """Return the sensor temperature. + + Returns + ------- + float + The measured temperature. + """ + return self.cur_temp + + @property + def hvac_mode(self): + """Return current operation. + + Returns + ------- + string + HVAC mode only from homeassistant.components.climate.const is valid + """ + return self.bt_hvac_mode + + @property + def hvac_action(self): + """Return the current HVAC action""" + if ( + self.attr_hvac_action is None + and self.bt_target_temp is not None + and self.cur_temp is not None + ): + if self.bt_target_temp > self.cur_temp and self.window_open is False: + self.attr_hvac_action = HVACAction.HEATING + else: + self.attr_hvac_action = HVACAction.IDLE + return self.attr_hvac_action + + @property + def target_temperature(self): + """Return the temperature we try to reach. + + Returns + ------- + float + Target temperature. + """ + if None in (self.bt_max_temp, self.bt_min_temp, self.bt_target_temp): + return self.bt_target_temp + # if target temp is below minimum, return minimum + if self.bt_target_temp < self.bt_min_temp: + return self.bt_min_temp + # if target temp is above maximum, return maximum + if self.bt_target_temp > self.bt_max_temp: + return self.bt_max_temp + return self.bt_target_temp + + @property + def hvac_modes(self): + """List of available operation modes. + + Returns + ------- + array + A list of HVAC modes only from homeassistant.components.climate.const is valid + """ + return self._hvac_list + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode. + + Returns + ------- + None + """ + if hvac_mode in (HVACMode.HEAT, HVACMode.OFF): + self.bt_hvac_mode = hvac_mode + else: + _LOGGER.error( + "better_thermostat %s: Unsupported hvac_mode %s", self.name, hvac_mode + ) + self.async_write_ha_state() + await self.control_queue_task.put(self) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature. + + Parameters + ---------- + kwargs : + Arguments piped from HA. + + Returns + ------- + None + """ + _new_setpoint = convert_to_float( + str(kwargs.get(ATTR_TEMPERATURE, None)), + self.name, + "controlling.settarget_temperature()", + ) + if _new_setpoint is None: + _LOGGER.debug( + f"better_thermostat {self.name}: received a new setpoint from HA, but temperature attribute was not set, ignoring" + ) + return + self.bt_target_temp = _new_setpoint + self.async_write_ha_state() + await self.control_queue_task.put(self) + + @property + def min_temp(self): + """Return the minimum temperature. + + Returns + ------- + float + the minimum temperature. + """ + if self.bt_min_temp is not None: + return self.bt_min_temp + + # get default temp from super class + return super().min_temp + + @property + def max_temp(self): + """Return the maximum temperature. + + Returns + ------- + float + the maximum temperature. + """ + if self.bt_max_temp is not None: + return self.bt_max_temp + + # Get default temp from super class + return super().max_temp + + @property + def _is_device_active(self): + """Get the current state of the device for HA. + + Returns + ------- + string + State of the device. + """ + if self.bt_hvac_mode == HVACMode.OFF: + return False + if self.window_open: + return False + return True + + @property + def supported_features(self): + """Return the list of supported features. + + Returns + ------- + array + Supported features. + """ + return self._support_flags diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py new file mode 100644 index 00000000..8142a02b --- /dev/null +++ b/custom_components/better_thermostat/config_flow.py @@ -0,0 +1,625 @@ +import logging +import voluptuous as vol +from collections import OrderedDict + +from .utils.bridge import load_adapter + +from .utils.helpers import get_device_model, get_trv_intigration + +from .const import ( + CONF_PROTECT_OVERHEATING, + CONF_CALIBRATION, + CONF_CHILD_LOCK, + CONF_HEAT_AUTO_SWAPPED, + CONF_HEATER, + CONF_HOMATICIP, + CONF_HUMIDITY, + CONF_MODEL, + CONF_NO_SYSTEM_MODE_OFF, + CONF_OFF_TEMPERATURE, + CONF_OUTDOOR_SENSOR, + CONF_SENSOR, + CONF_SENSOR_WINDOW, + CONF_VALVE_MAINTENANCE, + CONF_WEATHER, + CONF_WINDOW_TIMEOUT, + CONF_CALIBRATION_MODE, + CalibrationMode, + CalibrationType, +) +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import selector +from homeassistant.components.climate.const import HVACMode +from homeassistant.helpers import config_validation as cv + + +from . import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +CALIBRATION_TYPE_SELECTOR = selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict( + value=CalibrationType.TARGET_TEMP_BASED, + label="Target Temperature Based", + ) + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) +) + +CALIBRATION_TYPE_ALL_SELECTOR = selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict( + value=CalibrationType.TARGET_TEMP_BASED, + label="Target Temperature Based", + ), + selector.SelectOptionDict( + value=CalibrationType.LOCAL_BASED, label="Offset Based" + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) +) + +CALIBRATION_MODE_SELECTOR = selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict(value=CalibrationMode.DEFAULT, label="Normal"), + selector.SelectOptionDict( + value=CalibrationMode.FIX_CALIBRATION, label="Agressive" + ), + selector.SelectOptionDict( + value=CalibrationMode.HEATING_POWER_CALIBRATION, label="AI Time Based" + ), + selector.SelectOptionDict( + value=CalibrationMode.NO_CALIBRATION, label="No Calibration" + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 5 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the config flow.""" + self.name = "" + self.data = None + self.model = None + self.heater_entity_id = None + self.trv_bundle = [] + self.integration = None + self.i = 0 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + """Get the options flow for this handler.""" + + async def async_step_confirm(self, user_input=None, confirm_type=None): + """Handle user-confirmation of discovered node.""" + errors = {} + self.data[CONF_HEATER] = self.trv_bundle + if user_input is not None: + if self.data is not None: + _LOGGER.debug("Confirm: %s", self.data[CONF_HEATER]) + await self.async_set_unique_id(self.data["name"]) + return self.async_create_entry(title=self.data["name"], data=self.data) + if confirm_type is not None: + errors["base"] = confirm_type + _trvs = ",".join([x["trv"] for x in self.data[CONF_HEATER]]) + return self.async_show_form( + step_id="confirm", + errors=errors, + description_placeholders={"name": self.data[CONF_NAME], "trv": _trvs}, + ) + + async def async_step_advanced(self, user_input=None, _trv_config=None): + """Handle options flow.""" + if user_input is not None: + self.trv_bundle[self.i]["advanced"] = user_input + self.trv_bundle[self.i]["adapter"] = None + + self.i += 1 + if len(self.trv_bundle) > self.i: + return await self.async_step_advanced(None, self.trv_bundle[self.i]) + + _has_off_mode = True + for trv in self.trv_bundle: + if HVACMode.OFF not in self.hass.states.get( + trv.get("trv") + ).attributes.get("hvac_modes"): + _has_off_mode = False + + if _has_off_mode is False: + return await self.async_step_confirm(None, "no_off_mode") + return await self.async_step_confirm() + + user_input = user_input or {} + homematic = False + if _trv_config.get("integration").find("homematic") != -1: + homematic = True + + fields = OrderedDict() + + _default_calibration = "target_temp_based" + _adapter = _trv_config.get("adapter", None) + if _adapter is not None: + _info = await _adapter.get_info(self, _trv_config.get("trv")) + + if _info.get("support_offset", False): + _default_calibration = "local_calibration_based" + + if _default_calibration == "local_calibration_based": + fields[ + vol.Required( + CONF_CALIBRATION, + default=user_input.get(CONF_CALIBRATION, _default_calibration), + ) + ] = CALIBRATION_TYPE_ALL_SELECTOR + else: + fields[ + vol.Required( + CONF_CALIBRATION, + default=user_input.get(CONF_CALIBRATION, _default_calibration), + ) + ] = CALIBRATION_TYPE_SELECTOR + + fields[ + vol.Required( + CONF_CALIBRATION_MODE, + default=user_input.get( + CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION + ), + ) + ] = CALIBRATION_MODE_SELECTOR + + fields[ + vol.Optional( + CONF_PROTECT_OVERHEATING, + default=user_input.get(CONF_PROTECT_OVERHEATING, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_NO_SYSTEM_MODE_OFF, + default=user_input.get(CONF_NO_SYSTEM_MODE_OFF, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_HEAT_AUTO_SWAPPED, + default=user_input.get(CONF_HEAT_AUTO_SWAPPED, False), + ) + ] = bool + + if _info.get("support_valve", False): + fields[ + vol.Optional( + CONF_VALVE_MAINTENANCE, + default=user_input.get(CONF_VALVE_MAINTENANCE, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_CHILD_LOCK, default=user_input.get(CONF_CHILD_LOCK, False) + ) + ] = bool + fields[ + vol.Optional( + CONF_HOMATICIP, default=user_input.get(CONF_HOMATICIP, homematic) + ) + ] = bool + + return self.async_show_form( + step_id="advanced", + data_schema=vol.Schema(fields), + last_step=False, + description_placeholders={"trv": _trv_config.get("trv")}, + ) + + async def async_step_user(self, user_input=None): + errors = {} + + if user_input is not None: + if self.data is None: + self.data = user_input + self.heater_entity_id = self.data[CONF_HEATER] + if self.data[CONF_NAME] == "": + errors["base"] = "no_name" + if CONF_SENSOR_WINDOW not in self.data: + self.data[CONF_SENSOR_WINDOW] = None + if CONF_HUMIDITY not in self.data: + self.data[CONF_HUMIDITY] = None + if CONF_OUTDOOR_SENSOR not in self.data: + self.data[CONF_OUTDOOR_SENSOR] = None + if CONF_WEATHER not in self.data: + self.data[CONF_WEATHER] = None + + if CONF_WINDOW_TIMEOUT in self.data: + self.data[CONF_WINDOW_TIMEOUT] = ( + int( + cv.time_period_dict( + user_input.get(CONF_WINDOW_TIMEOUT, None) + ).total_seconds() + ) + or 0 + ) + else: + self.data[CONF_WINDOW_TIMEOUT] = 0 + + if "base" not in errors: + for trv in self.heater_entity_id: + _intigration = await get_trv_intigration(self, trv) + self.trv_bundle.append( + { + "trv": trv, + "integration": _intigration, + "model": await get_device_model(self, trv), + "adapter": load_adapter(self, _intigration, trv), + } + ) + self.data[CONF_MODEL] = "/".join([x["model"] for x in self.trv_bundle]) + return await self.async_step_advanced(None, self.trv_bundle[0]) + + user_input = user_input or {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Optional(CONF_NAME, default=user_input.get(CONF_NAME, "")): str, + vol.Required(CONF_HEATER): selector.EntitySelector( + selector.EntitySelectorConfig(domain="climate", multiple=True) + ), + vol.Required(CONF_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "number", "input_number"], + device_class="temperature", + multiple=False, + ) + ), + vol.Optional(CONF_HUMIDITY): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "number", "input_number"], + device_class="humidity", + multiple=False, + ) + ), + vol.Optional(CONF_OUTDOOR_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "input_number", "number"], + device_class="temperature", + multiple=False, + ) + ), + vol.Optional(CONF_SENSOR_WINDOW): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=[ + "group", + "sensor", + "input_boolean", + "binary_sensor", + ], + multiple=False, + ) + ), + vol.Optional(CONF_WEATHER): selector.EntitySelector( + selector.EntitySelectorConfig(domain="weather", multiple=False) + ), + vol.Optional(CONF_WINDOW_TIMEOUT): selector.DurationSelector(), + vol.Optional( + CONF_OFF_TEMPERATURE, + default=user_input.get(CONF_OFF_TEMPERATURE, 20), + ): int, + } + ), + errors=errors, + last_step=False, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for a config entry.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + self.i = 0 + self.trv_bundle = [] + self.name = "" + self._last_step = False + self.updated_config = {} + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_advanced( + self, user_input=None, _trv_config=None, _update_config=None + ): + """Manage the advanced options.""" + if user_input is not None: + self.trv_bundle[self.i]["advanced"] = user_input + self.trv_bundle[self.i]["adapter"] = None + + self.i += 1 + if len(self.trv_bundle) - 1 >= self.i: + self._last_step = True + + if len(self.trv_bundle) > self.i: + return await self.async_step_advanced( + None, self.trv_bundle[self.i], _update_config + ) + + self.updated_config[CONF_HEATER] = self.trv_bundle + _LOGGER.debug("Updated config: %s", self.updated_config) + self.hass.config_entries.async_update_entry( + self.config_entry, data=self.updated_config + ) + return self.async_create_entry( + title=self.updated_config["name"], data=self.updated_config + ) + + user_input = user_input or {} + homematic = False + if _trv_config.get("integration").find("homematic") != -1: + homematic = True + + fields = OrderedDict() + + _default_calibration = "target_temp_based" + self.name = user_input.get(CONF_NAME, "-") + + _adapter = load_adapter( + self, _trv_config.get("integration"), _trv_config.get("trv") + ) + if _adapter is not None: + _info = await _adapter.get_info(self, _trv_config.get("trv")) + + if _info.get("support_offset", False): + _default_calibration = "local_calibration_based" + + if _default_calibration == "local_calibration_based": + fields[ + vol.Required( + CONF_CALIBRATION, + default=user_input.get( + CONF_CALIBRATION, + _trv_config["advanced"].get( + CONF_CALIBRATION, _default_calibration + ), + ), + ) + ] = CALIBRATION_TYPE_ALL_SELECTOR + else: + fields[ + vol.Required( + CONF_CALIBRATION, + default=user_input.get( + CONF_CALIBRATION, + _trv_config["advanced"].get( + CONF_CALIBRATION, _default_calibration + ), + ), + ) + ] = CALIBRATION_TYPE_SELECTOR + + fields[ + vol.Required( + CONF_CALIBRATION_MODE, + default=_trv_config["advanced"].get( + CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION + ), + ) + ] = CALIBRATION_MODE_SELECTOR + + fields[ + vol.Optional( + CONF_PROTECT_OVERHEATING, + default=_trv_config["advanced"].get(CONF_PROTECT_OVERHEATING, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_NO_SYSTEM_MODE_OFF, + default=_trv_config["advanced"].get(CONF_NO_SYSTEM_MODE_OFF, False), + ) + ] = bool + + has_auto = False + trv = self.hass.states.get(_trv_config.get("trv")) + if HVACMode.AUTO in trv.attributes.get("hvac_modes"): + has_auto = True + + fields[ + vol.Optional( + CONF_HEAT_AUTO_SWAPPED, + default=_trv_config["advanced"].get(CONF_HEAT_AUTO_SWAPPED, has_auto), + ) + ] = bool + + if _info.get("support_valve", False): + fields[ + vol.Optional( + CONF_VALVE_MAINTENANCE, + default=_trv_config["advanced"].get(CONF_VALVE_MAINTENANCE, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_CHILD_LOCK, + default=_trv_config["advanced"].get(CONF_CHILD_LOCK, False), + ) + ] = bool + fields[ + vol.Optional( + CONF_HOMATICIP, + default=_trv_config["advanced"].get(CONF_HOMATICIP, homematic), + ) + ] = bool + + return self.async_show_form( + step_id="advanced", + data_schema=vol.Schema(fields), + last_step=self._last_step, + description_placeholders={"trv": _trv_config.get("trv")}, + ) + + async def async_step_user(self, user_input=None): + if user_input is not None: + current_config = self.config_entry.data + self.updated_config = dict(current_config) + self.updated_config[CONF_SENSOR] = user_input.get(CONF_SENSOR, None) + self.updated_config[CONF_SENSOR_WINDOW] = user_input.get( + CONF_SENSOR_WINDOW, None + ) + self.updated_config[CONF_HUMIDITY] = user_input.get(CONF_HUMIDITY, None) + self.updated_config[CONF_OUTDOOR_SENSOR] = user_input.get( + CONF_OUTDOOR_SENSOR, None + ) + self.updated_config[CONF_WEATHER] = user_input.get(CONF_WEATHER, None) + + if CONF_WINDOW_TIMEOUT in self.updated_config: + self.updated_config[CONF_WINDOW_TIMEOUT] = ( + int( + cv.time_period_dict( + user_input.get(CONF_WINDOW_TIMEOUT, None) + ).total_seconds() + ) + or 0 + ) + else: + self.updated_config[CONF_WINDOW_TIMEOUT] = 0 + + self.updated_config[CONF_OFF_TEMPERATURE] = user_input.get( + CONF_OFF_TEMPERATURE + ) + + for trv in self.updated_config[CONF_HEATER]: + trv["adapter"] = None + self.trv_bundle.append(trv) + + return await self.async_step_advanced( + None, self.trv_bundle[0], self.updated_config + ) + + fields = OrderedDict() + + fields[ + vol.Optional( + CONF_SENSOR, + description={ + "suggested_value": self.config_entry.data.get(CONF_SENSOR, "") + }, + ) + ] = selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "number", "input_number"], + device_class="temperature", + multiple=False, + ) + ) + + fields[ + vol.Optional( + CONF_HUMIDITY, + description={ + "suggested_value": self.config_entry.data.get(CONF_HUMIDITY, "") + }, + ) + ] = selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "number", "input_number"], + device_class="humidity", + multiple=False, + ) + ) + + fields[ + vol.Optional( + CONF_SENSOR_WINDOW, + description={ + "suggested_value": self.config_entry.data.get( + CONF_SENSOR_WINDOW, "" + ) + }, + ) + ] = selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["group", "sensor", "input_boolean", "binary_sensor"], + multiple=False, + ) + ) + + fields[ + vol.Optional( + CONF_OUTDOOR_SENSOR, + description={ + "suggested_value": self.config_entry.data.get( + CONF_OUTDOOR_SENSOR, "" + ) + }, + ) + ] = selector.EntitySelector( + selector.EntitySelectorConfig( + domain=["sensor", "input_number", "number"], + device_class="temperature", + multiple=False, + ) + ) + + fields[ + vol.Optional( + CONF_WEATHER, + description={ + "suggested_value": self.config_entry.data.get(CONF_WEATHER, "") + }, + ) + ] = selector.EntitySelector( + selector.EntitySelectorConfig(domain="weather", multiple=False) + ) + + _timeout = self.config_entry.data.get(CONF_WINDOW_TIMEOUT, 0) + _timeout = str(cv.time_period_seconds(_timeout)) + _timeout = { + "hours": int(_timeout.split(":", maxsplit=1)[0]), + "minutes": int(_timeout.split(":")[1]), + "seconds": int(_timeout.split(":")[2]), + } + fields[ + vol.Optional( + CONF_WINDOW_TIMEOUT, + default=_timeout, + description={"suggested_value": _timeout}, + ) + ] = selector.DurationSelector() + + fields[ + vol.Optional( + CONF_OFF_TEMPERATURE, + default=self.config_entry.data.get(CONF_OFF_TEMPERATURE, 5), + ) + ] = int + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(fields), last_step=False + ) diff --git a/custom_components/better_thermostat/const.py b/custom_components/better_thermostat/const.py new file mode 100644 index 00000000..073490a8 --- /dev/null +++ b/custom_components/better_thermostat/const.py @@ -0,0 +1,99 @@ +"""""" +import json +from enum import IntEnum +from homeassistant.backports.enum import StrEnum + +import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, +) +from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_NAME = "Better Thermostat" +VERSION = "master" +try: + with open("custom_components/better_thermostat/manifest.json") as manifest_file: + manifest = json.load(manifest_file) + VERSION = manifest["version"] +except (FileNotFoundError, KeyError, json.JSONDecodeError) as e: + _LOGGER.error("better_thermostat %s: could not read version from manifest file.", e) + + +CONF_HEATER = "thermostat" +CONF_SENSOR = "temperature_sensor" +CONF_HUMIDITY = "humidity_sensor" +CONF_SENSOR_WINDOW = "window_sensors" +CONF_TARGET_TEMP = "target_temp" +CONF_WEATHER = "weather" +CONF_OFF_TEMPERATURE = "off_temperature" +CONF_WINDOW_TIMEOUT = "window_off_delay" +CONF_OUTDOOR_SENSOR = "outdoor_sensor" +CONF_VALVE_MAINTENANCE = "valve_maintenance" +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" +CONF_PRECISION = "precision" +CONF_CALIBRATION = "calibration" +CONF_CHILD_LOCK = "child_lock" +CONF_PROTECT_OVERHEATING = "protect_overheating" +CONF_CALIBRATION_MODE = "calibration_mode" +CONF_FIX_CALIBRATION = "fix_calibration" +CONF_HEATING_POWER_CALIBRATION = "heating_power_calibration" +CONF_NO_CALIBRATION = "no_calibration" +CONF_HEAT_AUTO_SWAPPED = "heat_auto_swapped" +CONF_MODEL = "model" +CONF_HOMATICIP = "homaticip" +CONF_INTEGRATION = "integration" +CONF_NO_SYSTEM_MODE_OFF = "no_off_system_mode" +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + +ATTR_STATE_WINDOW_OPEN = "window_open" +ATTR_STATE_CALL_FOR_HEAT = "call_for_heat" +ATTR_STATE_LAST_CHANGE = "last_change" +ATTR_STATE_SAVED_TEMPERATURE = "saved_temperature" +ATTR_VALVE_POSITION = "valve_position" +ATTR_STATE_HUMIDIY = "humidity" +ATTR_STATE_MAIN_MODE = "main_mode" +ATTR_STATE_HEATING_POWER = "heating_power" +ATTR_STATE_HEATING_STATS = "heating_stats" +ATTR_STATE_ERRORS = "errors" +ATTR_STATE_BATTERIES = "batteries" + +SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE = "restore_saved_target_temperature" +SERVICE_SET_TEMP_TARGET_TEMPERATURE = "set_temp_target_temperature" +SERVICE_RESET_HEATING_POWER = "reset_heating_power" + +BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA = vol.All( + cv.has_at_least_one_key(ATTR_TEMPERATURE), + make_entity_service_schema( + {vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float)} + ), +) + + +class BetterThermostatEntityFeature(IntEnum): + """Supported features of the climate entity.""" + + TARGET_TEMPERATURE = 1 + TARGET_TEMPERATURE_RANGE = 2 + + +class CalibrationType(StrEnum): + """Calibration type""" + + TARGET_TEMP_BASED = "target_temp_based" + LOCAL_BASED = "local_calibration_based" + + +class CalibrationMode(StrEnum): + """Calibration mode.""" + + DEFAULT = "default" + FIX_CALIBRATION = "fix_calibration" + HEATING_POWER_CALIBRATION = "heating_power_calibration" + NO_CALIBRATION = "no_calibration" diff --git a/custom_components/better_thermostat/device_trigger.py b/custom_components/better_thermostat/device_trigger.py new file mode 100644 index 00000000..aa134f6d --- /dev/null +++ b/custom_components/better_thermostat/device_trigger.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import voluptuous as vol +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry + +from homeassistant.components.homeassistant.triggers import ( + numeric_state as numeric_state_trigger, + state as state_trigger, +) +from . import DOMAIN +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_CURRENT_HUMIDITY, + HVAC_MODES, +) +from homeassistant.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_FOR, + CONF_PLATFORM, + CONF_TYPE, + PERCENTAGE, +) + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device triggers for Climate devices.""" + registry = entity_registry.async_get(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # Add triggers for each entity that belongs to this integration + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + triggers.append({**base_trigger, CONF_TYPE: "hvac_mode_changed"}) + + if state and ATTR_CURRENT_TEMPERATURE in state.attributes: + triggers.append({**base_trigger, CONF_TYPE: "current_temperature_changed"}) + + if state and ATTR_CURRENT_HUMIDITY in state.attributes: + triggers.append({**base_trigger, CONF_TYPE: "current_humidity_changed"}) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + if (trigger_type := config[CONF_TYPE]) == "hvac_mode_changed": + state_config = { + state_trigger.CONF_PLATFORM: "state", + state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: config[state_trigger.CONF_TO], + state_trigger.CONF_FROM: [ + mode for mode in HVAC_MODES if mode != config[state_trigger.CONF_TO] + ], + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + state_config = await state_trigger.async_validate_trigger_config( + hass, state_config + ) + return await state_trigger.async_attach_trigger( + hass, state_config, action, trigger_info, platform_type="device" + ) + + numeric_state_config = { + numeric_state_trigger.CONF_PLATFORM: "numeric_state", + numeric_state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + if trigger_type == "current_temperature_changed": + numeric_state_config[ + numeric_state_trigger.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_temperature }}" + else: + numeric_state_config[ + numeric_state_trigger.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_humidity }}" + + if CONF_ABOVE in config: + numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[CONF_BELOW] = config[CONF_BELOW] + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) + return await numeric_state_trigger.async_attach_trigger( + hass, numeric_state_config, action, trigger_info, platform_type="device" + ) + + +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + trigger_type = config[CONF_TYPE] + + if trigger_type == "hvac_action_changed": + return {} + + if trigger_type == "hvac_mode_changed": + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + if trigger_type == "current_temperature_changed": + unit_of_measurement = hass.config.units.temperature_unit + else: + unit_of_measurement = PERCENTAGE + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/custom_components/better_thermostat/diagnostics.py b/custom_components/better_thermostat/diagnostics.py new file mode 100644 index 00000000..f13b74dc --- /dev/null +++ b/custom_components/better_thermostat/diagnostics.py @@ -0,0 +1,51 @@ +"""Diagnostics support for Brother.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .utils.bridge import load_adapter + +from .const import CONF_HEATER, CONF_SENSOR, CONF_SENSOR_WINDOW + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + trvs = {} + for trv_id in config_entry.data[CONF_HEATER]: + trv = hass.states.get(trv_id["trv"]) + if trv is None: + continue + _adapter_name = load_adapter(hass, trv_id["integration"], trv_id["trv"], True) + trv_id["adapter"] = _adapter_name + trvs[trv_id["trv"]] = { + "name": trv.name, + "state": trv.state, + "attributes": trv.attributes, + "bt_config": trv_id["advanced"], + "bt_adapter": trv_id["adapter"], + "bt_integration": trv_id["integration"], + "model": trv_id["model"], + } + external_temperature = hass.states.get(config_entry.data[CONF_SENSOR]) + + window = "-" + window_entity_id = config_entry.data.get(CONF_SENSOR_WINDOW, False) + if window_entity_id: + try: + window = hass.states.get(window_entity_id) + except KeyError: + pass + + _cleaned_data = dict(config_entry.data.copy()) + del _cleaned_data[CONF_HEATER] + diagnostics_data = { + "info": _cleaned_data, + "thermostat": trvs, + "external_temperature_sensor": external_temperature, + "window_sensor": window, + } + + return diagnostics_data diff --git a/custom_components/better_thermostat/events/temperature.py b/custom_components/better_thermostat/events/temperature.py new file mode 100644 index 00000000..762846be --- /dev/null +++ b/custom_components/better_thermostat/events/temperature.py @@ -0,0 +1,62 @@ +import logging + +from custom_components.better_thermostat.const import CONF_HOMATICIP +from ..utils.helpers import convert_to_float +from datetime import datetime + +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + + +@callback +async def trigger_temperature_change(self, event): + """Handle temperature changes. + + Parameters + ---------- + self : + self instance of better_thermostat + event : + Event object from the eventbus. Contains the current trigger time. + + Returns + ------- + None + """ + if self.startup_running: + return + + new_state = event.data.get("new_state") + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return + + _incoming_temperature = convert_to_float( + str(new_state.state), self.name, "external_temperature" + ) + + _time_diff = 5 + + try: + for trv in self.all_trvs: + if trv["advanced"][CONF_HOMATICIP]: + _time_diff = 600 + except KeyError: + pass + + if ( + _incoming_temperature != self.cur_temp + and (datetime.now() - self.last_external_sensor_change).total_seconds() + > _time_diff + ): + _LOGGER.debug( + "better_thermostat %s: external_temperature changed from %s to %s", + self.name, + self.cur_temp, + _incoming_temperature, + ) + self.cur_temp = _incoming_temperature + self.last_external_sensor_change = datetime.now() + self.async_write_ha_state() + await self.control_queue_task.put(self) diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py new file mode 100644 index 00000000..e92bc5e3 --- /dev/null +++ b/custom_components/better_thermostat/events/trv.py @@ -0,0 +1,368 @@ +import asyncio +from datetime import datetime +import logging +from typing import Union +from custom_components.better_thermostat.const import CONF_HOMATICIP + +from homeassistant.components.climate.const import ( + HVACMode, + ATTR_HVAC_ACTION, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, +) +from homeassistant.core import State, callback +from ..utils.helpers import ( + calculate_local_setpoint_delta, + calculate_setpoint_override, + convert_to_float, + mode_remap, + round_to_half_degree, +) +from custom_components.better_thermostat.utils.bridge import get_current_offset + +_LOGGER = logging.getLogger(__name__) + + +@callback +async def trigger_trv_change(self, event): + """Trigger a change in the trv state.""" + if self.startup_running: + return + if self.control_queue_task is None: + return + asyncio.create_task(update_hvac_action(self)) + _main_change = False + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + entity_id = event.data.get("entity_id") + + if None in (new_state, old_state, new_state.attributes): + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} update contained not all necessary data for processing, skipping" + ) + return + + if not isinstance(new_state, State) or not isinstance(old_state, State): + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} update contained not a State, skipping" + ) + return + # set context HACK TO FIND OUT IF AN EVENT WAS SEND BY BT + + # Check if the update is coming from the code + if self.context == event.context: + return + + _LOGGER.debug(f"better_thermostat {self.name}: TRV {entity_id} update received") + + _org_trv_state = self.hass.states.get(entity_id) + child_lock = self.real_trvs[entity_id]["advanced"].get("child_lock") + + _new_current_temp = convert_to_float( + str(_org_trv_state.attributes.get("current_temperature", None)), + self.name, + "TRV_current_temp", + ) + + _time_diff = 5 + try: + for trv in self.all_trvs: + if trv["advanced"][CONF_HOMATICIP]: + _time_diff = 600 + except KeyError: + pass + if ( + _new_current_temp is not None + and self.real_trvs[entity_id]["current_temperature"] != _new_current_temp + and ( + (datetime.now() - self.last_internal_sensor_change).total_seconds() + > _time_diff + or ( + self.real_trvs[entity_id]["calibration_received"] is False + and self.real_trvs[entity_id]["calibration"] == 0 + ) + ) + ): + _old_temp = self.real_trvs[entity_id]["current_temperature"] + self.real_trvs[entity_id]["current_temperature"] = _new_current_temp + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} sends new internal temperature from {_old_temp} to {_new_current_temp}" + ) + self.last_internal_sensor_change = datetime.now() + _main_change = True + + # TODO: async def in controlling? + if self.real_trvs[entity_id]["calibration_received"] is False: + self.real_trvs[entity_id]["calibration_received"] = True + _LOGGER.debug( + f"better_thermostat {self.name}: calibration accepted by TRV {entity_id}" + ) + _main_change = False + self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"] + self.old_external_temp = self.cur_temp + if self.real_trvs[entity_id]["calibration"] == 0: + self.real_trvs[entity_id][ + "last_calibration" + ] = await get_current_offset(self, entity_id) + + if self.ignore_states: + return + + try: + mapped_state = convert_inbound_states(self, entity_id, _org_trv_state) + except TypeError: + _LOGGER.debug( + f"better_thermostat {self.name}: remapping TRV {entity_id} state failed, skipping" + ) + return + + if mapped_state in (HVACMode.OFF, HVACMode.HEAT): + if ( + self.real_trvs[entity_id]["hvac_mode"] != _org_trv_state.state + and not child_lock + ): + _old = self.real_trvs[entity_id]["hvac_mode"] + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} decoded TRV mode changed from {_old} to {_org_trv_state.state} - converted {new_state.state}" + ) + self.real_trvs[entity_id]["hvac_mode"] = _org_trv_state.state + _main_change = True + if ( + child_lock is False + and self.real_trvs[entity_id]["system_mode_received"] is True + and self.real_trvs[entity_id]["last_hvac_mode"] != _org_trv_state.state + ): + self.bt_hvac_mode = mapped_state + + _old_heating_setpoint = convert_to_float( + str(old_state.attributes.get("temperature", None)), + self.name, + "trigger_trv_change()", + ) + _new_heating_setpoint = convert_to_float( + str(new_state.attributes.get("temperature", None)), + self.name, + "trigger_trv_change()", + ) + if ( + _new_heating_setpoint is not None + and _old_heating_setpoint is not None + and self.bt_hvac_mode is not HVACMode.OFF + ): + _LOGGER.debug( + f"better_thermostat {self.name}: trigger_trv_change / _old_heating_setpoint: {_old_heating_setpoint} - _new_heating_setpoint: {_new_heating_setpoint} - _last_temperature: {self.real_trvs[entity_id]['last_temperature']}" + ) + if ( + _new_heating_setpoint < self.bt_min_temp + or self.bt_max_temp < _new_heating_setpoint + ): + _LOGGER.warning( + f"better_thermostat {self.name}: New TRV {entity_id} setpoint outside of range, overwriting it" + ) + + if _new_heating_setpoint < self.bt_min_temp: + _new_heating_setpoint = self.bt_min_temp + else: + _new_heating_setpoint = self.bt_max_temp + + if ( + self.bt_target_temp != _new_heating_setpoint + and _old_heating_setpoint != _new_heating_setpoint + and self.real_trvs[entity_id]["last_temperature"] != _new_heating_setpoint + and not child_lock + and self.real_trvs[entity_id]["target_temp_received"] is True + and self.real_trvs[entity_id]["system_mode_received"] is True + and self.real_trvs[entity_id]["hvac_mode"] is not HVACMode.OFF + and self.window_open is False + ): + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}" + ) + self.bt_target_temp = _new_heating_setpoint + _main_change = True + + if self.real_trvs[entity_id]["advanced"].get("no_off_system_mode", False): + if _new_heating_setpoint == self.real_trvs[entity_id]["min_temp"]: + self.bt_hvac_mode = HVACMode.OFF + else: + self.bt_hvac_mode = HVACMode.HEAT + _main_change = True + + if _main_change is True: + self.async_write_ha_state() + return await self.control_queue_task.put(self) + self.async_write_ha_state() + return + + +async def update_hvac_action(self): + """Update the hvac action.""" + if self.startup_running or self.control_queue_task is None: + return + + hvac_action = None + for trv in self.all_trvs: + if trv["advanced"][CONF_HOMATICIP]: + entity_id = trv["trv"] + state = self.hass.states.get(entity_id) + if state is None: + continue + + if state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT: + hvac_action = CURRENT_HVAC_HEAT + break + elif state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_IDLE: + hvac_action = CURRENT_HVAC_IDLE + + if hvac_action is None: + hvac_action = CURRENT_HVAC_IDLE + + if self.hvac_action != hvac_action: + self.attr_hvac_action = hvac_action + await self.async_update_ha_state(force_refresh=True) + + +def convert_inbound_states(self, entity_id, state: State) -> str: + """Convert hvac mode in a thermostat state from HA + Parameters + ---------- + self : + self instance of better_thermostat + state : State + Inbound thermostat state, which will be modified + Returns + ------- + Modified state + """ + + if state is None: + raise TypeError("convert_inbound_states() received None state, cannot convert") + + if state.attributes is None or state.state is None: + raise TypeError("convert_inbound_states() received None state, cannot convert") + + remapped_state = mode_remap(self, entity_id, str(state.state), True) + + if remapped_state not in (HVACMode.OFF, HVACMode.HEAT): + return None + return remapped_state + + +def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: + """Creates the new outbound thermostat state. + Parameters + ---------- + self : + self instance of better_thermostat + hvac_mode : + the HA mode to convert to + Returns + ------- + dict + A dictionary containing the new outbound thermostat state containing the following keys: + temperature: float + local_temperature: float + local_temperature_calibration: float + system_mode: string + None + In case of an error. + """ + + _new_local_calibration = None + _new_heating_setpoint = None + + try: + _calibration_type = self.real_trvs[entity_id].get("calibration", 1) + + if _calibration_type is None: + _LOGGER.warning( + "better_thermostat %s: no calibration type found in device config, talking to the TRV using fallback mode", + self.name, + ) + _new_heating_setpoint = self.bt_target_temp + _new_local_calibration = round_to_half_degree( + calculate_local_setpoint_delta(self, entity_id) + ) + if _new_local_calibration is None: + return None + + else: + if _calibration_type == 0: + _round_calibration = self.real_trvs[entity_id]["advanced"].get( + "calibration_round" + ) + + if _round_calibration is not None and ( + ( + isinstance(_round_calibration, str) + and _round_calibration.lower() == "true" + ) + or _round_calibration is True + ): + _new_local_calibration = round_to_half_degree( + calculate_local_setpoint_delta(self, entity_id) + ) + else: + _new_local_calibration = calculate_local_setpoint_delta( + self, entity_id + ) + + _new_heating_setpoint = self.bt_target_temp + + elif _calibration_type == 1: + _round_calibration = self.real_trvs[entity_id]["advanced"].get( + "calibration_round" + ) + + if _round_calibration is not None and ( + ( + isinstance(_round_calibration, str) + and _round_calibration.lower() == "true" + ) + or _round_calibration is True + ): + _new_heating_setpoint = round_to_half_degree( + calculate_setpoint_override(self, entity_id) + ) + else: + _new_heating_setpoint = calculate_setpoint_override(self, entity_id) + + _system_modes = self.real_trvs[entity_id]["hvac_modes"] + _has_system_mode = False + if _system_modes is not None: + _has_system_mode = True + + # Handling different devices with or without system mode reported or contained in the device config + + hvac_mode = mode_remap(self, entity_id, str(hvac_mode), False) + + if _has_system_mode is False: + _LOGGER.debug( + f"better_thermostat {self.name}: device config expects no system mode, while the device has one. Device system mode will be ignored" + ) + if hvac_mode == HVACMode.OFF: + _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] + hvac_mode = None + if ( + HVACMode.OFF not in _system_modes + or self.real_trvs[entity_id]["advanced"].get( + "no_off_system_mode", False + ) + is True + ): + if hvac_mode == HVACMode.OFF: + _LOGGER.debug( + f"better_thermostat {self.name}: sending 5°C to the TRV because this device has no system mode off and heater should be off" + ) + _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] + hvac_mode = None + + return { + "temperature": _new_heating_setpoint, + "local_temperature": self.real_trvs[entity_id]["current_temperature"], + "system_mode": hvac_mode, + "local_temperature_calibration": _new_local_calibration, + } + except Exception as e: + _LOGGER.error(e) + return None diff --git a/custom_components/better_thermostat/events/window.py b/custom_components/better_thermostat/events/window.py new file mode 100644 index 00000000..b88c7aef --- /dev/null +++ b/custom_components/better_thermostat/events/window.py @@ -0,0 +1,90 @@ +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.const import STATE_OFF + +_LOGGER = logging.getLogger(__name__) + + +@callback +async def trigger_window_change(self, event) -> None: + """Triggered by window sensor event from HA to check if the window is open. + + Parameters + ---------- + self : + self instance of better_thermostat + event : + Event object from the eventbus. Contains the new and old state from the window (group). + + Returns + ------- + None + """ + + new_state = event.data.get("new_state") + + if None in (self.hass.states.get(self.window_id), self.window_id, new_state): + return + + new_state = new_state.state + + old_window_open = self.window_open + + if new_state in ("on", "unknown", "unavailable"): + new_window_open = True + if new_state == "unknown": + _LOGGER.warning( + "better_thermostat %s: Window sensor state is unknown, assuming window is open", + self.name, + ) + + # window was opened, disable heating power calculation for this period + self.heating_start_temp = None + self.async_write_ha_state() + elif new_state == "off": + new_window_open = False + else: + _LOGGER.error( + f"better_thermostat {self.name}: New window sensor state '{new_state}' not recognized" + ) + return + + # make sure to skip events which do not change the saved window state: + if new_window_open == old_window_open: + _LOGGER.debug( + f"better_thermostat {self.name}: Window state did not change, skipping event" + ) + return + await self.window_queue_task.put(new_window_open) + + +async def window_queue(self): + while True: + window_event_to_process = await self.window_queue_task.get() + if window_event_to_process is not None: + if window_event_to_process: + # TODO: window open delay + await asyncio.sleep(self.window_delay) + else: + # TODO: window close delay + await asyncio.sleep(self.window_delay) + # remap off on to true false + current_window_state = True + if self.hass.states.get(self.window_id).state == STATE_OFF: + current_window_state = False + # make sure the current state is the suggested change state to prevent a false positive: + if current_window_state == window_event_to_process: + self.window_open = window_event_to_process + self.async_write_ha_state() + if not self.control_queue_task.empty(): + empty_queue(self.control_queue_task) + await self.control_queue_task.put(self) + self.window_queue_task.task_done() + + +def empty_queue(q: asyncio.Queue): + for _ in range(q.qsize()): + q.get_nowait() + q.task_done() diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json new file mode 100644 index 00000000..a7ca72bd --- /dev/null +++ b/custom_components/better_thermostat/manifest.json @@ -0,0 +1,20 @@ +{ + "domain": "better_thermostat", + "name": "Better Thermostat", + "after_dependencies": [ + "climate" + ], + "codeowners": [ + "@kartoffeltoby" + ], + "config_flow": true, + "dependencies": [ + "climate", + "recorder" + ], + "documentation": "https://github.com/KartoffelToby/better_thermostat", + "iot_class": "local_push", + "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", + "requirements": [], + "version": "1.2.2" +} \ No newline at end of file diff --git a/custom_components/better_thermostat/model_fixes/BHT-002-GCLZB.py b/custom_components/better_thermostat/model_fixes/BHT-002-GCLZB.py new file mode 100644 index 00000000..f6b52a3d --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/BHT-002-GCLZB.py @@ -0,0 +1,31 @@ +""" +This model fix is due to any floating point number being set to +/- 1 million by Zigbee2MQTT for the local_calibration +""" + +import math + + +def fix_local_calibration(self, entity_id, offset): + """ + If still heating, round UP the offset + + This creates a lower "fake" thermostat temperature, making it heat the room + """ + if self.cur_temp < self.bt_target_temp: + offset = math.ceil(offset) + else: + offset = math.floor(offset) + + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py b/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py new file mode 100644 index 00000000..6deb9782 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py @@ -0,0 +1,29 @@ +def fix_local_calibration(self, entity_id, offset): + # device SEA802 fix + if (self.cur_temp - self.bt_target_temp) < -0.2: + offset -= 2.5 + + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + # device SEA802 fix + _cur_trv_temp = float( + self.hass.states.get(entity_id).attributes["current_temperature"] + ) + if _cur_trv_temp is None: + return temperature + if ( + round(temperature, 1) > round(_cur_trv_temp, 1) + and temperature - _cur_trv_temp < 2.5 + ): + temperature += 2.5 + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/model_fixes/SPZB0001.py b/custom_components/better_thermostat/model_fixes/SPZB0001.py new file mode 100644 index 00000000..19d5d61f --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/SPZB0001.py @@ -0,0 +1,18 @@ +def fix_local_calibration(self, entity_id, offset): + if offset > 5: + offset = 5 + elif offset < -5: + offset = -5 + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/model_fixes/TS0601.py b/custom_components/better_thermostat/model_fixes/TS0601.py new file mode 100644 index 00000000..8dc284a8 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TS0601.py @@ -0,0 +1,30 @@ +def fix_local_calibration(self, entity_id, offset): + if (self.cur_temp - 0.5) <= self.bt_target_temp: + offset -= 2.5 + elif (self.cur_temp + 0.10) >= self.bt_target_temp: + offset = round(offset + 0.5, 1) + + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + _cur_trv_temp = float( + self.hass.states.get(entity_id).attributes["current_temperature"] + ) + if _cur_trv_temp is None: + return temperature + if ( + round(temperature, 1) > round(_cur_trv_temp, 1) + and temperature - _cur_trv_temp < 1.5 + ): + temperature += 1.5 + + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py new file mode 100644 index 00000000..8dc284a8 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py @@ -0,0 +1,30 @@ +def fix_local_calibration(self, entity_id, offset): + if (self.cur_temp - 0.5) <= self.bt_target_temp: + offset -= 2.5 + elif (self.cur_temp + 0.10) >= self.bt_target_temp: + offset = round(offset + 0.5, 1) + + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + _cur_trv_temp = float( + self.hass.states.get(entity_id).attributes["current_temperature"] + ) + if _cur_trv_temp is None: + return temperature + if ( + round(temperature, 1) > round(_cur_trv_temp, 1) + and temperature - _cur_trv_temp < 1.5 + ): + temperature += 1.5 + + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py new file mode 100644 index 00000000..6eba1577 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py @@ -0,0 +1,86 @@ +# Quirks for TV02-Zigbee +import logging +from homeassistant.components.climate.const import HVACMode + +_LOGGER = logging.getLogger(__name__) + + +def fix_local_calibration(self, entity_id, offset): + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + """Enable specific device quirks while setting hvac mode + Parameters + ---------- + self : + self instance of better_thermostat + entity_id : + Entity id of the TRV. + hvac_mode: + HVAC mode to be set. + Returns + ------- + None + """ + await self.hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": hvac_mode}, + blocking=True, + context=self.context, + ) + model = self.real_trvs[entity_id]["model"] + if model == "TV02-Zigbee" and hvac_mode != HVACMode.OFF: + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} device quirk hvac trv02-zigbee active" + ) + await self.hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "manual"}, + blocking=True, + context=self.context, + ) + return True + + +async def override_set_temperature(self, entity_id, temperature): + """Enable specific device quirks while setting temperature + Parameters + ---------- + self : + self instance of better_thermostat + entity_id : + Entity id of the TRV. + temperature: + Temperature to be set. + Returns + ------- + None + """ + model = self.real_trvs[entity_id]["model"] + if model == "TV02-Zigbee": + _LOGGER.debug( + f"better_thermostat {self.name}: TRV {entity_id} device quirk trv02-zigbee active" + ) + await self.hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "manual"}, + blocking=True, + context=self.context, + ) + + await self.hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": temperature}, + blocking=True, + context=self.context, + ) + return True diff --git a/custom_components/better_thermostat/model_fixes/default.py b/custom_components/better_thermostat/model_fixes/default.py new file mode 100644 index 00000000..f3f27188 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/default.py @@ -0,0 +1,14 @@ +def fix_local_calibration(self, entity_id, offset): + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return temperature + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return False + + +async def override_set_temperature(self, entity_id, temperature): + return False diff --git a/custom_components/better_thermostat/services.yaml b/custom_components/better_thermostat/services.yaml new file mode 100644 index 00000000..9fa995b2 --- /dev/null +++ b/custom_components/better_thermostat/services.yaml @@ -0,0 +1,48 @@ +save_current_target_temperature: + description: Save the current target temperature for later restore. + target: + entity: + domain: climate + integration: better_thermostat + device: + integration: better_thermostat + +restore_saved_target_temperature: + description: Restore the saved target temperature. + target: + entity: + domain: climate + integration: better_thermostat + device: + integration: better_thermostat + +reset_heating_power: + description: Reset heating power to default value. + target: + entity: + domain: climate + integration: better_thermostat + device: + integration: better_thermostat + +set_temp_target_temperature: + description: Set the target temperature to a temporay like night mode, and save the old one. + fields: + temperature: + name: Target temperature + description: The target temperature to set. + example: 18.5 + required: true + advanced: true + selector: + number: + min: 0 + max: 35 + step: 0.5 + mode: box + target: + entity: + domain: climate + integration: better_thermostat + device: + integration: better_thermostat diff --git a/custom_components/better_thermostat/strings.json b/custom_components/better_thermostat/strings.json new file mode 100644 index 00000000..65cf63ef --- /dev/null +++ b/custom_components/better_thermostat/strings.json @@ -0,0 +1,106 @@ +{ + "config":{ + "step":{ + "user":{ + "description":"Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ", + "data":{ + "name":"Name", + "thermostat":"The real thermostat", + "temperature_sensor":"Temperature sensor", + "humidity_sensor":"Humidity sensor", + "window_sensors":"Window sensor", + "off_temperature":"The outdoor temperature when the thermostat turn off", + "window_off_delay":"Delay before the thermostat turn off when the window is open and on when the window is closed", + "outdoor_sensor":"If you have an outdoor sensor, you can use it to get the outdoor temperature", + "weather":"Your weather entity to get the outdoor temperature" + } + }, + "advanced":{ + "description":"Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data":{ + "protect_overheating":"Overheating protection?", + "heat_auto_swapped":"If the auto means heat for your TRV and you want to swap it", + "child_lock":"Ignore all inputs on the TRV like a child lock", + "homaticip":"If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance":"If your thermostat has no own maintenance mode, you can use this one", + "calibration":"Calibration Type", + "calibration_mode":"Calibration mode", + "no_off_system_mode":"If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + }, + "data_description":{ + "protect_overheating":"Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.", + "calibration_mode":"The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration":"How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." + } + }, + "confirm":{ + "title":"Confirm adding a Better Thermostat", + "description":"You are about to add `{name}` to Home Assistant.\nWith {trv} as the real Thermostat" + } + }, + "error":{ + "failed":"something went wrong.", + "no_name":"Please enter a name.", + "no_off_mode":"You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.", + "no_outside_temp":"You have no outside temperature sensor. Better Thermostat will use the weather entity instead." + }, + "abort":{ + "single_instance_allowed":"Only a single Thermostat for each real is allowed.", + "no_devices_found":"No thermostat entity found, make sure you have a climate entity in your home assistant" + } + }, + "options":{ + "step":{ + "user":{ + "description":"Update your Better Thermostat settings", + "data":{ + "name":"Name", + "thermostat":"The real thermostat", + "temperature_sensor":"Temperature Sensor", + "humidity_sensor":"Humidity sensor", + "window_sensors":"Window Sensor", + "off_temperature":"The outdoor temperature when the thermostat turn off", + "window_off_delay":"Delay before the thermostat turn off when the window is open and on when the window is closed", + "outdoor_sensor":"If you have an outdoor sensor, you can use it to get the outdoor temperature", + "valve_maintenance":"If your thermostat has no own maintenance mode, you can use this one", + "calibration":"The sort of calibration https://better-thermostat.org/configuration#second-step", + "weather":"Your weather entity to get the outdoor temperature", + "heat_auto_swapped":"If the auto means heat for your TRV and you want to swap it", + "child_lock":"Ignore all inputs on the TRV like a child lock", + "homaticip":"If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle" + } + }, + "advanced":{ + "description":"Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data":{ + "protect_overheating":"Overheating protection?", + "heat_auto_swapped":"If the auto means heat for your TRV and you want to swap it", + "child_lock":"Ignore all inputs on the TRV like a child lock", + "homaticip":"If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance":"If your thermostat has no own maintenance mode, you can use this one", + "calibration":"The sort of calibration you want to use", + "calibration_mode":"Calibration mode", + "no_off_system_mode":"If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + }, + "data_description":{ + "protect_overheating":"Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.", + "calibration_mode":"The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration":"How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." + } + } + } + }, + "issues":{ + "missing_entity":{ + "title":"BT: {name} - related entity is missing", + "fix_flow":{ + "step":{ + "confirm":{ + "title":"The related entity {entity} is missing", + "description":"The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/translations/da.json b/custom_components/better_thermostat/translations/da.json new file mode 100644 index 00000000..46f0fc0d --- /dev/null +++ b/custom_components/better_thermostat/translations/da.json @@ -0,0 +1,97 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Konfigurer din Better Thermostat til at integrere med Home Assistant\n**Hvis du har brug for mere info: https://better-thermostat.org/configuration#first-step**", + "data": { + "name": "Navn", + "thermostat": "Den rigtige termostat", + "temperature_sensor": "Temperatur måler", + "humidity_sensor": "Fugtighedssensor", + "window_sensors": "Vinduessensor", + "off_temperature": "Udetemperaturen, når termostaten slukker", + "window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket", + "outdoor_sensor": "Hvis du har en udeføler, kan du bruge den til at få udetemperaturen", + "weather": "Din vejrentitet for at få udendørstemperaturen" + } + }, + "advanced": { + "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", + "child_lock": "Ignorer alle input på TRV som en børnesikring", + "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", + "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", + "calibration": "Den slags kalibrering du vil bruge", + "no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet", + "calibration_mode": "Kalibreringstilstand", + "protect_overheating": "Overophedningsbeskyttelse?" + }, + "data_description": { + "protect_overheating": "Nogle TRV'er lukker ikke ventilen helt, når temperaturen er nået. Eller radiatoren har meget hvilevarme. Dette kan forårsage overophedning. Denne mulighed kan forhindre dette.", + "calibration_mode": "Den slags, hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fastgjort af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.", + "calibration": "Hvordan kalibreringen skal anvendes på TRV (Target temp or offset)\n***Måltemperaturbaseret***: Anvend kalibreringen til måltemperaturen.\n***Offset-baseret***: Anvend kalibreringen til offset." + } + }, + "confirm": { + "title": "Bekræft tilføjelse af en Better Thermostat", + "description": "Du er ved at tilføje {name} til Home Assistant.\nMed {trv} som en rigtige termostat" + } + }, + "error": { + "no_outside_temp": "Du har ingen udetemperaturføler. Bedre termostat vil bruge vejret i stedet.", + "failed": "noget gik galt.", + "no_name": "Indtast venligst et navn.", + "no_off_mode": "Din enhed er meget speciel og har ingen slukket tilstand :(\nDet virker alligevel, men du skal oprette en automatisering, der passer til dine specialer baseret på enhedsbegivenhederne" + }, + "abort": { + "single_instance_allowed": "Kun en enkelt termostat for hver virkelige er tilladt.", + "no_devices_found": "Der blev ikke fundet nogen termostatenhed, sørg for at have en klimaenhed i home assistant" + } + }, + "options": { + "step": { + "user": { + "description": "Opdater dine Better Thermostat indstillinger", + "data": { + "temperature_sensor": "Temperatur måler", + "humidity_sensor": "Fugtighedssensor", + "window_sensors": "Vinduessensor", + "off_temperature": "Udendørstemperaturen, når termostaten skal slukker", + "window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket", + "outdoor_sensor": "Har du en udendørsføler, kan du bruge den til at få udetemperaturen", + "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", + "calibration": "Den slags kalibrering https://better-thermostat.org/configuration#second-step", + "weather": "Din vejrentitet for at få udendørstemperaturen", + "heat_auto_swapped": "Hvis auto betyder varme til din TRV, og du ønsker at bytte den", + "child_lock": "Ignorer alle input på TRV som en børnesikring", + "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus" + } + }, + "advanced": { + "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", + "child_lock": "Ignorer alle input på TRV som en børnesikring", + "homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus", + "valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne", + "calibration": "Den slags kalibrering du vil bruge" + } + } + } + }, + "issues": { + "missing_entity": { + "title": "BT: {name} - related entity is missing", + "fix_flow": { + "step": { + "confirm": { + "title": "The related entity {entity} is missing", + "description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/translations/de.json b/custom_components/better_thermostat/translations/de.json new file mode 100644 index 00000000..f5af602b --- /dev/null +++ b/custom_components/better_thermostat/translations/de.json @@ -0,0 +1,107 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr Informationen: https://better-thermostat.org/configuration#first-step** ", + "data": { + "name": "Name", + "thermostat": "Das reale Thermostat", + "temperature_sensor": "Externer Temperatursensor", + "humidity_sensor": "Luftfeuchtigkeitssensor", + "window_sensors": "Fenstersensor(en)", + "off_temperature": "Außentemperatur, bei welcher das Thermostat abgeschaltet wird.", + "window_off_delay": "Wartezeit, bevor das Thermostat bei geöffnetem Fenster abgeschaltet bzw. bei geschlossendem Fenter angeschaltet wird.", + "outdoor_sensor": "Wenn ein Außentemperaturssensor vorhanden ist, kann dieser anstelle der Wetter-Entität genutzt werden.", + "weather": "Die Wetter-Entität für die Außentemperatur." + } + }, + "advanced": { + "description": "Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/configuration#second-step*** ", + "data": { + "protect_overheating": "Überhitzung verhindern?", + "heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.", + "child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).", + "homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-übertragung zu reduzieren.", + "valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?", + "calibration": "Kalibrierungstyp", + "calibration_mode": "Kalibrierungsmodus", + "no_off_system_mode": "Wenn das TRV keinen Aus Modus nutzen kann, kann diese Option aktiviert werden, um das TRV stattdessen auf 5°C zu setzen." + }, + "data_description": { + "protect_overheating": "Manche TRVs schließen auch nach Erreichen der Temperatur das Ventil nicht vollständig, dies kann zu Überhitzungen führen. Ebenso falls der Radiator viel Restwärme abstrahlt. Diese Option kann dies verhindern.", + "calibration_mode": "Wie die Kalibrierung berechnet wird\n***Normal***: In diesem Modus wird die interne TRV-Temperatur an die des externen Sensors angeglichen.\n\n***Aggresive***: In diesem Modus wird die interne TRV-Temperatur an die des externen Sensors angeglichen allerdings mit größeren Werten, dies ist hilfreich wenn ein Raum schnell aufgeheizt werden soll, oder das TRV träge ist.\n\n***AI Time Based***: In diesem Modus wird ein eigener Algorithmus genutzt anhand des externen Temperatursensors, um die Kalibrierung zu berechnen. Dieser Modus versucht den TRV internen Algorithmus zu optimieren.", + "calibration": "Wie die Kalibrierung auf das TRV angewendet werden soll.\n\n***Target Temperature Based***: Kalibiert das TRV über die Zieltemperatur.\n\n***Offset Based***: Kalibiert das TRV über eine Offset Funktion im TRV selbst. (Empfohlen)" + } + }, + "confirm": { + "title": "Bestätige das Hinzufügen eines Better Thermostat", + "description": "Du bist dabei ein Gerät mit dem Namen `{name}` zu Home Assistant hinzuzufügen.\nMit {trv} als reales Thermostat\nund dem Kalibrierungsmodus:" + } + }, + "error": { + "no_outside_temp": "Es kann keine Außentemperatur geladen werden", + "failed": "Ups, hier stimmt was nicht.", + "no_name": "Du musst einen Namen vergeben.", + "no_off_mode": "Dein Gerät ist ein Sonderfall, es hat keinen OFF Modus :(\nBetter Thermostat wird stattdessen das TRV auf den Minimalwert setzen." + }, + "abort": { + "single_instance_allowed": "Es ist nur ein einzelnes BT je realem Thermostat erlaubt.", + "no_devices_found": "Es konnten keine Climate-Entitäten in Home Assistant gefunden werden. Stelle sicher, dass dein reales Thermostat in Home Assistant vorhanden ist." + } + }, + "options": { + "step": { + "user": { + "description": "Aktualisiere die Better Thermostat Einstellungen", + "data": { + "name": "Name", + "thermostat": "Das reale Thermostat", + "temperature_sensor": "Externer Temperatursensor", + "humidity_sensor": "Luftfeuchtigkeitssensor", + "window_sensors": "Fenstersensor(en)", + "off_temperature": "Außentemperatur, bei welcher das Thermostat abgeschaltet wird.", + "window_off_delay": "Wartezeit, bevor das Thermostat bei geöffnetem Fenster abgeschaltet bzw. bei geschlossenem Fenter angeschaltet wird.", + "outdoor_sensor": "Wenn ein Außentemperatursensor vorhanden ist, kann dieser anstelle der Wetter-Entität genutzt werden.", + "weather": "Die Wetter-Entität für die Außentemperatur.", + "valve_maintenance": "Wenn Ihr Thermostat keinen eigenen Wartungsmodus hat, können Sie diesen verwenden", + "child_lock": "Ignorieren Sie alle Eingaben am TRV wie eine Kindersicherung", + "homaticip": "Wenn Sie HomaticIP verwenden, sollten Sie dies aktivieren, um die Anfragen zu verlangsamen und den Duty Cycle zu verhindern", + "heat_auto_swapped": "Wenn das Auto Wärme für Ihr TRV bedeutet und Sie es austauschen möchten", + "calibration": "Die Art der Kalibrierung https://better-thermostat.org/configuration#second-step" + } + }, + "advanced": { + "description": "Aktuallisere die Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/configuration#second-step*** ", + "data": { + "protect_overheating": "Überhitzung verhindern?", + "heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.", + "child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).", + "homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-übertragung zu reduzieren.", + "valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?", + "calibration": "Kalibrierungstyp", + "calibration_mode": "Kalibrierungsmodus", + "no_off_system_mode": "Wenn das TRV keinen Aus Modus nutzen kann, kann diese Option aktiviert werden, um das TRV stattdessen auf 5°C zu setzen." + }, + "data_description": { + "protect_overheating": "Manche TRVs schließen auch nach Erreichen der Temperatur das Ventil nicht vollständig, dies kann zu Überhitzungen führen. Ebenso falls der Radiator viel Restwärme abstrahlt. Diese Option kann dies verhindern.", + "calibration_mode": "Wie die Kalibrierung berechnet wird\n***Normal***: In diesem Modus wird die interne TRV-Temperatur an die des externen Sensors angeglichen.\n\n***Aggresive***: In diesem Modus wird die interne TRV-Temperatur an die des externen Sensors angeglichen allerdings mit größeren Werten, dies ist hilfreich wenn ein Raum schnell aufgeheizt werden soll, oder das TRV träge ist.\n\n***AI Time Based***: In diesem Modus wird ein eigener Algorithmus genutzt anhand des externen Temperatursensors, um die Kalibrierung zu berechnen. Dieser Modus versucht den TRV internen Algorithmus zu optimieren.", + "calibration": "Wie die Kalibrierung auf das TRV angewendet werden soll.\n\n***Target Temperature Based***: Kalibiert das TRV über die Zieltemperatur.\n\n***Offset Based***: Kalibiert das TRV über eine Offset Funktion im TRV selbst. (Empfohlen)" + } + } + } + }, + "issues": { + "missing_entity": { + "title": "BT: {name} - related entity is missing", + "fix_flow": { + "step": { + "confirm": { + "title": "The related entity {entity} is missing", + "description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/translations/en.json b/custom_components/better_thermostat/translations/en.json new file mode 100644 index 00000000..73b055da --- /dev/null +++ b/custom_components/better_thermostat/translations/en.json @@ -0,0 +1,107 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ", + "data": { + "name": "Name", + "thermostat": "The real thermostat", + "temperature_sensor": "Temperature sensor", + "humidity_sensor": "Humidity sensor", + "window_sensors": "Window sensor", + "off_temperature": "The outdoor temperature when the thermostat turn off", + "window_off_delay": "Delay before the thermostat turn off when the window is open and on when the window is closed", + "outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature", + "weather": "Your weather entity to get the outdoor temperature" + } + }, + "advanced": { + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data": { + "protect_overheating": "Overheating protection?", + "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", + "child_lock": "Ignore all inputs on the TRV like a child lock", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", + "calibration": "Calibration Type", + "calibration_mode": "Calibration mode", + "no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + }, + "data_description": { + "protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.", + "calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." + } + }, + "confirm": { + "title": "Confirm adding a Better Thermostat", + "description": "You are about to add `{name}` to Home Assistant.\nWith {trv} as the real Thermostat" + } + }, + "error": { + "failed": "something went wrong.", + "no_name": "Please enter a name.", + "no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.", + "no_outside_temp": "You have no outside temperature sensor. Better Thermostat will use the weather entity instead." + }, + "abort": { + "single_instance_allowed": "Only a single Thermostat for each real is allowed.", + "no_devices_found": "No thermostat entity found, make sure you have a climate entity in your home assistant" + } + }, + "options": { + "step": { + "user": { + "description": "Update your Better Thermostat settings", + "data": { + "name": "Name", + "thermostat": "The real thermostat", + "temperature_sensor": "Temperature Sensor", + "humidity_sensor": "Humidity sensor", + "window_sensors": "Window Sensor", + "off_temperature": "The outdoor temperature when the thermostat turn off", + "window_off_delay": "Delay before the thermostat turn off when the window is open and on when the window is closed", + "outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature", + "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", + "calibration": "The sort of calibration https://better-thermostat.org/configuration#second-step", + "weather": "Your weather entity to get the outdoor temperature", + "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", + "child_lock": "Ignore all inputs on the TRV like a child lock", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle" + } + }, + "advanced": { + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data": { + "protect_overheating": "Overheating protection?", + "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", + "child_lock": "Ignore all inputs on the TRV like a child lock", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one", + "calibration": "The sort of calibration you want to use", + "calibration_mode": "Calibration mode", + "no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead" + }, + "data_description": { + "protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.", + "calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.", + "calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset." + } + } + } + }, + "issues": { + "missing_entity": { + "title": "BT: {name} - related entity is missing", + "fix_flow": { + "step": { + "confirm": { + "title": "The related entity {entity} is missing", + "description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/translations/fr.json b/custom_components/better_thermostat/translations/fr.json new file mode 100644 index 00000000..64d27a24 --- /dev/null +++ b/custom_components/better_thermostat/translations/fr.json @@ -0,0 +1,97 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Configuration de Better Thermostat pour l'intégrer à Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ", + "data": { + "name": "Nom", + "thermostat": "Le véritable thermostat", + "temperature_sensor": "Capteur de température", + "humidity_sensor": "Capteur d'humidité", + "window_sensors": "Capteur de fenêtre", + "off_temperature": "La température extérieure lorsque le thermostat s'éteint", + "window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte et ne s'allume lorsque la fenêtre est fermée", + "outdoor_sensor": "Si vous avez un capteur extérieur, vous pouvez l'utiliser pour obtenir la température extérieure", + "weather": "Votre entité météo pour obtenir la température extérieure" + } + }, + "advanced": { + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser (pour les utilisateurs de Google Home)", + "child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci", + "calibration": "The sort of calibration", + "no_off_system_mode": "Si votre TRV ne peut pas gérer le mode arrêt, vous pouvez l'activer pour utiliser la température cible de 5 °C à la place", + "calibration_mode": "Mode d'étalonnage", + "protect_overheating": "Protection contre la surchauffe ?" + }, + "data_description": { + "protect_overheating": "Certaines VTR ferment complètement la vanne lorsque la température est atteinte. Ou le radiateur a beaucoup de chaleur résiduelle. Cela peut provoquer une surchauffe. Cette option peut empêcher cela.", + "calibration_mode": "Le type de calcul de l'étalonnage\n***Normal*** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe.\n***Agressif*** : dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe mais réglé beaucoup plus bas/plus haut pour obtenir une accélération plus rapide.\n*** AI Time Based *** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe, mais la valeur est calculée par un algorithme personnalisé pour améliorer l'algorithme interne TRV.", + "calibration": "Comment l'étalonnage doit être appliqué sur la VTR (température cible ou décalage)\n***Basé sur la température cible*** : Appliquez l'étalonnage à la température cible.\n***Basé sur le décalage*** : Appliquez l'étalonnage au décalage." + } + }, + "confirm": { + "title": "Confirm adding a Better Thermostat", + "description": "You are about to add `{name}` to Home Assistant.\nWith {trv} as the real Thermostat" + } + }, + "error": { + "no_name": "Veuillez entrer un nom.", + "failed": "quelque chose s'est mal passé.", + "no_outside_temp": "Vous devez définir un capteur de température extérieure ou une entité météorologique.", + "no_off_mode": "You device is very special and has no off mode :(\nIt work anyway, but you have to create a automation to fit your specials based on the device events" + }, + "abort": { + "single_instance_allowed": "Un seul et unique thermostat est autorisé pour chaque thermostat réel", + "no_devices_found": "Aucune entité thermostat n'a été trouvée, assurez-vous d'avoir une entité climat dans votre home assistant" + } + }, + "options": { + "step": { + "user": { + "description": "Mettez à jour vos paramètres de Better Thermostat", + "data": { + "temperature_sensor": "Capteur de température", + "humidity_sensor": "Capteur d'humidité", + "window_sensors": "Capteur de fenêtre", + "off_temperature": "La température extérieure lorsque le thermostat s'éteint", + "window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte et ne s'allume lorsque la fenêtre est fermée", + "outdoor_sensor": "Si vous avez un capteur extérieur, vous pouvez l'utiliser pour obtenir la température extérieure", + "valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci", + "calibration": "The sort of calibration https://better-thermostat.org/configuration#second-step", + "weather": "Votre entité météo pour obtenir la température extérieure", + "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser", + "child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle" + } + }, + "advanced": { + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser", + "child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant", + "homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle", + "valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci", + "calibration": "The sort of calibration" + } + } + } + }, + "issues": { + "missing_entity": { + "title": "BT: {name} - related entity is missing", + "fix_flow": { + "step": { + "confirm": { + "title": "The related entity {entity} is missing", + "description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/translations/pl.json b/custom_components/better_thermostat/translations/pl.json new file mode 100644 index 00000000..3816c390 --- /dev/null +++ b/custom_components/better_thermostat/translations/pl.json @@ -0,0 +1,97 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Skonfiguruj swój Better Thermostat do integracji z Home Assistant\n**Więcej informacji znajdziesz na: https://better-thermostat.org/configuration#first-step** ", + "data": { + "name": "Nazwa", + "thermostat": "Twój termostat", + "temperature_sensor": "Sensor temperatury", + "humidity_sensor": "Sensor wilgotności", + "window_sensors": "Sensor okna", + "off_temperature": "Temperatura zewnętrzna, przy której termostat ma się wyłączyć", + "window_off_delay": "Opóźnienie wyłączenia termostatu, kiedy otworzysz okno lub je zamkniesz (w sekundach)", + "outdoor_sensor": "Jeśli masz czujnik zewnętrzny, możesz go użyć, aby uzyskać temperaturę zewnętrzną", + "weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną" + } + }, + "advanced": { + "description": "Zaawansowana konfiguracja\n\n**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", + "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", + "homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania cyklu pracy", + "valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.", + "calibration": "Rodzaj kalibracji, której chcesz użyć", + "no_off_system_mode": "Jeśli Twój TRV nie obsługuje trybu wyłączenia, możesz go włączyć, aby zamiast tego używać temperatury docelowej 5°C", + "calibration_mode": "Tryb kalibracji", + "protect_overheating": "Zabezpieczenie przed przegrzaniem?" + }, + "data_description": { + "protect_overheating": "Niektóre TRV całkowicie zamykają zawór po osiągnięciu temperatury. Lub grzejnik ma dużo ciepła resztkowego. Może to spowodować przegrzanie. Ta opcja może temu zapobiec.", + "calibration_mode": "Rodzaj sposobu obliczania kalibracji\n***Normalny***: W tym trybie wewnętrzny czujnik temperatury TRV jest zależny od zewnętrznego czujnika temperatury.\n***Agresywny***: W tym trybie wewnętrzny czujnik temperatury TRV jest ustalany przez zewnętrzny czujnik temperatury, ale jest ustawiony znacznie niżej/wyżej, aby uzyskać szybsze przyspieszenie.\n***AI Time Based***: W tym trybie wewnętrzny czujnik temperatury TRV jest ustalany przez zewnętrzny czujnik temperatury, ale wartość jest obliczana przez niestandardowy algorytm w celu ulepszenia wewnętrznego algorytmu TRV.", + "calibration": "Jak kalibracja powinna być zastosowana w TRV (temperatura docelowa lub przesunięcie)\n***Target Temperature Based***: Zastosuj kalibrację do temperatury docelowej.\n***Oparte na przesunięciu***: Zastosuj kalibrację do przesunięcia." + } + }, + "confirm": { + "title": "Potwierdź dodanie Better Thermostat", + "description": "Zamierzasz dodać `{name}` do Home Assistant.\nUżywając {trv} jako termostatu" + } + }, + "error": { + "no_outside_temp": "Nie masz czujnika temperatury zewnętrznej. Lepszy termostat zamiast tego użyje jednostki pogodowej.", + "failed": "coś poszło nie tak.", + "no_name": "Proszę podać nazwę.", + "no_off_mode": "Twoje urządzenie jest inne i nie ma trybu wyłączenia :(\nTo będzie działać, ale musisz stworzyć automatyzację, aby ustawić funkcje pod swoje urządzenie" + }, + "abort": { + "single_instance_allowed": "Dozwolony jest tylko jeden termostat dla jednego urządzenia.", + "no_devices_found": "Nie znaleziono jednostki termostatu, upewnij się, że masz jednostkę klimatyczną w swoim asystencie domowym." + } + }, + "options": { + "step": { + "user": { + "description": "Zaktualizuj ustawienia Better Thermostat", + "data": { + "temperature_sensor": "Sensor temperatury", + "humidity_sensor": "Sensor wilgotności", + "window_sensors": "Sensor okna", + "off_temperature": "Temperatura zewnętrzna, przy której termostat ma się wyłączyć", + "window_off_delay": "Opóźnienie wyłączenia termostatu, kiedy otworzysz okno lub je zamkniesz (w sekundach)", + "outdoor_sensor": "Jeśli masz czujnik zewnętrzny, możesz go użyć, aby uzyskać temperaturę zewnętrzną", + "valve_maintenance": "Jeżeli Twój termostat nie ma własnego trybu konserwacji, możesz użyć tej opcji.", + "calibration": "Rodzaje kalibracji https://better-thermostat.org/configuration#second-step", + "weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną", + "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", + "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", + "homaticip": "Jeżeli używasz HomaticIP, powinienieś włączyć tę opcję żeby spowolnić żądania" + } + }, + "advanced": { + "description": "Zaawansowana konfiguracja**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ", + "data": { + "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", + "child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca", + "homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania spowolnienia cyklu pracy", + "valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.", + "calibration": "Rodzaj kalibracji, której chcesz użyć" + } + } + } + }, + "issues": { + "missing_entity": { + "title": "BT: {name} - related entity is missing", + "fix_flow": { + "step": { + "confirm": { + "title": "The related entity {entity} is missing", + "description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue." + } + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/better_thermostat/utils/bridge.py b/custom_components/better_thermostat/utils/bridge.py new file mode 100644 index 00000000..6b48a927 --- /dev/null +++ b/custom_components/better_thermostat/utils/bridge.py @@ -0,0 +1,98 @@ +from importlib import import_module +import logging + +_LOGGER = logging.getLogger(__name__) + + +def load_adapter(self, integration, entity_id, get_name=False): + """Load adapter.""" + if get_name: + self.name = "-" + + if integration == "generic_thermostat": + integration = "generic" + + try: + self.adapter = import_module( + "custom_components.better_thermostat.adapters." + integration, + package="better_thermostat", + ) + _LOGGER.debug( + "better_thermostat %s: uses adapter %s for trv %s", + self.name, + integration, + entity_id, + ) + except Exception: + self.adapter = import_module( + "custom_components.better_thermostat.adapters.generic", + package="better_thermostat", + ) + _LOGGER.info( + "better_thermostat %s: intigration: %s isn't native supported, feel free to open an issue, fallback adapter %s", + self.name, + integration, + "generic", + ) + pass + + if get_name: + return integration + return self.adapter + + +async def init(self, entity_id): + """Init adapter.""" + return await self.real_trvs[entity_id]["adapter"].init(self, entity_id) + + +async def get_info(self, entity_id): + return await self.real_trvs[entity_id]["adapter"].get_info(self, entity_id) + + +async def get_current_offset(self, entity_id): + """Get current offset.""" + return await self.real_trvs[entity_id]["adapter"].get_current_offset( + self, entity_id + ) + + +async def get_offset_steps(self, entity_id): + """get offset setps.""" + return await self.real_trvs[entity_id]["adapter"].get_offset_steps(self, entity_id) + + +async def get_min_offset(self, entity_id): + """Get min offset.""" + return await self.real_trvs[entity_id]["adapter"].get_min_offset(self, entity_id) + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + return await self.real_trvs[entity_id]["adapter"].get_max_offset(self, entity_id) + + +async def set_temperature(self, entity_id, temperature): + """Set new target temperature.""" + return await self.real_trvs[entity_id]["adapter"].set_temperature( + self, entity_id, temperature + ) + + +async def set_hvac_mode(self, entity_id, hvac_mode): + """Set new target hvac mode.""" + return await self.real_trvs[entity_id]["adapter"].set_hvac_mode( + self, entity_id, hvac_mode + ) + + +async def set_offset(self, entity_id, offset): + """Set new target offset.""" + return await self.real_trvs[entity_id]["adapter"].set_offset( + self, entity_id, offset + ) + + +async def set_valve(self, entity_id, valve): + """Set new target valve.""" + return await self.real_trvs[entity_id]["adapter"].set_valve(self, entity_id, valve) diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py new file mode 100644 index 00000000..1d606537 --- /dev/null +++ b/custom_components/better_thermostat/utils/controlling.py @@ -0,0 +1,295 @@ +import asyncio +import logging + +from custom_components.better_thermostat.utils.model_quirks import ( + override_set_hvac_mode, + override_set_temperature, +) + +from .bridge import ( + set_offset, + get_current_offset, + get_offset_steps, + set_temperature, + set_hvac_mode, +) +from ..events.trv import convert_outbound_states +from homeassistant.components.climate.const import HVACMode + +from .helpers import convert_to_float, calibration_round + +_LOGGER = logging.getLogger(__name__) + + +async def control_queue(self): + """The accutal control loop. + Parameters + ---------- + self : + instance of better_thermostat + + Returns + ------- + None + """ + while True: + if self.ignore_states or self.startup_running: + await asyncio.sleep(1) + continue + else: + controls_to_process = await self.control_queue_task.get() + if controls_to_process is not None: + self.ignore_states = True + result = True + for trv in self.real_trvs.keys(): + try: + _temp = await control_trv(self, trv) + if _temp is False: + result = False + except Exception: + _LOGGER.exception( + "better_thermostat %s: ERROR controlling: %s", + self.name, + trv, + ) + result = False + + # Retry task if some TRVs failed. Discard the task if the queue is full + # to avoid blocking and therefore deadlocking this function. + if result is False: + try: + self.control_queue_task.put_nowait(self) + except asyncio.QueueFull: + _LOGGER.debug( + "better_thermostat %s: control queue is full, discarding task" + ) + + self.control_queue_task.task_done() + self.ignore_states = False + + +async def control_trv(self, heater_entity_id=None): + """This is the main controller for the real TRV + + Parameters + ---------- + self : + instance of better_thermostat + + Returns + ------- + None + """ + async with self._temp_lock: + self.real_trvs[heater_entity_id]["ignore_trv_states"] = True + await self.calculate_heating_power() + _trv = self.hass.states.get(heater_entity_id) + _current_set_temperature = convert_to_float( + str(_trv.attributes.get("temperature", None)), self.name, "controlling()" + ) + + _remapped_states = convert_outbound_states( + self, heater_entity_id, self.bt_hvac_mode + ) + if not isinstance(_remapped_states, dict): + _LOGGER.debug( + f"better_thermostat {self.name}: ERROR {heater_entity_id} {_remapped_states}" + ) + await asyncio.sleep(10) + self.ignore_states = False + self.real_trvs[heater_entity_id]["ignore_trv_states"] = False + return False + + _temperature = _remapped_states.get("temperature", None) + _calibration = _remapped_states.get("local_temperature_calibration", None) + + _new_hvac_mode = handle_window_open(self, _remapped_states) + + # if we don't need ot heat, we force HVACMode to be off + if self.call_for_heat is False: + _new_hvac_mode = HVACMode.OFF + + # Manage TRVs with no HVACMode.OFF + _no_off_system_mode = ( + HVACMode.OFF not in self.real_trvs[heater_entity_id]["hvac_modes"] + ) or ( + self.real_trvs[heater_entity_id]["advanced"].get( + "no_off_system_mode", False + ) + is True + ) + if _no_off_system_mode is True and _new_hvac_mode == HVACMode.OFF: + _min_temp = self.real_trvs[heater_entity_id]["min_temp"] + _LOGGER.debug( + f"better_thermostat {self.name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off" + ) + _temperature = _min_temp + + # send new HVAC mode to TRV, if it changed + if ( + _new_hvac_mode is not None + and _new_hvac_mode != _trv.state + and ( + (_no_off_system_mode is True and _new_hvac_mode != HVACMode.OFF) + or (_no_off_system_mode is False) + ) + ): + _LOGGER.debug( + f"better_thermostat {self.name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}" + ) + self.real_trvs[heater_entity_id]["last_hvac_mode"] = _new_hvac_mode + _tvr_has_quirk = await override_set_hvac_mode( + self, heater_entity_id, _new_hvac_mode + ) + if _tvr_has_quirk is False: + await set_hvac_mode(self, heater_entity_id, _new_hvac_mode) + if self.real_trvs[heater_entity_id]["system_mode_received"] is True: + self.real_trvs[heater_entity_id]["system_mode_received"] = False + asyncio.create_task(check_system_mode(self, heater_entity_id)) + + # set new calibration offset + if _calibration is not None and _new_hvac_mode != HVACMode.OFF: + old_calibration = await get_current_offset(self, heater_entity_id) + step_calibration = await get_offset_steps(self, heater_entity_id) + if old_calibration is None or step_calibration is None: + _LOGGER.error( + "better_thermostat %s: calibration fatal error %s", + self.name, + heater_entity_id, + ) + # this should not be before, set_hvac_mode (because if it fails, the new hvac mode will never be sent) + self.ignore_states = False + self.real_trvs[heater_entity_id]["ignore_trv_states"] = False + return True + current_calibration = convert_to_float( + str(old_calibration), self.name, "controlling()" + ) + if step_calibration.is_integer(): + _calibration = calibration_round( + float(str(format(float(_calibration), ".1f"))) + ) + else: + _calibration = float(str(format(float(_calibration), ".1f"))) + + old = self.real_trvs[heater_entity_id].get( + "last_calibration", current_calibration + ) + + if self.real_trvs[heater_entity_id][ + "calibration_received" + ] is True and float(old) != float(_calibration): + _LOGGER.debug( + f"better_thermostat {self.name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {old} to: {_calibration}" + ) + await set_offset(self, heater_entity_id, _calibration) + self.real_trvs[heater_entity_id]["calibration_received"] = False + + # set new target temperature + if ( + _temperature is not None + and _new_hvac_mode != HVACMode.OFF + or _no_off_system_mode + ): + if _temperature != _current_set_temperature: + old = self.real_trvs[heater_entity_id].get("last_temperature", "?") + _LOGGER.debug( + f"better_thermostat {self.name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}" + ) + self.real_trvs[heater_entity_id]["last_temperature"] = _temperature + _tvr_has_quirk = await override_set_temperature( + self, heater_entity_id, _temperature + ) + if _tvr_has_quirk is False: + await set_temperature(self, heater_entity_id, _temperature) + if self.real_trvs[heater_entity_id]["target_temp_received"] is True: + self.real_trvs[heater_entity_id]["target_temp_received"] = False + asyncio.create_task( + check_target_temperature(self, heater_entity_id) + ) + + await asyncio.sleep(3) + self.real_trvs[heater_entity_id]["ignore_trv_states"] = False + return True + + +def handle_window_open(self, _remapped_states): + """handle window open""" + _converted_hvac_mode = _remapped_states.get("system_mode", None) + _hvac_mode_send = _converted_hvac_mode + + if self.window_open is True and self.last_window_state is False: + # if the window is open or the sensor is not available, we're done + self.last_main_hvac_mode = _hvac_mode_send + _hvac_mode_send = HVACMode.OFF + self.last_window_state = True + _LOGGER.debug( + f"better_thermostat {self.name}: control_trv: window is open or status of window is unknown, setting window open" + ) + elif self.window_open is False and self.last_window_state is True: + _hvac_mode_send = self.last_main_hvac_mode + self.last_window_state = False + _LOGGER.debug( + f"better_thermostat {self.name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}" + ) + + # Force off on window open + if self.window_open is True: + _hvac_mode_send = HVACMode.OFF + + return _hvac_mode_send + + +async def check_system_mode(self, heater_entity_id=None): + """check system mode""" + _timeout = 0 + _real_trv = self.real_trvs[heater_entity_id] + while _real_trv["hvac_mode"] != _real_trv["last_hvac_mode"]: + if _timeout > 360: + _LOGGER.debug( + f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the system mode change" + ) + _timeout = 0 + break + await asyncio.sleep(1) + _timeout += 1 + await asyncio.sleep(2) + _real_trv["system_mode_received"] = True + return True + + +async def check_target_temperature(self, heater_entity_id=None): + """Check if target temperature is reached.""" + _timeout = 0 + _real_trv = self.real_trvs[heater_entity_id] + while True: + _current_set_temperature = convert_to_float( + str( + self.hass.states.get(heater_entity_id).attributes.get( + "temperature", None + ) + ), + self.name, + "check_target_temperature()", + ) + if _timeout == 0: + _LOGGER.debug( + f"better_thermostat {self.name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}" + ) + if ( + _current_set_temperature is None + or _real_trv["last_temperature"] == _current_set_temperature + ): + _timeout = 0 + break + if _timeout > 120: + _LOGGER.debug( + f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the target temperature change" + ) + _timeout = 0 + break + await asyncio.sleep(1) + _timeout += 1 + await asyncio.sleep(2) + + _real_trv["target_temp_received"] = True + return True diff --git a/custom_components/better_thermostat/utils/helpers.py b/custom_components/better_thermostat/utils/helpers.py new file mode 100644 index 00000000..51b8e6dd --- /dev/null +++ b/custom_components/better_thermostat/utils/helpers.py @@ -0,0 +1,643 @@ +"""Helper functions for the Better Thermostat component.""" +import re +import logging +from datetime import datetime +from typing import Union +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import async_entries_for_config_entry + +from homeassistant.components.climate.const import HVACMode + +from custom_components.better_thermostat.utils.model_quirks import ( + fix_local_calibration, + fix_target_temperature_calibration, +) + + +from ..const import ( + CONF_HEAT_AUTO_SWAPPED, + CONF_HEATING_POWER_CALIBRATION, + CONF_FIX_CALIBRATION, + CONF_PROTECT_OVERHEATING, +) + + +_LOGGER = logging.getLogger(__name__) + + +def mode_remap(self, entity_id, hvac_mode: str, inbound: bool = False) -> str: + """Remap HVAC mode to correct mode if nessesary. + + Parameters + ---------- + self : + FIXME + hvac_mode : str + HVAC mode to be remapped + + inbound : bool + True if the mode is coming from the device, False if it is coming from the HA. + + Returns + ------- + str + remapped mode according to device's quirks + """ + _heat_auto_swapped = self.real_trvs[entity_id]["advanced"].get( + CONF_HEAT_AUTO_SWAPPED, False + ) + + if _heat_auto_swapped: + if hvac_mode == HVACMode.HEAT and inbound is False: + return HVACMode.AUTO + elif hvac_mode == HVACMode.AUTO and inbound is True: + return HVACMode.HEAT + else: + return hvac_mode + else: + if hvac_mode != HVACMode.AUTO: + return hvac_mode + else: + _LOGGER.error( + f"better_thermostat {self.name}: {entity_id} HVAC mode {hvac_mode} is not supported by this device, is it possible that you forgot to set the heat auto swapped option?" + ) + return HVACMode.OFF + + +def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]: + """Calculate local delta to adjust the setpoint of the TRV based on the air temperature of the external sensor. + + This calibration is for devices with local calibration option, it syncs the current temperature of the TRV to the target temperature of + the external sensor. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + float + new local calibration delta + """ + _context = "calculate_local_setpoint_delta()" + + if None in (self.cur_temp, self.bt_target_temp, self.old_internal_temp): + return None + + # check if we need to calculate + if ( + self.real_trvs[entity_id]["current_temperature"] == self.old_internal_temp + and self.cur_temp == self.old_external_temp + ): + return None + + _cur_trv_temp = convert_to_float( + str(self.real_trvs[entity_id]["current_temperature"]), self.name, _context + ) + + _calibration_delta = float( + str(format(float(abs(_cur_trv_temp - self.cur_temp)), ".1f")) + ) + + if _calibration_delta <= 0.5: + return None + + self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"] + self.old_external_temp = self.cur_temp + + _current_trv_calibration = round_to_half_degree( + convert_to_float( + str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context + ) + ) + + if None in (_current_trv_calibration, self.cur_temp, _cur_trv_temp): + _LOGGER.warning( + f"better thermostat {self.name}: {entity_id} Could not calculate local setpoint delta in {_context}:" + f" current_trv_calibration: {_current_trv_calibration}, current_trv_temp: {_cur_trv_temp}, cur_temp: {self.cur_temp}" + ) + return None + + _new_local_calibration = (self.cur_temp - _cur_trv_temp) + _current_trv_calibration + + _calibration_mode = self.real_trvs[entity_id]["advanced"].get( + "calibration_mode", "default" + ) + if _calibration_mode == CONF_FIX_CALIBRATION: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.30 and _new_local_calibration > -2.5: + _new_local_calibration -= 2.5 + + elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.0: + valve_position = heating_power_valve_position(self, entity_id) + _new_local_calibration = _current_trv_calibration - ( + (self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp) + * valve_position + ) + + _new_local_calibration = fix_local_calibration( + self, entity_id, _new_local_calibration + ) + + _overheating_protection = self.real_trvs[entity_id]["advanced"].get( + CONF_PROTECT_OVERHEATING, False + ) + + if _overheating_protection is True: + if self.cur_temp >= self.bt_target_temp: + _new_local_calibration += (self.cur_temp - self.bt_target_temp) * 10.0 + + _new_local_calibration = round_down_to_half_degree(_new_local_calibration) + + if _new_local_calibration > float( + self.real_trvs[entity_id]["local_calibration_max"] + ): + _new_local_calibration = float( + self.real_trvs[entity_id]["local_calibration_max"] + ) + elif _new_local_calibration < float( + self.real_trvs[entity_id]["local_calibration_min"] + ): + _new_local_calibration = float( + self.real_trvs[entity_id]["local_calibration_min"] + ) + + _new_local_calibration = convert_to_float( + str(_new_local_calibration), self.name, _context + ) + + _LOGGER.debug( + "better_thermostat %s: %s - output calib: %s", + self.name, + entity_id, + _new_local_calibration, + ) + + return convert_to_float(str(_new_local_calibration), self.name, _context) + + +def calculate_setpoint_override(self, entity_id) -> Union[float, None]: + """Calculate new setpoint for the TRV based on its own temperature measurement and the air temperature of the external sensor. + + This calibration is for devices with no local calibration option, it syncs the target temperature of the TRV to a new target + temperature based on the current temperature of the external sensor. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + float + new target temp with calibration + """ + if None in (self.cur_temp, self.bt_target_temp): + return None + + _cur_trv_temp = self.hass.states.get(entity_id).attributes["current_temperature"] + if None in (self.bt_target_temp, self.cur_temp, _cur_trv_temp): + return None + + _calibrated_setpoint = (self.bt_target_temp - self.cur_temp) + _cur_trv_temp + + _calibration_mode = self.real_trvs[entity_id]["advanced"].get( + "calibration_mode", "default" + ) + if _calibration_mode == CONF_FIX_CALIBRATION: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.0 and _calibrated_setpoint - _cur_trv_temp < 2.5: + _calibrated_setpoint += 2.5 + + elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.0: + valve_position = heating_power_valve_position(self, entity_id) + _calibrated_setpoint = _cur_trv_temp + ( + (self.real_trvs[entity_id]["max_temp"] - _cur_trv_temp) * valve_position + ) + + _calibrated_setpoint = fix_target_temperature_calibration( + self, entity_id, _calibrated_setpoint + ) + + _overheating_protection = self.real_trvs[entity_id]["advanced"].get( + CONF_PROTECT_OVERHEATING, False + ) + + if _overheating_protection is True: + if self.cur_temp >= self.bt_target_temp: + _calibrated_setpoint -= (self.cur_temp - self.bt_target_temp) * 10.0 + + _calibrated_setpoint = round_down_to_half_degree(_calibrated_setpoint) + + # check if new setpoint is inside the TRV's range, else set to min or max + if _calibrated_setpoint < self.real_trvs[entity_id]["min_temp"]: + _calibrated_setpoint = self.real_trvs[entity_id]["min_temp"] + if _calibrated_setpoint > self.real_trvs[entity_id]["max_temp"]: + _calibrated_setpoint = self.real_trvs[entity_id]["max_temp"] + + return _calibrated_setpoint + + +def heating_power_valve_position(self, entity_id): + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + valve_pos = (_temp_diff / self.heating_power) / 100 + if valve_pos < 0.0: + valve_pos = 0.0 + if valve_pos > 1.0: + valve_pos = 1.0 + + _LOGGER.debug( + f"better_thermostat {self.name}: {entity_id} / heating_power_valve_position - temp diff: {round(_temp_diff, 1)} - heating power: {round(self.heating_power, 4)} - expected valve position: {round(valve_pos * 100)}%" + ) + return valve_pos + + +def convert_to_float( + value: Union[str, int, float], instance_name: str, context: str +) -> Union[float, None]: + """Convert value to float or print error message. + + Parameters + ---------- + value : str, int, float + the value to convert to float + instance_name : str + the name of the instance thermostat + context : str + the name of the function which is using this, for printing an error message + + Returns + ------- + float + the converted value + None + If error occurred and cannot convert the value. + """ + if isinstance(value, float): + return round(value, 1) + elif value is None or value == "None": + return None + else: + try: + return round(float(str(format(float(value), ".1f"))), 1) + except (ValueError, TypeError, AttributeError, KeyError): + _LOGGER.debug( + f"better thermostat {instance_name}: Could not convert '{value}' to float in {context}" + ) + return None + + +def calibration_round(value: Union[int, float, None]) -> Union[float, int, None]: + """Round the calibration value to the nearest 0.5. + + Parameters + ---------- + value : float + the value to round + + Returns + ------- + float + the rounded value + """ + if value is None: + return None + split = str(float(str(value))).split(".", 1) + decimale = int(split[1]) + if decimale > 8: + return float(str(split[0])) + 1.0 + else: + return float(str(split[0])) + + +def round_down_to_half_degree( + value: Union[int, float, None] +) -> Union[float, int, None]: + """Round the value down to the nearest 0.5. + + Parameters + ---------- + value : float + the value to round + + Returns + ------- + float + the rounded value + """ + if value is None: + return None + split = str(float(str(value))).split(".", 1) + decimale = int(split[1]) + if decimale >= 5: + if float(split[0]) > 0: + return float(str(split[0])) + 0.5 + else: + return float(str(split[0])) - 0.5 + else: + return float(str(split[0])) + + +def round_to_half_degree(value: Union[int, float, None]) -> Union[float, int, None]: + """Rounds numbers to the nearest n.5/n.0 + + Parameters + ---------- + value : int, float + input value + + Returns + ------- + float, int + either an int, if input was an int, or a float rounded to n.5/n.0 + + """ + if value is None: + return None + elif isinstance(value, float): + return round(value * 2) / 2 + elif isinstance(value, int): + return value + + +def round_to_hundredth_degree( + value: Union[int, float, None] +) -> Union[float, int, None]: + """Rounds numbers to the nearest n.nn0 + + Parameters + ---------- + value : int, float + input value + + Returns + ------- + float, int + either an int, if input was an int, or a float rounded to n.nn0 + + """ + if value is None: + return None + elif isinstance(value, float): + return round(value * 100) / 100 + elif isinstance(value, int): + return value + + +def check_float(potential_float): + """Check if a string is a float. + + Parameters + ---------- + potential_float : + the value to check + + Returns + ------- + bool + True if the value is a float, False otherwise. + + """ + try: + float(potential_float) + return True + except ValueError: + return False + + +def convert_time(time_string): + """Convert a time string to a datetime object. + + Parameters + ---------- + time_string : + a string representing a time + + Returns + ------- + datetime + the converted time as a datetime object. + None + If the time string is not a valid time. + """ + try: + _current_time = datetime.now() + _get_hours_minutes = datetime.strptime(time_string, "%H:%M") + return _current_time.replace( + hour=_get_hours_minutes.hour, + minute=_get_hours_minutes.minute, + second=0, + microsecond=0, + ) + except ValueError: + return None + + +async def find_valve_entity(self, entity_id): + """Find the local calibration entity for the TRV. + + This is a hacky way to find the local calibration entity for the TRV. It is not possible to find the entity + automatically, because the entity_id is not the same as the friendly_name. The friendly_name is the same for all + thermostats of the same brand, but the entity_id is different. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + str + the entity_id of the local calibration entity + None + if no local calibration entity was found + """ + entity_registry = er.async_get(self.hass) + reg_entity = entity_registry.async_get(entity_id) + if reg_entity is None: + return None + entity_entries = async_entries_for_config_entry( + entity_registry, reg_entity.config_entry_id + ) + for entity in entity_entries: + uid = entity.unique_id + # Make sure we use the correct device entities + if entity.device_id == reg_entity.device_id: + if "_valve_position" in uid or "_position" in uid: + _LOGGER.debug( + f"better thermostat: Found valve position entity {entity.entity_id} for {entity_id}" + ) + return entity.entity_id + + _LOGGER.debug( + f"better thermostat: Could not find valve position entity for {entity_id}" + ) + return None + + +async def find_battery_entity(self, entity_id): + entity_registry = er.async_get(self.hass) + + entity_info = entity_registry.entities.get(entity_id) + + if entity_info is None: + return None + + device_id = entity_info.device_id + + for entity in entity_registry.entities.values(): + if entity.device_id == device_id and ( + entity.device_class == "battery" + or entity.original_device_class == "battery" + ): + return entity.entity_id + + return None + + +async def find_local_calibration_entity(self, entity_id): + """Find the local calibration entity for the TRV. + + This is a hacky way to find the local calibration entity for the TRV. It is not possible to find the entity + automatically, because the entity_id is not the same as the friendly_name. The friendly_name is the same for all + thermostats of the same brand, but the entity_id is different. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + str + the entity_id of the local calibration entity + None + if no local calibration entity was found + """ + entity_registry = er.async_get(self.hass) + reg_entity = entity_registry.async_get(entity_id) + if reg_entity is None: + return None + entity_entries = async_entries_for_config_entry( + entity_registry, reg_entity.config_entry_id + ) + for entity in entity_entries: + uid = entity.unique_id + # Make sure we use the correct device entities + if entity.device_id == reg_entity.device_id: + if "local_temperature_calibration" in uid: + _LOGGER.debug( + f"better thermostat: Found local calibration entity {entity.entity_id} for {entity_id}" + ) + return entity.entity_id + + _LOGGER.debug( + f"better thermostat: Could not find local calibration entity for {entity_id}" + ) + return None + + +async def get_trv_intigration(self, entity_id): + """Get the integration of the TRV. + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + str + the integration of the TRV + """ + entity_reg = er.async_get(self.hass) + entry = entity_reg.async_get(entity_id) + try: + return entry.platform + except AttributeError: + return "generic_thermostat" + + +def get_max_value(obj, value, default): + """Get the max value of an dict object.""" + try: + _raw = [] + for key in obj.keys(): + _temp = obj[key].get(value, 0) + if _temp is not None: + _raw.append(_temp) + return max(_raw, key=lambda x: float(x)) + except (KeyError, ValueError): + return default + + +def get_min_value(obj, value, default): + """Get the min value of an dict object.""" + try: + _raw = [] + for key in obj.keys(): + _temp = obj[key].get(value, 999) + if _temp is not None: + _raw.append(_temp) + return min(_raw, key=lambda x: float(x)) + except (KeyError, ValueError): + return default + + +async def get_device_model(self, entity_id): + """Fetches the device model from HA. + Parameters + ---------- + self : + self instance of better_thermostat + Returns + ------- + string + the name of the thermostat model + """ + if self.model is None: + try: + entity_reg = er.async_get(self.hass) + entry = entity_reg.async_get(entity_id) + dev_reg = dr.async_get(self.hass) + device = dev_reg.async_get(entry.device_id) + _LOGGER.debug(f"better_thermostat {self.name}: found device:") + _LOGGER.debug(device) + try: + # Z2M reports the device name as a long string with the actual model name in braces, we need to extract it + return re.search("\\((.+?)\\)", device.model).group(1) + except AttributeError: + # Other climate integrations might report the model name plainly, need more infos on this + return device.model + except ( + RuntimeError, + ValueError, + AttributeError, + KeyError, + TypeError, + NameError, + IndexError, + ): + try: + return ( + self.hass.states.get(entity_id) + .attributes.get("device") + .get("model", "generic") + ) + except ( + RuntimeError, + ValueError, + AttributeError, + KeyError, + TypeError, + NameError, + IndexError, + ): + return "generic" + else: + return self.model diff --git a/custom_components/better_thermostat/utils/model_quirks.py b/custom_components/better_thermostat/utils/model_quirks.py new file mode 100644 index 00000000..a1a938a5 --- /dev/null +++ b/custom_components/better_thermostat/utils/model_quirks.py @@ -0,0 +1,55 @@ +from importlib import import_module +import logging + +_LOGGER = logging.getLogger(__name__) + + +def load_model_quirks(self, model, entity_id): + """Load model.""" + + # remove / from model + model = model.replace("/", "_") + + try: + self.model_quirks = import_module( + "custom_components.better_thermostat.model_fixes." + model, + package="better_thermostat", + ) + _LOGGER.debug( + "better_thermostat %s: uses quirks fixes for model %s for trv %s", + self.name, + model, + entity_id, + ) + except Exception: + self.model_quirks = import_module( + "custom_components.better_thermostat.model_fixes.default", + package="better_thermostat", + ) + pass + + return self.model_quirks + + +def fix_local_calibration(self, entity_id, offset): + return self.real_trvs[entity_id]["model_quirks"].fix_local_calibration( + self, entity_id, offset + ) + + +def fix_target_temperature_calibration(self, entity_id, temperature): + return self.real_trvs[entity_id]["model_quirks"].fix_target_temperature_calibration( + self, entity_id, temperature + ) + + +async def override_set_hvac_mode(self, entity_id, hvac_mode): + return await self.real_trvs[entity_id]["model_quirks"].override_set_hvac_mode( + self, entity_id, hvac_mode + ) + + +async def override_set_temperature(self, entity_id, temperature): + return await self.real_trvs[entity_id]["model_quirks"].override_set_temperature( + self, entity_id, temperature + ) diff --git a/custom_components/better_thermostat/utils/watcher.py b/custom_components/better_thermostat/utils/watcher.py new file mode 100644 index 00000000..5c7a1969 --- /dev/null +++ b/custom_components/better_thermostat/utils/watcher.py @@ -0,0 +1,72 @@ +from __future__ import annotations +from homeassistant.helpers import issue_registry as ir +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +import logging + +DOMAIN = "better_thermostat" +_LOGGER = logging.getLogger(__name__) + + +async def check_entity(self, entity) -> bool: + if entity is None: + return False + entity_states = self.hass.states.get(entity) + if entity_states is None: + return False + state = entity_states.state + if state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + None, + "missing", + "unknown", + "unavail", + "unavailable", + ): + _LOGGER.debug( + f"better_thermostat {self.name}: {entity} is unavailable. with state {state}" + ) + return False + if entity in self.devices_errors: + self.devices_errors.remove(entity) + self.async_write_ha_state() + ir.async_delete_issue(self.hass, DOMAIN, f"missing_entity_{entity}") + self.hass.async_create_task(get_battery_status(self, entity)) + return True + + +async def get_battery_status(self, entity): + if entity in self.devices_states: + battery_id = self.devices_states[entity].get("battery_id") + if battery_id is not None: + new_battery = self.hass.states.get(battery_id) + if new_battery is not None: + battery = new_battery.state + self.devices_states[entity] = { + "battery": battery, + "battery_id": battery_id, + } + self.async_write_ha_state() + return + + +async def check_all_entities(self) -> bool: + entities = self.all_entities + for entity in entities: + if not await check_entity(self, entity): + name = entity + self.devices_errors.append(name) + self.async_write_ha_state() + ir.async_create_issue( + hass=self.hass, + domain=DOMAIN, + issue_id=f"missing_entity_{name}", + is_fixable=True, + is_persistent=False, + learn_more_url="https://better-thermostat.org/qanda/missing_entity", + severity=ir.IssueSeverity.WARNING, + translation_key="missing_entity", + translation_placeholders={"entity": str(name), "name": str(self.name)}, + ) + return False + return True diff --git a/custom_components/better_thermostat/utils/weather.py b/custom_components/better_thermostat/utils/weather.py new file mode 100644 index 00000000..f5ec0de1 --- /dev/null +++ b/custom_components/better_thermostat/utils/weather.py @@ -0,0 +1,243 @@ +from collections import deque +import logging +from datetime import timedelta, datetime +import homeassistant.util.dt as dt_util +from homeassistant.components.recorder import get_instance, history +from contextlib import suppress + +# from datetime import datetime, timedelta + +# import homeassistant.util.dt as dt_util +# from homeassistant.components.recorder.history import state_changes_during_period + +from .helpers import convert_to_float +from statistics import median + + +_LOGGER = logging.getLogger(__name__) + + +def check_weather(self) -> bool: + """check weather predictions or ambient air temperature if available + + Parameters + ---------- + self : + self instance of better_thermostat + + Returns + ------- + bool + true if call_for_heat was changed + """ + old_call_for_heat = self.call_for_heat + _call_for_heat_weather = False + _call_for_heat_outdoor = False + + self.call_for_heat = True + + if self.weather_entity is not None: + _call_for_heat_weather = check_weather_prediction(self) + self.call_for_heat = _call_for_heat_weather + + if self.outdoor_sensor is not None: + if None in (self.last_avg_outdoor_temp, self.off_temperature): + # TODO: add condition if heating period (oct-mar) then set it to true? + _LOGGER.warning( + "better_thermostat %s: no outdoor sensor data found. fallback to heat", + self.name, + ) + _call_for_heat_outdoor = True + else: + _call_for_heat_outdoor = self.last_avg_outdoor_temp < self.off_temperature + + self.call_for_heat = _call_for_heat_outdoor + + if self.weather_entity is None and self.outdoor_sensor is None: + self.call_for_heat = True + return True + + if old_call_for_heat != self.call_for_heat: + return True + else: + return False + + +def check_weather_prediction(self) -> bool: + """Checks configured weather entity for next two days of temperature predictions. + + Returns + ------- + bool + True if the maximum forcast temperature is lower than the off temperature + None + if not successful + """ + if self.weather_entity is None: + _LOGGER.warning(f"better_thermostat {self.name}: weather entity not available.") + return None + + if self.off_temperature is None or not isinstance(self.off_temperature, float): + _LOGGER.warning( + f"better_thermostat {self.name}: off_temperature not set or not a float." + ) + return None + + try: + forecast = self.hass.states.get(self.weather_entity).attributes.get("forecast") + if len(forecast) > 0: + cur_outside_temp = convert_to_float( + str( + self.hass.states.get(self.weather_entity).attributes.get( + "temperature" + ) + ), + self.name, + "check_weather_prediction()", + ) + max_forecast_temp = int( + round( + ( + convert_to_float( + str(forecast[0]["temperature"]), + self.name, + "check_weather_prediction()", + ) + + convert_to_float( + str(forecast[1]["temperature"]), + self.name, + "check_weather_prediction()", + ) + ) + / 2 + ) + ) + return ( + cur_outside_temp < self.off_temperature + or max_forecast_temp < self.off_temperature + ) + else: + raise TypeError + except TypeError: + _LOGGER.warning(f"better_thermostat {self.name}: no weather entity data found.") + return None + + +async def check_ambient_air_temperature(self): + """Gets the history for two days and evaluates the necessary for heating. + + Returns + ------- + bool + True if the average temperature is lower than the off temperature + None + if not successful + """ + if self.outdoor_sensor is None: + return None + + if self.off_temperature is None or not isinstance(self.off_temperature, float): + _LOGGER.warning( + f"better_thermostat {self.name}: off_temperature not set or not a float." + ) + return None + + self.last_avg_outdoor_temp = convert_to_float( + self.hass.states.get(self.outdoor_sensor).state, + self.name, + "check_ambient_air_temperature()", + ) + if "recorder" in self.hass.config.components: + _temp_history = DailyHistory(2) + start_date = dt_util.utcnow() - timedelta(days=2) + entity_id = self.outdoor_sensor + if entity_id is None: + _LOGGER.debug( + "Not reading the history from the database as " + "there is no outdoor sensor configured" + ) + return + _LOGGER.debug( + "Initializing values for %s from the database", self.outdoor_sensor + ) + lower_entity_id = entity_id.lower() + history_list = await get_instance(self.hass).async_add_executor_job( + history.state_changes_during_period, + self.hass, + start_date, + dt_util.utcnow(), + lower_entity_id, + ) + + for item in history_list.get(lower_entity_id): + # filter out all None, NaN and "unknown" states + # only keep real values + with suppress(ValueError): + if item.state != "unknown": + _temp_history.add_measurement( + convert_to_float( + item.state, self.name, "check_ambient_air_temperature()" + ), + datetime.fromtimestamp(item.last_updated.timestamp()), + ) + + avg_temp = _temp_history.min + + _LOGGER.debug("Initializing from database completed") + else: + avg_temp = self.last_avg_outdoor_temp + + _LOGGER.debug( + f"better_thermostat {self.name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}" + ) + + if avg_temp is not None: + self.call_for_heat = avg_temp < self.off_temperature + else: + self.call_for_heat = True + + self.last_avg_outdoor_temp = avg_temp + + +class DailyHistory: + """Stores one measurement per day for a maximum number of days. + At the moment only the maximum value per day is kept. + """ + + def __init__(self, max_length): + """Create new DailyHistory with a maximum length of the history.""" + self.max_length = max_length + self._days = None + self._max_dict = {} + self.min = None + + def add_measurement(self, value, timestamp=None): + """Add a new measurement for a certain day.""" + day = (timestamp or datetime.now()).date() + if not isinstance(value, (int, float)): + return + if self._days is None: + self._days = deque() + self._add_day(day, value) + else: + current_day = self._days[-1] + if day == current_day: + self._max_dict[day] = min(value, self._max_dict[day]) + elif day > current_day: + self._add_day(day, value) + else: + _LOGGER.warning("Received old measurement, not storing it") + + self.min = median(self._max_dict.values()) + + def _add_day(self, day, value): + """Add a new day to the history. + Deletes the oldest day, if the queue becomes too long. + """ + if len(self._days) == self.max_length: + oldest = self._days.popleft() + del self._max_dict[oldest] + self._days.append(day) + if not isinstance(value, (int, float)): + return + self._max_dict[day] = value diff --git a/custom_components/frigate/__init__.py b/custom_components/frigate/__init__.py index 21546fc1..88011612 100644 --- a/custom_components/frigate/__init__.py +++ b/custom_components/frigate/__init__.py @@ -48,14 +48,7 @@ STATUS_RUNNING, STATUS_STARTING, ) -from .views import ( - JSMPEGProxyView, - NotificationsProxyView, - SnapshotsProxyView, - ThumbnailsProxyView, - VodProxyView, - VodSegmentProxyView, -) +from .views import async_setup as views_async_setup from .ws_api import async_setup as ws_api_async_setup SCAN_INTERVAL = timedelta(seconds=5) @@ -171,21 +164,16 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: hass.data.setdefault(DOMAIN, {}) ws_api_async_setup(hass) - - session = async_get_clientsession(hass) - hass.http.register_view(JSMPEGProxyView(session)) - hass.http.register_view(NotificationsProxyView(session)) - hass.http.register_view(SnapshotsProxyView(session)) - hass.http.register_view(ThumbnailsProxyView(session)) - hass.http.register_view(VodProxyView(session)) - hass.http.register_view(VodSegmentProxyView(session)) + views_async_setup(hass) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" - - client = FrigateApiClient(entry.data.get(CONF_URL), async_get_clientsession(hass)) + client = FrigateApiClient( + entry.data.get(CONF_URL), + async_get_clientsession(hass), + ) coordinator = FrigateDataUpdateCoordinator(hass, client=client) await coordinator.async_config_entry_first_refresh() @@ -222,6 +210,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for item in get_cameras_and_zones(config): current_devices.add(get_frigate_device_identifier(entry, item)) + if config.get("birdseye", {}).get("restream", False): + current_devices.add(get_frigate_device_identifier(entry, "birdseye")) + device_registry = dr.async_get(hass) for device_entry in dr.async_entries_for_config_entry( device_registry, entry.entry_id @@ -286,7 +277,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=new_name, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) return True diff --git a/custom_components/frigate/api.py b/custom_components/frigate/api.py index c5c10ca3..e73d8f9f 100644 --- a/custom_components/frigate/api.py +++ b/custom_components/frigate/api.py @@ -53,27 +53,31 @@ async def async_get_stats(self) -> dict[str, Any]: async def async_get_events( self, - camera: str | None = None, - label: str | None = None, - zone: str | None = None, + cameras: list[str] | None = None, + labels: list[str] | None = None, + sub_labels: list[str] | None = None, + zones: list[str] | None = None, after: int | None = None, before: int | None = None, limit: int | None = None, has_clip: bool | None = None, has_snapshot: bool | None = None, + favorites: bool | None = None, decode_json: bool = True, ) -> list[dict[str, Any]]: """Get data from the API.""" params = { - "camera": camera, - "label": label, - "zone": zone, + "cameras": ",".join(cameras) if cameras else None, + "labels": ",".join(labels) if labels else None, + "sub_labels": ",".join(sub_labels) if sub_labels else None, + "zones": ",".join(zones) if zones else None, "after": after, "before": before, "limit": limit, "has_clip": int(has_clip) if has_clip is not None else None, "has_snapshot": int(has_snapshot) if has_snapshot is not None else None, "include_thumbnails": 0, + "favorites": int(favorites) if favorites is not None else None, } return cast( @@ -93,12 +97,14 @@ async def async_get_event_summary( self, has_clip: bool | None = None, has_snapshot: bool | None = None, + timezone: str | None = None, decode_json: bool = True, ) -> list[dict[str, Any]]: """Get data from the API.""" params = { "has_clip": int(has_clip) if has_clip is not None else None, "has_snapshot": int(has_snapshot) if has_snapshot is not None else None, + "timezone": str(timezone) if timezone is not None else None, } return cast( @@ -137,15 +143,21 @@ async def async_retain( return cast(dict[str, Any], result) if decode_json else result async def async_get_recordings_summary( - self, camera: str, decode_json: bool = True - ) -> dict[str, Any] | str: + self, camera: str, timezone: str, decode_json: bool = True + ) -> list[dict[str, Any]] | str: """Get recordings summary.""" + params = {"timezone": timezone} + result = await self.api_wrapper( "get", - str(URL(self._host) / f"api/{camera}/recordings/summary"), + str( + URL(self._host) + / f"api/{camera}/recordings/summary" + % {k: v for k, v in params.items() if v is not None} + ), decode_json=decode_json, ) - return cast(dict[str, Any], result) if decode_json else result + return cast(list[dict[str, Any]], result) if decode_json else result async def async_get_recordings( self, diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index eab67275..515a9610 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -5,8 +5,7 @@ from typing import Any, cast from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -134,7 +133,7 @@ def is_on(self) -> bool: @property def device_class(self) -> str: """Return the device class.""" - return cast(str, DEVICE_CLASS_OCCUPANCY) + return cast(str, BinarySensorDeviceClass.OCCUPANCY) @property def icon(self) -> str: @@ -210,4 +209,4 @@ def is_on(self) -> bool: @property def device_class(self) -> str: """Return the device class.""" - return cast(str, DEVICE_CLASS_MOTION) + return cast(str, BinarySensorDeviceClass.MOTION) diff --git a/custom_components/frigate/camera.py b/custom_components/frigate/camera.py index 10aea4e7..ef05a6e0 100644 --- a/custom_components/frigate/camera.py +++ b/custom_components/frigate/camera.py @@ -36,6 +36,7 @@ ATTR_EVENT_ID, ATTR_FAVORITE, CONF_RTMP_URL_TEMPLATE, + CONF_RTSP_URL_TEMPLATE, DEVICE_CLASS_CAMERA, DOMAIN, NAME, @@ -66,6 +67,11 @@ async def async_setup_entry( FrigateMqttSnapshots(entry, frigate_config, cam_name, obj_name) for cam_name, obj_name in get_cameras_and_objects(frigate_config, False) ] + + ( + [BirdseyeCamera(entry, frigate_client)] + if frigate_config.get("birdseye", {}).get("restream", False) + else [] + ) ) # setup services @@ -81,7 +87,7 @@ async def async_setup_entry( class FrigateCamera(FrigateMQTTEntity, Camera): # type: ignore[misc] - """Representation a Frigate camera.""" + """Representation of a Frigate camera.""" # sets the entity name to same as device name ex: camera.front_doorbell _attr_name = None @@ -130,7 +136,11 @@ def __init__( # The device_class is used to filter out regular camera entities # from motion camera entities on selectors self._attr_device_class = DEVICE_CLASS_CAMERA - self._attr_is_streaming = self._camera_config.get("rtmp", {}).get("enabled") + self._attr_is_streaming = ( + self._camera_config.get("rtmp", {}).get("enabled") + or self._cam_name + in self._frigate_config.get("go2rtc", {}).get("streams", {}).keys() + ) self._attr_is_recording = self._camera_config.get("record", {}).get("enabled") self._attr_motion_detection_enabled = self._camera_config.get("motion", {}).get( "enabled" @@ -139,20 +149,48 @@ def __init__( f"{frigate_config['mqtt']['topic_prefix']}" f"/{self._cam_name}/motion/set" ) - streaming_template = config_entry.options.get( - CONF_RTMP_URL_TEMPLATE, "" - ).strip() - - if streaming_template: - # Can't use homeassistant.helpers.template as it requires hass which - # is not available in the constructor, so use direct jinja2 - # template instead. This means templates cannot access HomeAssistant - # state, but rather only the camera config. - self._stream_source = Template(streaming_template).render( - **self._camera_config - ) + if ( + self._cam_name + in self._frigate_config.get("go2rtc", {}).get("streams", {}).keys() + ): + self._restream_type = "rtsp" + streaming_template = config_entry.options.get( + CONF_RTSP_URL_TEMPLATE, "" + ).strip() + + if streaming_template: + # Can't use homeassistant.helpers.template as it requires hass which + # is not available in the constructor, so use direct jinja2 + # template instead. This means templates cannot access HomeAssistant + # state, but rather only the camera config. + self._stream_source = Template(streaming_template).render( + **self._camera_config + ) + else: + self._stream_source = ( + f"rtsp://{URL(self._url).host}:8554/{self._cam_name}" + ) + + elif self._camera_config.get("rtmp", {}).get("enabled"): + self._restream_type = "rtmp" + streaming_template = config_entry.options.get( + CONF_RTMP_URL_TEMPLATE, "" + ).strip() + + if streaming_template: + # Can't use homeassistant.helpers.template as it requires hass which + # is not available in the constructor, so use direct jinja2 + # template instead. This means templates cannot access HomeAssistant + # state, but rather only the camera config. + self._stream_source = Template(streaming_template).render( + **self._camera_config + ) + else: + self._stream_source = ( + f"rtmp://{URL(self._url).host}/live/{self._cam_name}" + ) else: - self._stream_source = f"rtmp://{URL(self._url).host}/live/{self._cam_name}" + self._restream_type = "none" @callback # type: ignore[misc] def _state_message_received(self, msg: ReceiveMessage) -> None: @@ -189,6 +227,13 @@ def device_info(self) -> dict[str, Any]: "manufacturer": NAME, } + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return entity specific state attributes.""" + return { + "restream_type": self._restream_type, + } + @property def supported_features(self) -> int: """Return supported features of this camera.""" @@ -209,7 +254,7 @@ async def async_camera_image( % ({"h": height} if height is not None and height > 0 else {}) ) - with async_timeout.timeout(10): + async with async_timeout.timeout(10): response = await websession.get(image_url) return await response.read() @@ -244,6 +289,93 @@ async def favorite_event(self, event_id: str, favorite: bool) -> None: await self._client.async_retain(event_id, favorite) +class BirdseyeCamera(FrigateEntity, Camera): # type: ignore[misc] + """Representation of the Frigate birdseye camera.""" + + # sets the entity name to same as device name ex: camera.front_doorbell + _attr_name = None + + def __init__( + self, + config_entry: ConfigEntry, + frigate_client: FrigateApiClient, + ) -> None: + """Initialize the birdseye camera.""" + self._client = frigate_client + FrigateEntity.__init__(self, config_entry) + Camera.__init__(self) + self._url = config_entry.data[CONF_URL] + self._attr_is_on = True + # The device_class is used to filter out regular camera entities + # from motion camera entities on selectors + self._attr_device_class = DEVICE_CLASS_CAMERA + self._attr_is_streaming = True + self._attr_is_recording = False + + streaming_template = config_entry.options.get( + CONF_RTSP_URL_TEMPLATE, "" + ).strip() + + if streaming_template: + # Can't use homeassistant.helpers.template as it requires hass which + # is not available in the constructor, so use direct jinja2 + # template instead. This means templates cannot access HomeAssistant + # state, but rather only the camera config. + self._stream_source = Template(streaming_template).render( + {"name": "birdseye"} + ) + else: + self._stream_source = f"rtsp://{URL(self._url).host}:8554/birdseye" + + @property + def unique_id(self) -> str: + """Return a unique ID to use for this entity.""" + return get_frigate_entity_unique_id( + self._config_entry.entry_id, + "camera", + "birdseye", + ) + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return { + "identifiers": { + get_frigate_device_identifier(self._config_entry, "birdseye") + }, + "via_device": get_frigate_device_identifier(self._config_entry), + "name": "Birdseye", + "model": self._get_model(), + "configuration_url": f"{self._url}/cameras/birdseye", + "manufacturer": NAME, + } + + @property + def supported_features(self) -> int: + """Return supported features of this camera.""" + return cast(int, CameraEntityFeature.STREAM) + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return bytes of camera image.""" + websession = cast(aiohttp.ClientSession, async_get_clientsession(self.hass)) + + image_url = str( + URL(self._url) + / "api/birdseye/latest.jpg" + % ({"h": height} if height is not None and height > 0 else {}) + ) + + async with async_timeout.timeout(10): + response = await websession.get(image_url) + return await response.read() + + async def stream_source(self) -> str | None: + """Return the source of the stream.""" + return self._stream_source + + class FrigateMqttSnapshots(FrigateMQTTEntity, Camera): # type: ignore[misc] """Frigate best camera class.""" diff --git a/custom_components/frigate/config_flow.py b/custom_components/frigate/config_flow.py index 712bb9c4..26c9c741 100644 --- a/custom_components/frigate/config_flow.py +++ b/custom_components/frigate/config_flow.py @@ -20,6 +20,7 @@ CONF_NOTIFICATION_PROXY_ENABLE, CONF_NOTIFICATION_PROXY_EXPIRE_AFTER_SECONDS, CONF_RTMP_URL_TEMPLATE, + CONF_RTSP_URL_TEMPLATE, DEFAULT_HOST, DOMAIN, ) @@ -143,6 +144,16 @@ async def async_step_init( "", ), ): str, + # The input URL is not validated as being a URL to allow for the + # possibility the template input won't be a valid URL until after + # it's rendered. + vol.Optional( + CONF_RTSP_URL_TEMPLATE, + default=self._config_entry.options.get( + CONF_RTSP_URL_TEMPLATE, + "", + ), + ): str, vol.Optional( CONF_NOTIFICATION_PROXY_ENABLE, default=self._config_entry.options.get( diff --git a/custom_components/frigate/const.py b/custom_components/frigate/const.py index a3512bac..568793d9 100644 --- a/custom_components/frigate/const.py +++ b/custom_components/frigate/const.py @@ -40,6 +40,7 @@ CONF_PASSWORD = "password" CONF_PATH = "path" CONF_RTMP_URL_TEMPLATE = "rtmp_url_template" +CONF_RTSP_URL_TEMPLATE = "rtsp_url_template" CONF_NOTIFICATION_PROXY_EXPIRE_AFTER_SECONDS = "notification_proxy_expire_after_seconds" # Defaults diff --git a/custom_components/frigate/manifest.json b/custom_components/frigate/manifest.json index 00b38066..22d4ebe6 100644 --- a/custom_components/frigate/manifest.json +++ b/custom_components/frigate/manifest.json @@ -1,18 +1,18 @@ { "domain": "frigate", - "documentation": "https://github.com/blakeblackshear/frigate", "name": "Frigate", - "version": "3.0.0", - "issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues", + "codeowners": [ + "@blakeblackshear" + ], + "config_flow": true, "dependencies": [ "http", "media_source", "mqtt" ], - "config_flow": true, - "codeowners": [ - "@blakeblackshear" - ], - "requirements": [], - "iot_class": "local_push" + "documentation": "https://github.com/blakeblackshear/frigate", + "iot_class": "local_push", + "issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues", + "requirements": ["pytz==2022.7"], + "version": "4.0.0" } diff --git a/custom_components/frigate/media_source.py b/custom_components/frigate/media_source.py index 2151ec9b..a73e4dc1 100644 --- a/custom_components/frigate/media_source.py +++ b/custom_components/frigate/media_source.py @@ -4,10 +4,11 @@ import datetime as dt import enum import logging -from typing import Any +from typing import Any, cast import attr from dateutil.relativedelta import relativedelta +import pytz from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -26,6 +27,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import system_info from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util.dt import DEFAULT_TIME_ZONE @@ -123,7 +125,7 @@ def get_identifier_type(cls) -> str: """Get the identifier type.""" raise NotImplementedError - def get_integration_proxy_path(self) -> str: + def get_integration_proxy_path(self, timezone: str) -> str: """Get the proxy (Home Assistant view) path for this identifier.""" raise NotImplementedError @@ -249,7 +251,7 @@ def get_identifier_type(cls) -> str: """Get the identifier type.""" return "event" - def get_integration_proxy_path(self) -> str: + def get_integration_proxy_path(self, timezone: str) -> str: """Get the equivalent Frigate server path.""" if self.frigate_media_type == FrigateMediaType.CLIPS: return f"vod/event/{self.id}/index.{self.frigate_media_type.extension}" @@ -365,22 +367,15 @@ def media_class(self) -> str: return self.frigate_media_type.media_class -def _validate_year_month( +def _validate_year_month_day( inst: RecordingIdentifier, attribute: attr.Attribute, data: str | None ) -> None: """Validate input.""" if data: - year, month = data.split("-") - if int(year) < 0 or int(month) <= 0 or int(month) > 12: - raise ValueError("Invalid year-month in identifier: %s" % data) - - -def _validate_day( - inst: RecordingIdentifier, attribute: attr.Attribute, value: int | None -) -> None: - """Determine if a value is a valid day.""" - if value is not None and (int(value) < 1 or int(value) > 31): - raise ValueError("Invalid day in identifier: %s" % value) + try: + dt.datetime.strptime(data, "%Y-%m-%d") + except ValueError as exc: + raise ValueError("Invalid date in identifier: %s" % data) from exc def _validate_hour( @@ -395,20 +390,15 @@ def _validate_hour( class RecordingIdentifier(Identifier): """Recording Identifier.""" - year_month: str | None = attr.ib( - default=None, - validator=[ - attr.validators.instance_of((str, type(None))), - _validate_year_month, - ], + camera: str | None = attr.ib( + default=None, validator=[attr.validators.instance_of((str, type(None)))] ) - day: int | None = attr.ib( + year_month_day: str | None = attr.ib( default=None, - converter=_to_int_or_none, validator=[ - attr.validators.instance_of((int, type(None))), - _validate_day, + attr.validators.instance_of((str, type(None))), + _validate_year_month_day, ], ) @@ -421,10 +411,6 @@ class RecordingIdentifier(Identifier): ], ) - camera: str | None = attr.ib( - default=None, validator=[attr.validators.instance_of((str, type(None)))] - ) - @classmethod def from_str( cls, data: str, default_frigate_instance_id: str | None = None @@ -440,10 +426,9 @@ def from_str( try: return cls( frigate_instance_id=parts[0], - year_month=cls._get_index(parts, 2), - day=cls._get_index(parts, 3), + camera=cls._get_index(parts, 2), + year_month_day=cls._get_index(parts, 3), hour=cls._get_index(parts, 4), - camera=cls._get_index(parts, 5), ) except ValueError: return None @@ -455,10 +440,11 @@ def __str__(self) -> str: + [ self._empty_if_none(val) for val in ( - self.year_month, - f"{self.day:02}" if self.day is not None else None, - f"{self.hour:02}" if self.hour is not None else None, self.camera, + f"{self.year_month_day}" + if self.year_month_day is not None + else None, + f"{self.hour:02}" if self.hour is not None else None, ) ] ) @@ -468,38 +454,40 @@ def get_identifier_type(cls) -> str: """Get the identifier type.""" return "recordings" - def get_integration_proxy_path(self) -> str: + def get_integration_proxy_path(self, timezone: str) -> str: """Get the integration path that will proxy this identifier.""" - # The attributes of this class represent a path that the recording can - # be retrieved from the Frigate server. If there are holes in the path - # (i.e. missing attributes) the path won't work on the Frigate server, - # so the path returned is either complete or up until the first "hole" / - # missing attribute. - - in_parts = [ - self.get_identifier_type() if not self.camera else "vod", - self.year_month, - f"{self.day:02}" if self.day is not None else None, - f"{self.hour:02}" if self.hour is not None else None, - self.camera, - "index.m3u8" if self.camera else None, - ] - - out_parts = [] - for val in in_parts: - if val is None: - break - out_parts.append(str(val)) - - return "/".join(out_parts) - - def get_changes_to_set_next_empty(self, data: str) -> dict[str, str]: - """Get the changes that would set the next attribute in the hierarchy.""" - for attribute in self.__attrs_attrs__: # type: ignore[attr-defined] - if getattr(self, attribute.name) is None: - return {attribute.name: data} - raise ValueError("No empty attribute available") + if ( + self.camera is not None + and self.year_month_day is not None + and self.hour is not None + ): + year, month, day = self.year_month_day.split("-") + # Take the selected time in users local time and find the offset to + # UTC, convert to UTC then request the vod for that time. + start_date: dt.datetime = dt.datetime( + int(year), + int(month), + int(day), + int(self.hour), + tzinfo=dt.timezone.utc, + ) - (dt.datetime.now(pytz.timezone(timezone)).utcoffset() or dt.timedelta()) + + parts = [ + "vod", + f"{start_date.year}-{start_date.month:02}", + f"{start_date.day:02}", + f"{start_date.hour:02}", + self.camera, + "utc", + "index.m3u8", + ] + + return "/".join(parts) + + raise MediaSourceError( + "Can not get proxy-path without year_month_day and hour." + ) @property def mime_type(self) -> str: @@ -588,7 +576,10 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: if identifier and self._is_allowed_as_media_source( identifier.frigate_instance_id ): - server_path = identifier.get_integration_proxy_path() + info = await system_info.async_get_system_info(self.hass) + server_path = identifier.get_integration_proxy_path( + info.get("timezone", "utc") + ) return PlayMedia( f"/api/frigate/{identifier.frigate_instance_id}/{server_path}", identifier.mime_type, @@ -693,9 +684,10 @@ async def async_browse_media( events = await self._get_client(identifier).async_get_events( after=identifier.after, before=identifier.before, - camera=identifier.camera, - label=identifier.label, - zone=identifier.zone, + cameras=[identifier.camera] if identifier.camera else None, + labels=[identifier.label] if identifier.label else None, + sub_labels=None, + zones=[identifier.zone] if identifier.zone else None, limit=10000 if identifier.name.endswith(".all") else ITEM_LIMIT, **media_kwargs, ) @@ -707,18 +699,26 @@ async def async_browse_media( ) if isinstance(identifier, RecordingIdentifier): - path = identifier.get_integration_proxy_path() try: - recordings_folder = await self._get_client(identifier).async_get_path( - path + if not identifier.camera: + config = await self._get_client(identifier).async_get_config() + return self._get_camera_recording_folders(identifier, config) + + info = await system_info.async_get_system_info(self.hass) + recording_summary = cast( + list[dict[str, Any]], + await self._get_client(identifier).async_get_recordings_summary( + camera=identifier.camera, timezone=info.get("timezone", "utc") + ), ) + + if not identifier.year_month_day: + return self._get_recording_days(identifier, recording_summary) + + return self._get_recording_hours(identifier, recording_summary) except FrigateApiClientError as exc: raise MediaSourceError from exc - if identifier.hour is None: - return self._browse_recording_folders(identifier, recordings_folder) - return self._browse_recordings(identifier, recordings_folder) - raise MediaSourceError("Invalid media source identifier: %s" % item.identifier) async def _get_event_summary_data( @@ -727,12 +727,14 @@ async def _get_event_summary_data( """Get event summary data.""" try: + info = await system_info.async_get_system_info(self.hass) + if identifier.frigate_media_type == FrigateMediaType.CLIPS: kwargs = {"has_clip": True} else: kwargs = {"has_snapshot": True} summary_data = await self._get_client(identifier).async_get_event_summary( - **kwargs + timezone=info.get("timezone", "utc"), **kwargs ) except FrigateApiClientError as exc: raise MediaSourceError from exc @@ -1242,109 +1244,110 @@ def _count_by( ] ) - @classmethod - def _generate_recording_title( - cls, identifier: RecordingIdentifier, folder: dict[str, Any] | None = None - ) -> str | None: - """Generate recording title.""" - try: - if identifier.hour is not None: - if folder is None: - return dt.datetime.strptime( - f"{identifier.hour}.00.00", "%H.%M.%S" - ).strftime("%T") - return get_friendly_name(folder["name"]) - - if identifier.day is not None: - if folder is None: - return dt.datetime.strptime( - f"{identifier.year_month}-{identifier.day}", "%Y-%m-%d" - ).strftime("%B %d") - return dt.datetime.strptime( - f"{folder['name']}.00.00", "%H.%M.%S" - ).strftime("%T") - - if identifier.year_month is not None: - if folder is None: - return dt.datetime.strptime( - f"{identifier.year_month}", "%Y-%m" - ).strftime("%B %Y") - return dt.datetime.strptime( - f"{identifier.year_month}-{folder['name']}", "%Y-%m-%d" - ).strftime("%B %d") - - if folder is None: - return "Recordings" - return dt.datetime.strptime(f"{folder['name']}", "%Y-%m").strftime("%B %Y") - except ValueError: - return None - def _get_recording_base_media_source( self, identifier: RecordingIdentifier ) -> BrowseMediaSource: """Get the base BrowseMediaSource object for a recording identifier.""" - title = self._generate_recording_title(identifier) - - # Must be able to generate a title for the source folder. - if not title: - raise MediaSourceError - return BrowseMediaSource( domain=DOMAIN, identifier=identifier, media_class=MEDIA_CLASS_DIRECTORY, children_media_class=MEDIA_CLASS_DIRECTORY, media_content_type=identifier.media_type, - title=title, + title="Recordings", can_play=False, can_expand=True, thumbnail=None, children=[], ) - def _browse_recording_folders( - self, identifier: RecordingIdentifier, folders: list[dict[str, Any]] + def _get_camera_recording_folders( + self, identifier: RecordingIdentifier, config: dict[str, dict] ) -> BrowseMediaSource: - """Browse Frigate recording folders.""" + """List cameras for recordings.""" base = self._get_recording_base_media_source(identifier) - for folder in folders: - if folder["name"].endswith(".mp4"): - continue - title = self._generate_recording_title(identifier, folder) - if not title: - _LOGGER.warning("Skipping non-standard folder name: %s", folder["name"]) - continue + for camera in config["cameras"].keys(): base.children.append( BrowseMediaSource( domain=DOMAIN, identifier=attr.evolve( identifier, - **identifier.get_changes_to_set_next_empty(folder["name"]), + camera=camera, ), media_class=MEDIA_CLASS_DIRECTORY, children_media_class=MEDIA_CLASS_DIRECTORY, media_content_type=identifier.media_type, - title=title, + title=get_friendly_name(camera), can_play=False, can_expand=True, thumbnail=None, ) ) + return base - def _browse_recordings( - self, identifier: RecordingIdentifier, recordings: list[dict[str, Any]] + def _get_recording_days( + self, identifier: RecordingIdentifier, recording_days: list[dict[str, Any]] + ) -> BrowseMediaSource: + """List year-month-day options for camera.""" + base = self._get_recording_base_media_source(identifier) + + for day_item in recording_days: + try: + dt.datetime.strptime(day_item["day"], "%Y-%m-%d") + except ValueError as exc: + raise MediaSourceError( + "Media source is not valid for %s %s" + % (identifier, day_item["day"]) + ) from exc + + base.children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=attr.evolve( + identifier, + year_month_day=day_item["day"], + ), + media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=identifier.media_type, + title=day_item["day"], + can_play=False, + can_expand=True, + thumbnail=None, + ) + ) + + return base + + def _get_recording_hours( + self, identifier: RecordingIdentifier, recording_days: list[dict[str, Any]] ) -> BrowseMediaSource: """Browse Frigate recordings.""" base = self._get_recording_base_media_source(identifier) + hour_items: list[dict[str, Any]] = next( + ( + hours["hours"] + for hours in recording_days + if hours["day"] == identifier.year_month_day + ), + [], + ) + + for hour_data in hour_items: + try: + title = dt.datetime.strptime(hour_data["hour"], "%H").strftime("%H:00") + except ValueError as exc: + raise MediaSourceError( + "Media source is not valid for %s %s" + % (identifier, hour_data["hour"]) + ) from exc - for recording in recordings: - title = self._generate_recording_title(identifier, recording) base.children.append( BrowseMediaSource( domain=DOMAIN, - identifier=attr.evolve(identifier, camera=recording["name"]), + identifier=attr.evolve(identifier, hour=hour_data["hour"]), media_class=identifier.media_class, media_content_type=identifier.media_type, title=title, diff --git a/custom_components/frigate/sensor.py b/custom_components/frigate/sensor.py index 8c73886b..e38e57cb 100644 --- a/custom_components/frigate/sensor.py +++ b/custom_components/frigate/sensor.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL, TEMP_CELSIUS +from homeassistant.const import CONF_URL, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,6 +16,7 @@ FrigateEntity, FrigateMQTTEntity, ReceiveMessage, + get_cameras, get_cameras_zones_and_objects, get_friendly_name, get_frigate_device_identifier, @@ -34,6 +35,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Sensor entry setup.""" + frigate_config = hass.data[DOMAIN][entry.entry_id][ATTR_CONFIG] coordinator = hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATOR] entities = [] @@ -43,10 +45,24 @@ async def async_setup_entry( elif key == "detectors": for name in value.keys(): entities.append(DetectorSpeedSensor(coordinator, entry, name)) + elif key == "gpu_usages": + for name in value.keys(): + entities.append(GpuLoadSensor(coordinator, entry, name)) elif key == "service": # Temperature is only supported on PCIe Coral. for name in value.get("temperatures", {}): entities.append(DeviceTempSensor(coordinator, entry, name)) + elif key == "cpu_usages": + for camera in get_cameras(frigate_config): + entities.append( + CameraProcessCpuSensor(coordinator, entry, camera, "capture") + ) + entities.append( + CameraProcessCpuSensor(coordinator, entry, camera, "detect") + ) + entities.append( + CameraProcessCpuSensor(coordinator, entry, camera, "ffmpeg") + ) else: entities.extend( [CameraFpsSensor(coordinator, entry, key, t) for t in CAMERA_FPS_TYPES] @@ -228,6 +244,73 @@ def icon(self) -> str: return ICON_SPEEDOMETER +class GpuLoadSensor(FrigateEntity, CoordinatorEntity): # type: ignore[misc] + """Frigate GPU Load class.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + coordinator: FrigateDataUpdateCoordinator, + config_entry: ConfigEntry, + gpu_name: str, + ) -> None: + """Construct a GpuLoadSensor.""" + self._gpu_name = gpu_name + self._attr_name = f"{get_friendly_name(self._gpu_name)} gpu load" + FrigateEntity.__init__(self, config_entry) + CoordinatorEntity.__init__(self, coordinator) + self._attr_entity_registry_enabled_default = False + + @property + def unique_id(self) -> str: + """Return a unique ID to use for this entity.""" + return get_frigate_entity_unique_id( + self._config_entry.entry_id, "gpu_load", self._gpu_name + ) + + @property + def device_info(self) -> DeviceInfo: + """Get device information.""" + return { + "identifiers": {get_frigate_device_identifier(self._config_entry)}, + "name": NAME, + "model": self._get_model(), + "configuration_url": self._config_entry.data.get(CONF_URL), + "manufacturer": NAME, + } + + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + if self.coordinator.data: + data = ( + self.coordinator.data.get("gpu_usages", {}) + .get(self._gpu_name, {}) + .get("gpu") + ) + + if data is None or not isinstance(data, str): + return None + + try: + return float(data.replace("%", "").strip()) + except ValueError: + pass + + return None + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of the sensor.""" + return "%" + + @property + def icon(self) -> str: + """Return the icon of the sensor.""" + return ICON_SPEEDOMETER + + class CameraFpsSensor(FrigateEntity, CoordinatorEntity): # type: ignore[misc] """Frigate Camera Fps class.""" @@ -451,3 +534,77 @@ def unit_of_measurement(self) -> Any: def icon(self) -> str: """Return the icon of the sensor.""" return ICON_CORAL + + +class CameraProcessCpuSensor(FrigateEntity, CoordinatorEntity): # type: ignore[misc] + """Cpu usage for camera processes class.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + coordinator: FrigateDataUpdateCoordinator, + config_entry: ConfigEntry, + cam_name: str, + process_type: str, + ) -> None: + """Construct a CoralTempSensor.""" + self._cam_name = cam_name + self._process_type = process_type + self._attr_name = f"{self._process_type} cpu usage" + FrigateEntity.__init__(self, config_entry) + CoordinatorEntity.__init__(self, coordinator) + self._attr_entity_registry_enabled_default = False + + @property + def unique_id(self) -> str: + """Return a unique ID to use for this entity.""" + return get_frigate_entity_unique_id( + self._config_entry.entry_id, + f"{self._process_type}_cpu_usage", + self._cam_name, + ) + + @property + def device_info(self) -> DeviceInfo: + """Get device information.""" + return { + "identifiers": { + get_frigate_device_identifier(self._config_entry, self._cam_name) + }, + "via_device": get_frigate_device_identifier(self._config_entry), + "name": get_friendly_name(self._cam_name), + "model": self._get_model(), + "configuration_url": f"{self._config_entry.data.get(CONF_URL)}/cameras/{self._cam_name}", + "manufacturer": NAME, + } + + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + if self.coordinator.data: + pid_key = ( + "pid" if self._process_type == "detect" else f"{self._process_type}_pid" + ) + pid = str(self.coordinator.data.get(self._cam_name, {}).get(pid_key, "-1")) + data = ( + self.coordinator.data.get("cpu_usages", {}) + .get(pid, {}) + .get("cpu", None) + ) + + try: + return float(data) + except (TypeError, ValueError): + pass + return None + + @property + def unit_of_measurement(self) -> Any: + """Return the unit of measurement of the sensor.""" + return PERCENTAGE + + @property + def icon(self) -> str: + """Return the icon of the sensor.""" + return ICON_CORAL diff --git a/custom_components/frigate/translations/en.json b/custom_components/frigate/translations/en.json index 6df23335..07867085 100644 --- a/custom_components/frigate/translations/en.json +++ b/custom_components/frigate/translations/en.json @@ -21,6 +21,7 @@ "init": { "data": { "rtmp_url_template": "RTMP URL template (see documentation)", + "rtsp_url_template": "RTSP URL template (see documentation)", "media_browser_enable": "Enable the media browser", "notification_proxy_enable": "Enable the unauthenticated notification event proxy", "notification_proxy_expire_after_seconds": "Disallow unauthenticated notification access after seconds (0=never)" diff --git a/custom_components/frigate/translations/pt-BR.json b/custom_components/frigate/translations/pt-BR.json index 0adf068e..92cb58a6 100644 --- a/custom_components/frigate/translations/pt-BR.json +++ b/custom_components/frigate/translations/pt-BR.json @@ -21,6 +21,7 @@ "init": { "data": { "rtmp_url_template": "Modelo de URL RTMP (consulte a documentação)", + "rtsp_url_template": "Modelo de URL RTSP (consulte a documentação)", "notification_proxy_enable": "Habilitar o proxy de evento de notificação não autenticado" } } diff --git a/custom_components/frigate/translations/pt_br.json b/custom_components/frigate/translations/pt_br.json index 0adf068e..92cb58a6 100644 --- a/custom_components/frigate/translations/pt_br.json +++ b/custom_components/frigate/translations/pt_br.json @@ -21,6 +21,7 @@ "init": { "data": { "rtmp_url_template": "Modelo de URL RTMP (consulte a documentação)", + "rtsp_url_template": "Modelo de URL RTSP (consulte a documentação)", "notification_proxy_enable": "Habilitar o proxy de evento de notificação não autenticado" } } diff --git a/custom_components/frigate/update.py b/custom_components/frigate/update.py index 5f9bb99e..2f9d43d1 100644 --- a/custom_components/frigate/update.py +++ b/custom_components/frigate/update.py @@ -85,7 +85,7 @@ def latest_version(self) -> str | None: version = self.coordinator.data.get("service", {}).get("latest_version") - if not version or version == "unknown": + if not version or version == "unknown" or version == "disabled": return None return str(version) diff --git a/custom_components/frigate/views.py b/custom_components/frigate/views.py index 0dfdcc07..2a4d4c9e 100644 --- a/custom_components/frigate/views.py +++ b/custom_components/frigate/views.py @@ -32,6 +32,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -96,6 +97,20 @@ def get_frigate_instance_id_for_config_entry( return get_frigate_instance_id(config) if config else None +def async_setup(hass: HomeAssistant) -> None: + """Set up the views.""" + session = async_get_clientsession(hass) + hass.http.register_view(JSMPEGProxyView(session)) + hass.http.register_view(MSEProxyView(session)) + hass.http.register_view(WebRTCProxyView(session)) + hass.http.register_view(NotificationsProxyView(session)) + hass.http.register_view(SnapshotsProxyView(session)) + hass.http.register_view(RecordingProxyView(session)) + hass.http.register_view(ThumbnailsProxyView(session)) + hass.http.register_view(VodProxyView(session)) + hass.http.register_view(VodSegmentProxyView(session)) + + # These proxies are inspired by: # - https://github.com/home-assistant/supervisor/blob/main/supervisor/api/ingress.py @@ -211,6 +226,24 @@ def _create_path(self, **kwargs: Any) -> str | None: return f"api/events/{kwargs['eventid']}/snapshot.jpg" +class RecordingProxyView(ProxyView): + """A proxy for recordings.""" + + url = "/api/frigate/{frigate_instance_id:.+}/recording/{camera:.+}/start/{start:[.0-9]+}/end/{end:[.0-9]*}" + extra_urls = [ + "/api/frigate/recording/{camera:.+}/start/{start:[.0-9]+}/end/{end:[.0-9]*}" + ] + + name = "api:frigate:recording" + + def _create_path(self, **kwargs: Any) -> str | None: + """Create path.""" + return ( + f"api/{kwargs['camera']}/start/{kwargs['start']}" + + f"/end/{kwargs['end']}/clip.mp4" + ) + + class ThumbnailsProxyView(ProxyView): """A proxy for snapshots.""" @@ -430,8 +463,8 @@ async def _handle_request( ) as ws_to_frigate: await asyncio.wait( [ - self._proxy_msgs(ws_to_frigate, ws_to_user), - self._proxy_msgs(ws_to_user, ws_to_frigate), + asyncio.create_task(self._proxy_msgs(ws_to_frigate, ws_to_user)), + asyncio.create_task(self._proxy_msgs(ws_to_user, ws_to_frigate)), ], return_when=asyncio.tasks.FIRST_COMPLETED, ) @@ -448,7 +481,33 @@ class JSMPEGProxyView(WebsocketProxyView): def _create_path(self, **kwargs: Any) -> str | None: """Create path.""" - return f"live/{kwargs['path']}" + return f"live/jsmpeg/{kwargs['path']}" + + +class MSEProxyView(WebsocketProxyView): + """A proxy for MSE websocket.""" + + url = "/api/frigate/{frigate_instance_id:.+}/mse/{path:.+}" + extra_urls = ["/api/frigate/mse/{path:.+}"] + + name = "api:frigate:mse" + + def _create_path(self, **kwargs: Any) -> str | None: + """Create path.""" + return f"live/mse/{kwargs['path']}" + + +class WebRTCProxyView(WebsocketProxyView): + """A proxy for WebRTC websocket.""" + + url = "/api/frigate/{frigate_instance_id:.+}/webrtc/{path:.+}" + extra_urls = ["/api/frigate/webrtc/{path:.+}"] + + name = "api:frigate:webrtc" + + def _create_path(self, **kwargs: Any) -> str | None: + """Create path.""" + return f"live/webrtc/{kwargs['path']}" def _init_header(request: web.Request) -> CIMultiDict | dict[str, str]: diff --git a/custom_components/frigate/ws_api.py b/custom_components/frigate/ws_api.py index a54160ac..f6efe333 100644 --- a/custom_components/frigate/ws_api.py +++ b/custom_components/frigate/ws_api.py @@ -114,6 +114,7 @@ async def ws_get_recordings( vol.Required("type"): "frigate/recordings/summary", vol.Required("instance_id"): str, vol.Required("camera"): str, + vol.Optional("timezone"): str, } ) # type: ignore[misc] @websocket_api.async_response # type: ignore[misc] @@ -129,7 +130,9 @@ async def ws_get_recordings_summary( try: connection.send_result( msg["id"], - await client.async_get_recordings_summary(msg["camera"], decode_json=False), + await client.async_get_recordings_summary( + msg["camera"], msg.get("timezone", "utc"), decode_json=False + ), ) except FrigateApiClientError: connection.send_error( @@ -144,14 +147,17 @@ async def ws_get_recordings_summary( { vol.Required("type"): "frigate/events/get", vol.Required("instance_id"): str, - vol.Optional("camera"): str, - vol.Optional("label"): str, - vol.Optional("zone"): str, + vol.Optional("cameras"): [str], + vol.Optional("labels"): [str], + vol.Optional("sub_labels"): [str], + vol.Optional("zones"): [str], vol.Optional("after"): int, vol.Optional("before"): int, vol.Optional("limit"): int, vol.Optional("has_clip"): bool, vol.Optional("has_snapshot"): bool, + vol.Optional("has_snapshot"): bool, + vol.Optional("favorites"): bool, } ) # type: ignore[misc] @websocket_api.async_response # type: ignore[misc] @@ -169,14 +175,16 @@ async def ws_get_events( connection.send_result( msg["id"], await client.async_get_events( - msg.get("camera"), - msg.get("label"), - msg.get("zone"), + msg.get("cameras"), + msg.get("labels"), + msg.get("sub_labels"), + msg.get("zones"), msg.get("after"), msg.get("before"), msg.get("limit"), msg.get("has_clip"), msg.get("has_snapshot"), + msg.get("favorites"), decode_json=False, ), ) @@ -184,8 +192,8 @@ async def ws_get_events( connection.send_error( msg["id"], "frigate_error", - f"API error whilst retrieving events for camera " - f"{msg['camera']} for Frigate instance {msg['instance_id']}", + f"API error whilst retrieving events for cameras " + f"{msg['cameras']} for Frigate instance {msg['instance_id']}", ) @@ -195,6 +203,7 @@ async def ws_get_events( vol.Required("instance_id"): str, vol.Optional("has_clip"): bool, vol.Optional("has_snapshot"): bool, + vol.Optional("timezone"): str, } ) # type: ignore[misc] @websocket_api.async_response # type: ignore[misc] @@ -214,6 +223,7 @@ async def ws_get_events_summary( await client.async_get_event_summary( msg.get("has_clip"), msg.get("has_snapshot"), + msg.get("timezone", "utc"), decode_json=False, ), ) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index f6433b25..d8585dda 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -69,6 +69,10 @@ HVAC_MODE_HEAT: "Manual", HVAC_MODE_AUTO: "Program", }, + "m/p": { + HVAC_MODE_HEAT: "m", + HVAC_MODE_AUTO: "p", + }, "True/False": { HVAC_MODE_HEAT: True, }, diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index aa8992fd..cd503c2a 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -1,5 +1,6 @@ """Code shared between all platforms.""" import asyncio +import json.decoder import logging import time from datetime import timedelta @@ -25,18 +26,19 @@ 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, - CONF_DEFAULT_VALUE, - ATTR_STATE, - CONF_RESTORE_ON_RECONNECT, - CONF_RESET_DPIDS, - CONF_PASSIVE_ENTITY, ) _LOGGER = logging.getLogger(__name__) @@ -175,12 +177,13 @@ def connected(self): 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.debug("Connecting to %s", self._dev_config_entry[CONF_HOST]) + self.info("Trying to connect to %s...", self._dev_config_entry[CONF_HOST]) try: self._interface = await pytuya.connect( @@ -188,27 +191,30 @@ async def _make_connection(self): 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: # pylint: disable=broad-except - self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed") + 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: - self.debug("Retrieving initial state") - status = await self._interface.status() - if status is None: - raise Exception("Failed to retrieve status") + 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) + self._interface.start_heartbeat() + self.status_updated(status) - except Exception as ex: # pylint: disable=broad-except - try: + except Exception as ex: if (self._default_reset_dpids is not None) and ( len(self._default_reset_dpids) > 0 ): @@ -226,26 +232,19 @@ async def _make_connection(self): 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 as e: # pylint: disable=broad-except - self.exception( - f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s", - type(e), - ) - if self._interface is not None: - await self._interface.close() - self._interface = None - - except Exception as e: # pylint: disable=broad-except - self.exception( - f"Connect to {self._dev_config_entry[CONF_HOST]} failed" - ) - if "json.decode" in str(type(e)): - await self.update_local_key() + 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: + 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 @@ -268,14 +267,16 @@ def _new_entity_handler(entity_id): if ( CONF_SCAN_INTERVAL in self._dev_config_entry - and self._dev_config_entry[CONF_SCAN_INTERVAL] > 0 + and int(self._dev_config_entry[CONF_SCAN_INTERVAL]) > 0 ): self._unsub_interval = async_track_time_interval( self._hass, self._async_refresh, - timedelta(seconds=self._dev_config_entry[CONF_SCAN_INTERVAL]), + 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): @@ -308,7 +309,7 @@ async def close(self): await self._interface.close() if self._disconnect_task is not None: self._disconnect_task() - self.debug( + self.info( "Closed connection with device %s.", self._dev_config_entry[CONF_FRIENDLY_NAME], ) @@ -356,7 +357,11 @@ def disconnected(self): self._unsub_interval() self._unsub_interval = None self._interface = None - self.debug("Disconnected - waiting for discovery broadcast") + + 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): diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 1eeb3b5d..5c87e254 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -33,7 +33,9 @@ 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, @@ -41,11 +43,11 @@ CONF_RESET_DPIDS, CONF_SETUP_CLOUD, CONF_USER_ID, + CONF_ENABLE_ADD_ENTITIES, DATA_CLOUD, DATA_DISCOVERY, DOMAIN, PLATFORMS, - CONF_MANUAL_DPS, ) from .discovery import discover @@ -82,26 +84,17 @@ } ) -CONFIGURE_DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_FRIENDLY_NAME): str, - vol.Required(CONF_LOCAL_KEY): str, - vol.Required(CONF_HOST): str, - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), - vol.Optional(CONF_SCAN_INTERVAL): int, - vol.Optional(CONF_MANUAL_DPS): str, - vol.Optional(CONF_RESET_DPIDS): str, - } -) 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_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + 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, @@ -141,16 +134,20 @@ def options_schema(entities): ] return vol.Schema( { - vol.Required(CONF_FRIENDLY_NAME): str, - vol.Required(CONF_HOST): str, - vol.Required(CONF_LOCAL_KEY): str, - vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + 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): str, - vol.Optional(CONF_RESET_DPIDS): str, + 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, } ) @@ -247,6 +244,7 @@ async def validate_input(hass: core.HomeAssistant, data): 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(",") @@ -260,20 +258,21 @@ async def validate_input(hass: core.HomeAssistant, data): ) try: detected_dps = await interface.detect_available_dps() - except Exception: # pylint: disable=broad-except + except Exception as ex: try: - _LOGGER.debug("Initial state update failed, trying reset command") + _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: # pylint: disable=broad-except - _LOGGER.debug("No DPS able to be detected") + 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 @@ -497,8 +496,8 @@ async def async_step_add_device(self, user_input=None): errors["base"] = "address_in_use" else: errors["base"] = "discovery_failed" - except Exception: # pylint: disable= broad-except - _LOGGER.exception("discovery failed") + except Exception as ex: + _LOGGER.exception("discovery failed: %s", ex) errors["base"] = "discovery_failed" devices = { @@ -557,6 +556,17 @@ async def async_step_configure_device(self, user_input=None): 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, @@ -564,6 +574,11 @@ async def async_step_configure_device(self, user_input=None): 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]) @@ -585,16 +600,29 @@ async def async_step_configure_device(self, user_input=None): errors["base"] = "invalid_auth" except EmptyDpsList: errors["base"] = "empty_dps" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + 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() - schema = schema_defaults(options_schema(self.entities), **defaults) + 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] = "" @@ -611,7 +639,7 @@ async def async_step_configure_device(self, user_input=None): 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(CONFIGURE_DEVICE_SCHEMA, **defaults) + schema = schema_defaults(DEVICE_SCHEMA, **defaults) placeholders = {"for_device": ""} @@ -633,17 +661,6 @@ async def async_step_pick_entity_type(self, user_input=None): } dev_id = self.device_data.get(CONF_DEVICE_ID) - if dev_id in self.config_entry.data[CONF_DEVICES]: - self.hass.config_entries.async_update_entry( - self.config_entry, data=config - ) - return self.async_abort( - reason="device_success", - description_placeholders={ - "dev_name": config.get(CONF_FRIENDLY_NAME), - "action": "updated", - }, - ) new_data = self.config_entry.data.copy() new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) @@ -726,7 +743,7 @@ async def async_step_configure_entity(self, user_input=None): new_data = self.config_entry.data.copy() entry_id = self.config_entry.entry_id # removing entities from registry (they will be recreated) - ent_reg = await er.async_get_registry(self.hass) + 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) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 8010d18c..630d630a 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -28,12 +28,14 @@ # 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" diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 3b6b86de..b45669ca 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -108,13 +108,13 @@ def is_closing(self): def is_closed(self): """Return if the cover is closed or not.""" if self._config[CONF_POSITIONING_MODE] == COVER_MODE_NONE: - return None + return False if self._current_cover_position == 0: return True if self._current_cover_position == 100: return False - return None + return False async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 584ea84c..32c32899 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -27,12 +27,12 @@ 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, - CONF_FAN_DPS_TYPE, ) _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index 10b1b5a3..28e36fa0 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,14 +1,14 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "4.1.1", - "documentation": "https://github.com/rospogrigio/localtuya/", - "dependencies": [], "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": [], - "config_flow": true, - "iot_class": "local_push" + "version": "5.2.1" } diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 23d7ea9a..917d3d00 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -7,14 +7,13 @@ from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN from .common import LocalTuyaEntity, async_setup_entry - from .const import ( - CONF_MIN_VALUE, - CONF_MAX_VALUE, CONF_DEFAULT_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_PASSIVE_ENTITY, CONF_RESTORE_ON_RECONNECT, CONF_STEPSIZE_VALUE, - CONF_PASSIVE_ENTITY, ) _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index d36cd5e8..bcc8bbed 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -3,11 +3,8 @@ """ Python module to interface with Tuya WiFi smart devices. -Mostly derived from Shenzhen Xenon ESP8266MOD WiFi smart devices -E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U - -Author: clach04 -Maintained by: postlund +Author: clach04, postlund +Maintained by: rospogrigio For more information see https://github.com/clach04/python-tuya @@ -19,7 +16,7 @@ Functions json = status() # returns json payload - set_version(version) # 3.1 [default] or 3.3 + 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 @@ -27,18 +24,21 @@ 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) - * LocalTuya https://github.com/rospogrigio/localtuya-homeassistant by rospogrigio - Updated pytuya to support devices with Device IDs of 22 characters + 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 @@ -46,73 +46,174 @@ import weakref from abc import ABC, abstractmethod from collections import namedtuple -from hashlib import md5 +from hashlib import md5, sha256 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -version_tuple = (9, 0, 0) +version_tuple = (10, 0, 0) version = version_string = __version__ = "%d.%d.%d" % version_tuple -__author__ = "postlund" +__author__ = "rospogrigio" _LOGGER = logging.getLogger(__name__) -TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc") +# 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 -SET = "set" -STATUS = "status" -HEARTBEAT = "heartbeat" -RESET = "reset" -UPDATEDPS = "updatedps" # Request refresh of DPS PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_33 = b"3.3" +PROTOCOL_VERSION_BYTES_34 = b"3.4" -PROTOCOL_33_HEADER = PROTOCOL_VERSION_BYTES_33 + 12 * b"\x00" - -MESSAGE_HEADER_FMT = ">4I" # 4*uint32: prefix, seqno, cmd, length +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 as the status request -# type_0d devices require the 0d command as the status request, and the list of -# dps used set to null in the request payload (see generate_payload method) - +# '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) -PAYLOAD_DICT = { + +# 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": { - STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, - SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, - HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, - RESET: { - "hexByte": 0x12, - "command": { - "gwId": "", - "devId": "", - "uid": "", - "t": "", - "dpId": [18, 19, 20], - }, + 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": { - STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, - SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, - HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, + 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}, }, } @@ -132,13 +233,17 @@ class ContextualLogger: def __init__(self): """Initialize a new ContextualLogger.""" self._logger = None + self._enable_debug = False - def set_logger(self, logger, device_id): + 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): @@ -158,8 +263,9 @@ def exception(self, msg, *args): return self._logger.exception(msg, *args) -def pack_message(msg): +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( @@ -167,28 +273,106 @@ def pack_message(msg): PREFIX_VALUE, msg.seqno, msg.cmd, - len(msg.payload) + struct.calcsize(MESSAGE_END_FMT), + 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(MESSAGE_END_FMT, binascii.crc32(buffer), SUFFIX_VALUE) - + buffer += struct.pack(end_fmt, crc, SUFFIX_VALUE) return buffer -def unpack_message(data): +def unpack_message(data, hmac_key=None, header=None, no_retcode=False, logger=None): """Unpack bytes into a TuyaMessage.""" - header_len = struct.calcsize(MESSAGE_RECV_HEADER_FMT) - end_len = struct.calcsize(MESSAGE_END_FMT) + 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) - _, seqno, cmd, _, retcode = struct.unpack( - MESSAGE_RECV_HEADER_FMT, data[:header_len] + 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] ) - payload = data[header_len:-end_len] - crc, _ = struct.unpack(MESSAGE_END_FMT, data[-end_len:]) - return TuyaMessage(seqno, cmd, retcode, payload, crc) + + 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: @@ -199,19 +383,22 @@ def __init__(self, key): self.block_size = 16 self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend()) - def encrypt(self, raw, use_base64=True): + def encrypt(self, raw, use_base64=True, pad=True): """Encrypt data to be sent to device.""" encryptor = self.cipher.encryptor() - crypted_text = encryptor.update(self._pad(raw)) + encryptor.finalize() + 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): + 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() - return self._unpad(decryptor.update(enc) + decryptor.finalize()).decode() + 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 @@ -225,18 +412,22 @@ def _unpad(data): class MessageDispatcher(ContextualLogger): """Buffer and dispatcher for Tuya messages.""" - # Heartbeats always respond with sequence number 0, so they can't be waited for like - # other messages. This is a hack to allow waiting for heartbeats. + # 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): + 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.set_logger(_LOGGER, dev_id) + self.version = protocol_version + self.local_key = local_key + self.set_logger(_LOGGER, dev_id, enable_debug) def abort(self): """Abort all waiting clients.""" @@ -248,16 +439,19 @@ def abort(self): if isinstance(sem, asyncio.Semaphore): sem.release() - async def wait_for(self, seqno, timeout=5): + 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("Waiting for sequence number %d", 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 @@ -273,51 +467,44 @@ def add_data(self, data): if len(self.buffer) < header_len: break - # Parse header and check if enough data according to length in header - _, seqno, cmd, length, retcode = struct.unpack_from( - MESSAGE_RECV_HEADER_FMT, self.buffer - ) - if len(self.buffer[header_len - 4 :]) < length: - break - - # length includes payload length, retcode, crc and suffix - if (retcode & 0xFFFFFF00) != 0: - payload_start = header_len - 4 - payload_length = length - struct.calcsize(MESSAGE_END_FMT) - else: - payload_start = header_len - payload_length = length - 4 - struct.calcsize(MESSAGE_END_FMT) - payload = self.buffer[payload_start : payload_start + payload_length] - - crc, _ = struct.unpack_from( - MESSAGE_END_FMT, - self.buffer[payload_start + payload_length : payload_start + length], + 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 + length :] - self._dispatch(TuyaMessage(seqno, cmd, retcode, payload, crc)) + 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 %s", msg) + self.debug("Dispatching message CMD %r %s", msg.cmd, msg) if msg.seqno in self.listeners: - self.debug("Dispatching sequence number %d", msg.seqno) + # self.debug("Dispatching sequence number %d", msg.seqno) sem = self.listeners[msg.seqno] - self.listeners[msg.seqno] = msg - sem.release() - elif msg.cmd == 0x09: + 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 == 0x12: + 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 == 0x08: + 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] @@ -327,12 +514,15 @@ def _dispatch(self, msg): self.debug("Got status update") self.listener(msg) else: - self.debug( - "Got message type %d for unknown listener %d: %s", - msg.cmd, - msg.seqno, - msg, - ) + 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): @@ -360,7 +550,9 @@ def disconnected(self): class TuyaProtocol(asyncio.Protocol, ContextualLogger): """Implementation of the Tuya protocol.""" - def __init__(self, dev_id, local_key, protocol_version, on_connected, listener): + def __init__( + self, dev_id, local_key, protocol_version, enable_debug, on_connected, listener + ): """ Initialize a new TuyaInterface. @@ -374,23 +566,59 @@ def __init__(self, dev_id, local_key, protocol_version, on_connected, listener): """ super().__init__() self.loop = asyncio.get_running_loop() - self.set_logger(_LOGGER, dev_id) + self.set_logger(_LOGGER, dev_id, enable_debug) self.id = dev_id self.local_key = local_key.encode("latin1") - self.version = protocol_version + 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 = 0 + self.seqno = 1 self.transport = None self.listener = weakref.ref(listener) - self.dispatcher = self._setup_dispatcher() + 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 _setup_dispatcher(self): + 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"]) @@ -399,7 +627,9 @@ def _status_update(msg): if listener is not None: listener.status_updated(self.dps_cache) - return MessageDispatcher(self.id, _status_update) + return MessageDispatcher( + self.id, _status_update, self.version, self.local_key, enable_debug + ) def connection_made(self, transport): """Did connect to the device.""" @@ -434,11 +664,13 @@ async def 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: @@ -449,6 +681,7 @@ def connection_lost(self, exc): 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: @@ -464,31 +697,86 @@ async def close(self): 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 - 1 + seqno = self.seqno - if command == HEARTBEAT: + if payload.cmd == HEART_BEAT: seqno = MessageDispatcher.HEARTBEAT_SEQNO - elif command == RESET: + elif payload.cmd == UPDATEDPS: seqno = MessageDispatcher.RESET_SEQNO - self.transport.write(payload) - msg = await self.dispatcher.wait_for(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 @@ -504,21 +792,21 @@ async def exchange(self, command, dps=None): async def status(self): """Return device status.""" - status = await self.exchange(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(HEARTBEAT) + 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(RESET, dpIds) + return await self.exchange(UPDATEDPS, dpIds) return True @@ -529,7 +817,7 @@ async def update_dps(self, dps=None): Args: dps([int]): list of dps to update, default=detected&whitelisted """ - if self.version == 3.3: + if self.version in [3.2, 3.3]: # 3.2 behaves like 3.3 with type_0d if dps is None: if not self.dps_cache: await self.detect_available_dps() @@ -539,7 +827,8 @@ async def update_dps(self, dps=None): 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) - self.transport.write(payload) + enc_payload = self._encode_message(payload) + self.transport.write(enc_payload) return True async def set_dp(self, value, dp_index): @@ -550,11 +839,11 @@ async def set_dp(self, value, dp_index): dp_index(int): dps index to set value: new value for the dps index """ - return await self.exchange(SET, {str(dp_index): value}) + 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(SET, dps) + return await self.exchange(CONTROL, dps) async def detect_available_dps(self): """Return which datapoints are supported by the device.""" @@ -591,38 +880,203 @@ def add_dps_to_request(self, dp_indicies): self.dps_to_request.update({str(index): None for index in dp_indicies}) def _decode_payload(self, payload): - if not payload: - payload = "{}" - elif payload.startswith(b"{"): - pass - elif payload.startswith(PROTOCOL_VERSION_BYTES_31): - payload = payload[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header - # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 - # hexdigest of payload - payload = self.cipher.decrypt(payload[16:]) - elif self.version == 3.3: - if self.dev_type != "type_0a" or payload.startswith( - PROTOCOL_VERSION_BYTES_33 - ): - payload = payload[len(PROTOCOL_33_HEADER) :] - payload = self.cipher.decrypt(payload, False) + 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( - "switching to dev_type %s", + "'data unvalid' error detected: switching to dev_type %r", self.dev_type, ) return None - else: - raise Exception(f"Unexpected payload={payload}") + 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("Decrypted payload: %s", payload) - return json.loads(payload) + 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 + ) - def _generate_payload(self, command, data=None): + 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. @@ -631,58 +1085,81 @@ def _generate_payload(self, command, data=None): 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 """ - cmd_data = PAYLOAD_DICT[self.dev_type][command] - json_data = cmd_data["command"] - command_hb = cmd_data["hexByte"] + 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: - json_data["gwId"] = self.id + if gwId is not None: + json_data["gwId"] = gwId + else: + json_data["gwId"] = self.id if "devId" in json_data: - json_data["devId"] = self.id + if devId is not None: + json_data["devId"] = devId + else: + json_data["devId"] = self.id if "uid" in json_data: - json_data["uid"] = self.id # still use id, no separate uid + if uid is not None: + json_data["uid"] = uid + else: + json_data["uid"] = self.id if "t" in json_data: - json_data["t"] = str(int(time.time())) + 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 command_hb == 0x0D: + elif self.dev_type == "type_0d" and command == DP_QUERY: json_data["dps"] = self.dps_to_request - payload = json.dumps(json_data).replace(" ", "").encode("utf-8") - self.debug("Send payload: %s", payload) - - if self.version == 3.3: - payload = self.cipher.encrypt(payload, False) - if command_hb not in [0x0A, 0x12]: - # add the 3.3 header - payload = PROTOCOL_33_HEADER + payload - elif command == SET: - payload = self.cipher.encrypt(payload) - to_hash = ( - b"data=" - + payload - + b"||lpv=" - + PROTOCOL_VERSION_BYTES_31 - + b"||" - + self.local_key - ) - hasher = md5() - hasher.update(to_hash) - hexdigest = hasher.hexdigest() - payload = ( - PROTOCOL_VERSION_BYTES_31 - + hexdigest[8:][:16].encode("latin1") - + payload - ) + 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) - msg = TuyaMessage(self.seqno, command_hb, 0, payload, 0) - self.seqno += 1 - return pack_message(msg) + return MessagePayload(command_override, payload) def __repr__(self): """Return internal string representation of object.""" @@ -694,6 +1171,7 @@ async def connect( device_id, local_key, protocol_version, + enable_debug, listener=None, port=6668, timeout=5, @@ -706,6 +1184,7 @@ async def connect( device_id, local_key, protocol_version, + enable_debug, on_connected, listener or EmptyListener(), ), diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index f643e081..c9b1d1c6 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -4,19 +4,15 @@ import voluptuous as vol from homeassistant.components.select import DOMAIN, SelectEntity -from homeassistant.const import ( - CONF_DEVICE_CLASS, - STATE_UNKNOWN, -) +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_DEFAULT_VALUE, - CONF_RESTORE_ON_RECONNECT, CONF_PASSIVE_ENTITY, + CONF_RESTORE_ON_RECONNECT, ) diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index bc664bf5..3776836e 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -9,14 +9,14 @@ from .const import ( ATTR_CURRENT, ATTR_CURRENT_CONSUMPTION, - ATTR_VOLTAGE, ATTR_STATE, + ATTR_VOLTAGE, CONF_CURRENT, CONF_CURRENT_CONSUMPTION, - CONF_VOLTAGE, CONF_DEFAULT_VALUE, - CONF_RESTORE_ON_RECONNECT, CONF_PASSIVE_ENTITY, + CONF_RESTORE_ON_RECONNECT, + CONF_VOLTAGE, ) _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 4b3ddb0a..b9beee47 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -33,7 +33,8 @@ "options": { "abort": { "already_configured": "Device has already been configured.", - "device_success": "Device {dev_name} successfully {action}." + "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}", @@ -95,8 +96,10 @@ "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)" } diff --git a/custom_components/localtuya/translations/it.json b/custom_components/localtuya/translations/it.json index faf4afa0..9b05309e 100644 --- a/custom_components/localtuya/translations/it.json +++ b/custom_components/localtuya/translations/it.json @@ -33,7 +33,8 @@ "options": { "abort": { "already_configured": "Il dispositivo è già stato configurato.", - "device_success": "Dispositivo {dev_name} {action} con successo." + "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}", @@ -95,6 +96,7 @@ "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)" } diff --git a/custom_components/localtuya/translations/pt-BR.json b/custom_components/localtuya/translations/pt-BR.json index a2feed45..ca5629c1 100644 --- a/custom_components/localtuya/translations/pt-BR.json +++ b/custom_components/localtuya/translations/pt-BR.json @@ -33,7 +33,8 @@ "options": { "abort": { "already_configured": "O dispositivo já foi configurado.", - "device_success": "Dispositivo {dev_name} {action} com sucesso." + "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}", @@ -95,6 +96,7 @@ "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)" } diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py new file mode 100644 index 00000000..49f6d9f2 --- /dev/null +++ b/custom_components/octopus_energy/__init__.py @@ -0,0 +1,156 @@ +import logging +import asyncio +from datetime import timedelta + +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr + +from .coordinators.account import async_setup_account_info_coordinator +from .coordinators.intelligent_dispatches import async_setup_intelligent_dispatches_coordinator +from .coordinators.intelligent_settings import async_setup_intelligent_settings_coordinator +from .coordinators.electricity_rates import async_setup_electricity_rates_coordinator +from .coordinators.saving_sessions import async_setup_saving_sessions_coordinators + +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + CONFIG_MAIN_ELECTRICITY_PRICE_CAP, + CONFIG_MAIN_GAS_PRICE_CAP, + + CONFIG_TARGET_NAME, + + DATA_CLIENT, + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_ACCOUNT_ID, + DATA_ACCOUNT +) + +from .api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +async def async_setup_entry(hass, entry): + """This is called from the config flow.""" + hass.data.setdefault(DOMAIN, {}) + + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + if CONFIG_MAIN_API_KEY in config: + await async_setup_dependencies(hass, config) + + # Forward our entry to setup our default sensors + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "text") + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "number") + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "switch") + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "time") + ) + elif CONFIG_TARGET_NAME in config: + if DOMAIN not in hass.data or DATA_ELECTRICITY_RATES_COORDINATOR not in hass.data[DOMAIN] or DATA_ACCOUNT not in hass.data[DOMAIN]: + raise ConfigEntryNotReady + + # Forward our entry to setup our target rate sensors + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") + ) + + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + + return True + +async def async_setup_dependencies(hass, config): + """Setup the coordinator and api client which will be shared by various entities""" + + electricity_price_cap = None + if CONFIG_MAIN_ELECTRICITY_PRICE_CAP in config: + electricity_price_cap = config[CONFIG_MAIN_ELECTRICITY_PRICE_CAP] + + gas_price_cap = None + if CONFIG_MAIN_GAS_PRICE_CAP in config: + gas_price_cap = config[CONFIG_MAIN_GAS_PRICE_CAP] + + _LOGGER.info(f'electricity_price_cap: {electricity_price_cap}') + _LOGGER.info(f'gas_price_cap: {gas_price_cap}') + + client = OctopusEnergyApiClient(config[CONFIG_MAIN_API_KEY], electricity_price_cap, gas_price_cap) + hass.data[DOMAIN][DATA_CLIENT] = client + hass.data[DOMAIN][DATA_ACCOUNT_ID] = config[CONFIG_MAIN_ACCOUNT_ID] + + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + + # Remove gas meter devices which had incorrect identifier + if account_info is not None and len(account_info["gas_meter_points"]) > 0: + device_registry = dr.async_get(hass) + for point in account_info["gas_meter_points"]: + mprn = point["mprn"] + for meter in point["meters"]: + serial_number = meter["serial_number"] + device = device_registry.async_get_device(identifiers={(DOMAIN, f"electricity_{serial_number}_{mprn}")}) + if device is not None: + device_registry.async_remove_device(device.id) + + await async_setup_account_info_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + await async_setup_intelligent_dispatches_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + await async_setup_intelligent_settings_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + await async_setup_electricity_rates_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + await async_setup_saving_sessions_coordinators(hass) + +async def options_update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + + unload_ok = False + if CONFIG_MAIN_API_KEY in entry.data: + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, "sensor"), + hass.config_entries.async_forward_entry_unload(entry, "binary_sensor"), + hass.config_entries.async_forward_entry_unload(entry, "text"), + hass.config_entries.async_forward_entry_unload(entry, "number"), + hass.config_entries.async_forward_entry_unload(entry, "switch"), + hass.config_entries.async_forward_entry_unload(entry, "time") + ] + ) + ) + elif CONFIG_TARGET_NAME in entry.data: + unload_ok = all( + await asyncio.gather( + *[hass.config_entries.async_forward_entry_unload(entry, "binary_sensor")] + ) + ) + + return unload_ok \ No newline at end of file diff --git a/custom_components/octopus_energy/api_client.py b/custom_components/octopus_energy/api_client.py new file mode 100644 index 00000000..532a65ce --- /dev/null +++ b/custom_components/octopus_energy/api_client.py @@ -0,0 +1,967 @@ +import logging +import json +import aiohttp +from datetime import (datetime, timedelta, time) + +from homeassistant.util.dt import (as_utc, now, as_local, parse_datetime) + +from .utils import ( + get_tariff_parts, +) + +_LOGGER = logging.getLogger(__name__) + +api_token_query = '''mutation {{ + obtainKrakenToken(input: {{ APIKey: "{api_key}" }}) {{ + token + }} +}}''' + +account_query = '''query {{ + account(accountNumber: "{account_id}") {{ + electricityAgreements(active: true) {{ + meterPoint {{ + mpan + meters(includeInactive: false) {{ + makeAndType + serialNumber + makeAndType + meterType + smartExportElectricityMeter {{ + deviceId + manufacturer + model + firmwareVersion + }} + smartImportElectricityMeter {{ + deviceId + manufacturer + model + firmwareVersion + }} + }} + agreements {{ + validFrom + validTo + tariff {{ + ...on StandardTariff {{ + tariffCode + productCode + }} + ...on DayNightTariff {{ + tariffCode + productCode + }} + ...on ThreeRateTariff {{ + tariffCode + productCode + }} + ...on HalfHourlyTariff {{ + tariffCode + productCode + }} + ...on PrepayTariff {{ + tariffCode + productCode + }} + }} + }} + }} + }} + gasAgreements(active: true) {{ + meterPoint {{ + mprn + meters(includeInactive: false) {{ + serialNumber + consumptionUnits + modelName + mechanism + smartGasMeter {{ + deviceId + manufacturer + model + firmwareVersion + }} + }} + agreements {{ + validFrom + validTo + tariff {{ + tariffCode + productCode + }} + }} + }} + }} + }} +}}''' + +saving_session_query = '''query {{ + savingSessions {{ + account(accountNumber: "{account_id}") {{ + hasJoinedCampaign + joinedEvents {{ + eventId + startAt + endAt + }} + }} + }} + octoPoints {{ + account(accountNumber: "{account_id}") {{ + currentPointsInWallet + }} + }} +}}''' + +live_consumption_query = '''query {{ + smartMeterTelemetry( + deviceId: "{device_id}" + grouping: HALF_HOURLY + start: "{period_from}" + end: "{period_to}" + ) {{ + readAt + consumptionDelta + demand + }} +}}''' + +intelligent_dispatches_query = '''query {{ + plannedDispatches(accountNumber: "{account_id}") {{ + startDt + endDt + delta + meta {{ + source + }} + }} + completedDispatches(accountNumber: "{account_id}") {{ + startDt + endDt + delta + meta {{ + source + }} + }} +}}''' + +intelligent_device_query = '''query {{ + registeredKrakenflexDevice(accountNumber: "{account_id}") {{ + krakenflexDeviceId + vehicleMake + vehicleModel + vehicleBatterySizeInKwh + chargePointMake + chargePointModel + chargePointPowerInKw + }} +}}''' + +intelligent_settings_query = '''query vehicleChargingPreferences {{ + vehicleChargingPreferences(accountNumber: "{account_id}") {{ + weekdayTargetTime + weekdayTargetSoc + weekendTargetTime + weekendTargetSoc + }} + registeredKrakenflexDevice(accountNumber: "{account_id}") {{ + suspended + }} +}}''' + +intelligent_settings_mutation = '''mutation vehicleChargingPreferences {{ + setVehicleChargePreferences( + input: {{ + accountNumber: "{account_id}" + weekdayTargetSoc: {weekday_target_percentage} + weekendTargetSoc: {weekend_target_percentage} + weekdayTargetTime: "{weekday_target_time}" + weekendTargetTime: "{weekend_target_time}" + }} + ) {{ + krakenflexDevice {{ + krakenflexDeviceId + }} + }} +}}''' + +intelligent_turn_on_bump_charge_mutation = '''mutation {{ + triggerBoostCharge( + input: {{ + accountNumber: "{account_id}" + }} + ) {{ + krakenflexDevice {{ + krakenflexDeviceId + }} + }} +}}''' + +intelligent_turn_off_bump_charge_mutation = '''mutation {{ + deleteBoostCharge( + input: {{ + accountNumber: "{account_id}" + }} + ) {{ + krakenflexDevice {{ + krakenflexDeviceId + }} + }} +}}''' + +intelligent_turn_on_smart_charge_mutation = '''mutation {{ + resumeControl( + input: {{ + accountNumber: "{account_id}" + }} + ) {{ + krakenflexDevice {{ + krakenflexDeviceId + }} + }} +}}''' + +intelligent_turn_off_smart_charge_mutation = '''mutation {{ + suspendControl( + input: {{ + accountNumber: "{account_id}" + }} + ) {{ + krakenflexDevice {{ + krakenflexDeviceId + }} + }} +}}''' + +def get_valid_from(rate): + return rate["valid_from"] + +def rates_to_thirty_minute_increments(data, period_from: datetime, period_to: datetime, tariff_code: str, price_cap: float = None): + """Process the collection of rates to ensure they're in 30 minute periods""" + starting_period_from = period_from + results = [] + if ("results" in data): + items = data["results"] + items.sort(key=get_valid_from) + + # We need to normalise our data into 30 minute increments so that all of our rates across all tariffs are the same and it's + # easier to calculate our target rate sensors + for item in items: + value_inc_vat = float(item["value_inc_vat"]) + + is_capped = False + if (price_cap is not None and value_inc_vat > price_cap): + value_inc_vat = price_cap + is_capped = True + + if "valid_from" in item and item["valid_from"] is not None: + valid_from = as_utc(parse_datetime(item["valid_from"])) + + # If we're on a fixed rate, then our current time could be in the past so we should go from + # our target period from date otherwise we could be adjusting times quite far in the past + if (valid_from < starting_period_from): + valid_from = starting_period_from + else: + valid_from = starting_period_from + + # Some rates don't have end dates, so we should treat this as our period to target + if "valid_to" in item and item["valid_to"] is not None: + target_date = as_utc(parse_datetime(item["valid_to"])) + + # Cap our target date to our end period + if (target_date > period_to): + target_date = period_to + else: + target_date = period_to + + while valid_from < target_date: + valid_to = valid_from + timedelta(minutes=30) + results.append({ + "value_inc_vat": value_inc_vat, + "valid_from": valid_from, + "valid_to": valid_to, + "tariff_code": tariff_code, + "is_capped": is_capped + }) + + valid_from = valid_to + starting_period_from = valid_to + + return results + +class ServerError(Exception): ... + +class RequestError(Exception): ... + +class OctopusEnergyApiClient: + + def __init__(self, api_key, electricity_price_cap = None, gas_price_cap = None): + if (api_key is None): + raise Exception('API KEY is not set') + + self._api_key = api_key + self._base_url = 'https://api.octopus.energy' + + self._graphql_token = None + self._graphql_expiration = None + + self._product_tracker_cache = dict() + + self._electricity_price_cap = electricity_price_cap + self._gas_price_cap = gas_price_cap + + async def async_refresh_token(self): + """Get the user's refresh token""" + if (self._graphql_expiration is not None and (self._graphql_expiration - timedelta(minutes=5)) > now()): + return + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": api_token_query.format(api_key=self._api_key) } + async with client.post(url, json=payload) as token_response: + token_response_body = await self.__async_read_response__(token_response, url) + if (token_response_body is not None and + "data" in token_response_body and + "obtainKrakenToken" in token_response_body["data"] and + token_response_body["data"]["obtainKrakenToken"] is not None and + "token" in token_response_body["data"]["obtainKrakenToken"]): + + self._graphql_token = token_response_body["data"]["obtainKrakenToken"]["token"] + self._graphql_expiration = now() + timedelta(hours=1) + else: + _LOGGER.error("Failed to retrieve auth token") + + async def async_get_account(self, account_id): + """Get the user's account""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + # Get account response + payload = { "query": account_query.format(account_id=account_id) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as account_response: + account_response_body = await self.__async_read_response__(account_response, url) + + _LOGGER.debug(f'account: {account_response_body}') + + if (account_response_body is not None and + "data" in account_response_body and + "account" in account_response_body["data"] and + account_response_body["data"]["account"] is not None): + return { + "electricity_meter_points": list(map(lambda mp: { + "mpan": mp["meterPoint"]["mpan"], + "meters": list(map(lambda m: { + "serial_number": m["serialNumber"], + "is_export": m["smartExportElectricityMeter"] is not None, + "is_smart_meter": f'{m["meterType"]}'.startswith("S1") or f'{m["meterType"]}'.startswith("S2"), + "device_id": m["smartImportElectricityMeter"]["deviceId"] if m["smartImportElectricityMeter"] is not None else None, + "manufacturer": m["smartImportElectricityMeter"]["manufacturer"] + if m["smartImportElectricityMeter"] is not None + else m["smartExportElectricityMeter"]["manufacturer"] + if m["smartExportElectricityMeter"] is not None + else m["makeAndType"], + "model": m["smartImportElectricityMeter"]["model"] + if m["smartImportElectricityMeter"] is not None + else m["smartExportElectricityMeter"]["model"] + if m["smartExportElectricityMeter"] is not None + else None, + "firmware": m["smartImportElectricityMeter"]["firmwareVersion"] + if m["smartImportElectricityMeter"] is not None + else m["smartExportElectricityMeter"]["firmwareVersion"] + if m["smartExportElectricityMeter"] is not None + else None + }, + mp["meterPoint"]["meters"] + if "meterPoint" in mp and "meters" in mp["meterPoint"] and mp["meterPoint"]["meters"] is not None + else [] + )), + "agreements": list(map(lambda a: { + "valid_from": a["validFrom"], + "valid_to": a["validTo"], + "tariff_code": a["tariff"]["tariffCode"] if "tariff" in a and "tariffCode" in a["tariff"] else None, + "product_code": a["tariff"]["productCode"] if "tariff" in a and "productCode" in a["tariff"] else None, + }, + mp["meterPoint"]["agreements"] + if "meterPoint" in mp and "agreements" in mp["meterPoint"] and mp["meterPoint"]["agreements"] is not None + else [] + )) + }, + account_response_body["data"]["account"]["electricityAgreements"] + if "electricityAgreements" in account_response_body["data"]["account"] and account_response_body["data"]["account"]["electricityAgreements"] is not None + else [] + )), + "gas_meter_points": list(map(lambda mp: { + "mprn": mp["meterPoint"]["mprn"], + "meters": list(map(lambda m: { + "serial_number": m["serialNumber"], + "consumption_units": m["consumptionUnits"], + "is_smart_meter": m["mechanism"] == "S1" or m["mechanism"] == "S2", + "device_id": m["smartGasMeter"]["deviceId"] if m["smartGasMeter"] is not None else None, + "manufacturer": m["smartGasMeter"]["manufacturer"] + if m["smartGasMeter"] is not None + else m["modelName"], + "model": m["smartGasMeter"]["model"] + if m["smartGasMeter"] is not None + else None, + "firmware": m["smartGasMeter"]["firmwareVersion"] + if m["smartGasMeter"] is not None + else None + }, + mp["meterPoint"]["meters"] + if "meterPoint" in mp and "meters" in mp["meterPoint"] and mp["meterPoint"]["meters"] is not None + else [] + )), + "agreements": list(map(lambda a: { + "valid_from": a["validFrom"], + "valid_to": a["validTo"], + "tariff_code": a["tariff"]["tariffCode"] if "tariff" in a and "tariffCode" in a["tariff"] else None, + "product_code": a["tariff"]["productCode"] if "tariff" in a and "productCode" in a["tariff"] else None, + }, + mp["meterPoint"]["agreements"] + if "meterPoint" in mp and "agreements" in mp["meterPoint"] and mp["meterPoint"]["agreements"] is not None + else [] + )) + }, + account_response_body["data"]["account"]["gasAgreements"] + if "gasAgreements" in account_response_body["data"]["account"] and account_response_body["data"]["account"]["gasAgreements"] is not None + else [] + )), + } + else: + _LOGGER.error("Failed to retrieve account") + + return None + + async def async_get_saving_sessions(self, account_id): + """Get the user's seasons savings""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + # Get account response + payload = { "query": saving_session_query.format(account_id=account_id) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as account_response: + response_body = await self.__async_read_response__(account_response, url) + + if (response_body is not None and "data" in response_body): + return { + "points": int(response_body["data"]["octoPoints"]["account"]["currentPointsInWallet"]), + "events": list(map(lambda ev: { + "start": as_utc(parse_datetime(ev["startAt"])), + "end": as_utc(parse_datetime(ev["endAt"])) + }, response_body["data"]["savingSessions"]["account"]["joinedEvents"])) + } + else: + _LOGGER.error("Failed to retrieve account") + + return None + + async def async_get_smart_meter_consumption(self, device_id: str, period_from: datetime, period_to: datetime): + """Get the user's smart meter consumption""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + + payload = { "query": live_consumption_query.format(device_id=device_id, period_from=period_from.strftime("%Y-%m-%dT%H:%M:%S%z"), period_to=period_to.strftime("%Y-%m-%dT%H:%M:%S%z")) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as live_consumption_response: + response_body = await self.__async_read_response__(live_consumption_response, url) + + if (response_body is not None and "data" in response_body and "smartMeterTelemetry" in response_body["data"] and response_body["data"]["smartMeterTelemetry"] is not None and len(response_body["data"]["smartMeterTelemetry"]) > 0): + return list(map(lambda mp: { + "consumption": float(mp["consumptionDelta"]) / 1000, + "demand": float(mp["demand"]) if "demand" in mp and mp["demand"] is not None else None, + "interval_start": parse_datetime(mp["readAt"]), + "interval_end": parse_datetime(mp["readAt"]) + timedelta(minutes=30) + }, response_body["data"]["smartMeterTelemetry"])) + else: + _LOGGER.debug(f"Failed to retrieve smart meter consumption data - device_id: {device_id}; period_from: {period_from}; period_to: {period_to}") + + return None + + async def async_get_electricity_standard_rates(self, product_code, tariff_code, period_from, period_to): + """Get the current standard rates""" + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}/electricity-tariffs/{tariff_code}/standard-unit-rates?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if data is None: + return None + else: + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._electricity_price_cap) + + return results + + async def async_get_electricity_day_night_rates(self, product_code, tariff_code, is_smart_meter, period_from, period_to): + """Get the current day and night rates""" + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}/electricity-tariffs/{tariff_code}/day-unit-rates?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if data is None: + return None + else: + # Normalise the rates to be in 30 minute increments and remove any rates that fall outside of our day period + day_rates = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._electricity_price_cap) + for rate in day_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == False: + results.append(rate) + + url = f'{self._base_url}/v1/products/{product_code}/electricity-tariffs/{tariff_code}/night-unit-rates?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if data is None: + return None + + # Normalise the rates to be in 30 minute increments and remove any rates that fall outside of our night period + night_rates = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._electricity_price_cap) + for rate in night_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == True: + results.append(rate) + + # Because we retrieve our day and night periods separately over a 2 day period, we need to sort our rates + results.sort(key=get_valid_from) + + return results + + async def async_get_electricity_rates(self, tariff_code, is_smart_meter, period_from, period_to): + """Get the current rates""" + + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts.product_code + + if (tariff_parts.rate.startswith("1")): + return await self.async_get_electricity_standard_rates(product_code, tariff_code, period_from, period_to) + else: + return await self.async_get_electricity_day_night_rates(product_code, tariff_code, is_smart_meter, period_from, period_to) + + async def async_get_electricity_consumption(self, mpan, serial_number, period_from, period_to): + """Get the current electricity consumption""" + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/electricity-meter-points/{mpan}/meters/{serial_number}/consumption?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + + data = await self.__async_read_response__(response, url) + if (data is not None and "results" in data): + data = data["results"] + results = [] + for item in data: + item = self.__process_consumption(item) + + # For some reason, the end point returns slightly more data than we requested, so we need to filter out + # the results + if as_utc(item["interval_start"]) >= period_from and as_utc(item["interval_end"]) <= period_to: + results.append(item) + + results.sort(key=self.__get_interval_end) + return results + + return None + + async def async_get_gas_rates(self, tariff_code, period_from, period_to): + """Get the gas rates""" + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts.product_code + + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}/gas-tariffs/{tariff_code}/standard-unit-rates?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if data is None: + return None + else: + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._gas_price_cap) + + return results + + async def async_get_gas_consumption(self, mprn, serial_number, period_from, period_to): + """Get the current gas rates""" + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/gas-meter-points/{mprn}/meters/{serial_number}/consumption?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if (data is not None and "results" in data): + data = data["results"] + results = [] + for item in data: + item = self.__process_consumption(item) + + # For some reason, the end point returns slightly more data than we requested, so we need to filter out + # the results + if as_utc(item["interval_start"]) >= period_from and as_utc(item["interval_end"]) <= period_to: + results.append(item) + + results.sort(key=self.__get_interval_end) + return results + + return None + + async def async_get_product(self, product_code): + """Get all products""" + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}' + async with client.get(url, auth=auth) as response: + return await self.__async_read_response__(response, url) + + async def async_get_electricity_standing_charge(self, tariff_code, period_from, period_to): + """Get the electricity standing charges""" + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts.product_code + + if self.__is_tracker_tariff__(tariff_code): + return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) + + result = None + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}/electricity-tariffs/{tariff_code}/standing-charges?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if (data is not None and "results" in data and len(data["results"]) > 0): + result = { + "valid_from": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None, + "valid_to": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None, + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + + return result + + async def async_get_gas_standing_charge(self, tariff_code, period_from, period_to): + """Get the gas standing charges""" + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts.product_code + + if self.__is_tracker_tariff__(tariff_code): + return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) + + result = None + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products/{product_code}/gas-tariffs/{tariff_code}/standing-charges?period_from={period_from.strftime("%Y-%m-%dT%H:%M:%SZ")}&period_to={period_to.strftime("%Y-%m-%dT%H:%M:%SZ")}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if (data is not None and "results" in data and len(data["results"]) > 0): + result = { + "valid_from": parse_datetime(data["results"][0]["valid_from"]) if "valid_from" in data["results"][0] and data["results"][0]["valid_from"] is not None else None, + "valid_to": parse_datetime(data["results"][0]["valid_to"]) if "valid_to" in data["results"][0] and data["results"][0]["valid_to"] is not None else None, + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + + return result + + async def async_get_intelligent_dispatches(self, account_id: str): + """Get the user's intelligent dispatches""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + # Get account response + payload = { "query": intelligent_dispatches_query.format(account_id=account_id) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_get_intelligent_dispatches: {response_body}') + + if (response_body is not None and "data" in response_body): + return { + "planned": list(map(lambda ev: { + "start": as_utc(parse_datetime(ev["startDt"])), + "end": as_utc(parse_datetime(ev["endDt"])), + "charge_in_kwh": float(ev["delta"]) if "delta" in ev and ev["delta"] is not None else None, + "source": ev["meta"]["source"] if "meta" in ev and "source" in ev["meta"] else None, + }, response_body["data"]["plannedDispatches"] + if "plannedDispatches" in response_body["data"] and response_body["data"]["plannedDispatches"] is not None + else []) + ), + "completed": list(map(lambda ev: { + "start": as_utc(parse_datetime(ev["startDt"])), + "end": as_utc(parse_datetime(ev["endDt"])), + "charge_in_kwh": float(ev["delta"]) if "delta" in ev and ev["delta"] is not None else None, + "source": ev["meta"]["source"] if "meta" in ev and "source" in ev["meta"] else None, + }, response_body["data"]["completedDispatches"] + if "completedDispatches" in response_body["data"] and response_body["data"]["completedDispatches"] is not None + else []) + ) + } + else: + _LOGGER.error("Failed to retrieve intelligent dispatches") + + return None + + async def async_get_intelligent_settings(self, account_id: str): + """Get the user's intelligent settings""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_settings_query.format(account_id=account_id) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_get_intelligent_settings: {response_body}') + + _LOGGER.debug(f'Intelligent Settings: {response_body}') + if (response_body is not None and "data" in response_body): + + return { + "smart_charge": response_body["data"]["registeredKrakenflexDevice"]["suspended"] == False + if "registeredKrakenflexDevice" in response_body["data"] and "suspended" in response_body["data"]["registeredKrakenflexDevice"] + else None, + "charge_limit_weekday": int(response_body["data"]["vehicleChargingPreferences"]["weekdayTargetSoc"]) + if "vehicleChargingPreferences" in response_body["data"] and "weekdayTargetSoc" in response_body["data"]["vehicleChargingPreferences"] + else None, + "charge_limit_weekend": int(response_body["data"]["vehicleChargingPreferences"]["weekendTargetSoc"]) + if "vehicleChargingPreferences" in response_body["data"] and "weekendTargetSoc" in response_body["data"]["vehicleChargingPreferences"] + else None, + "ready_time_weekday": self.__ready_time_to_time__(response_body["data"]["vehicleChargingPreferences"]["weekdayTargetTime"]) + if "vehicleChargingPreferences" in response_body["data"] and "weekdayTargetTime" in response_body["data"]["vehicleChargingPreferences"] + else None, + "ready_time_weekend": self.__ready_time_to_time__(response_body["data"]["vehicleChargingPreferences"]["weekendTargetTime"]) + if "vehicleChargingPreferences" in response_body["data"] and "weekendTargetTime" in response_body["data"]["vehicleChargingPreferences"] + else None, + } + else: + _LOGGER.error("Failed to retrieve intelligent settings") + + return None + + def __ready_time_to_time__(self, time_str: str) -> time: + if time_str is not None: + parts = time_str.split(':') + if len(parts) != 2: + raise Exception(f"Unexpected number of parts in '{time_str}'") + + return time(int(parts[0]), int(parts[1])) + + return None + + async def async_update_intelligent_car_preferences( + self, account_id: str, + weekday_target_percentage: int, + weekend_target_percentage: int, + weekday_target_time: time, + weekend_target_time: time, + ): + """Update a user's intelligent car preferences""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_settings_mutation.format( + account_id=account_id, + weekday_target_percentage=weekday_target_percentage, + weekend_target_percentage=weekend_target_percentage, + weekday_target_time=weekday_target_time.strftime("%H:%M"), + weekend_target_time=weekend_target_time.strftime("%H:%M") + ) } + + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_update_intelligent_car_preferences: {response_body}') + + async def async_turn_on_intelligent_bump_charge( + self, account_id: str, + ): + """Turn on an intelligent bump charge""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_turn_on_bump_charge_mutation.format( + account_id=account_id, + ) } + + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_turn_on_intelligent_bump_charge: {response_body}') + + async def async_turn_off_intelligent_bump_charge( + self, account_id: str, + ): + """Turn off an intelligent bump charge""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_turn_off_bump_charge_mutation.format( + account_id=account_id, + ) } + + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_turn_off_intelligent_bump_charge: {response_body}') + + async def async_turn_on_intelligent_smart_charge( + self, account_id: str, + ): + """Turn on an intelligent bump charge""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_turn_on_smart_charge_mutation.format( + account_id=account_id, + ) } + + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_turn_on_intelligent_smart_charge: {response_body}') + + async def async_turn_off_intelligent_smart_charge( + self, account_id: str, + ): + """Turn off an intelligent bump charge""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_turn_off_smart_charge_mutation.format( + account_id=account_id, + ) } + + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_turn_off_intelligent_smart_charge: {response_body}') + + async def async_get_intelligent_device(self, account_id: str): + """Get the user's intelligent dispatches""" + await self.async_refresh_token() + + async with aiohttp.ClientSession() as client: + url = f'{self._base_url}/v1/graphql/' + payload = { "query": intelligent_device_query.format(account_id=account_id) } + headers = { "Authorization": f"JWT {self._graphql_token}" } + async with client.post(url, json=payload, headers=headers) as response: + response_body = await self.__async_read_response__(response, url) + _LOGGER.debug(f'async_get_intelligent_device: {response_body}') + + if (response_body is not None and "data" in response_body and + "registeredKrakenflexDevice" in response_body["data"]): + device = response_body["data"]["registeredKrakenflexDevice"] + return { + "krakenflexDeviceId": device["krakenflexDeviceId"], + "vehicleMake": device["vehicleMake"], + "vehicleModel": device["vehicleModel"], + "vehicleBatterySizeInKwh": float(device["vehicleBatterySizeInKwh"]) if "vehicleBatterySizeInKwh" in device and device["vehicleBatterySizeInKwh"] is not None else None, + "chargePointMake": device["chargePointMake"], + "chargePointModel": device["chargePointModel"], + "chargePointPowerInKw": float(device["chargePointPowerInKw"]) if "chargePointPowerInKw" in device and device["chargePointPowerInKw"] is not None else None, + + } + else: + _LOGGER.error("Failed to retrieve intelligent device") + + return None + + def __is_tracker_tariff__(self, tariff_code): + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts.product_code + + if product_code in self._product_tracker_cache: + return self._product_tracker_cache[product_code] + + return False + + def __get_interval_end(self, item): + return item["interval_end"] + + def __is_night_rate(self, rate, is_smart_meter): + # Normally the economy seven night rate is between 12am and 7am UK time + # https://octopus.energy/help-and-faqs/articles/what-is-an-economy-7-meter-and-tariff/ + # However, if a smart meter is being used then the times are between 12:30am and 7:30am UTC time + # https://octopus.energy/help-and-faqs/articles/what-happens-to-my-economy-seven-e7-tariff-when-i-have-a-smart-meter-installed/ + if is_smart_meter: + is_night_rate = self.__is_between_times(rate, "00:30:00", "07:30:00", True) + else: + is_night_rate = self.__is_between_times(rate, "00:00:00", "07:00:00", False) + return is_night_rate + + def __is_between_times(self, rate, target_from_time, target_to_time, use_utc): + """Determines if a current rate is between two times""" + rate_local_valid_from = as_local(rate["valid_from"]) + rate_local_valid_to = as_local(rate["valid_to"]) + + if use_utc: + rate_utc_valid_from = as_utc(rate["valid_from"]) + # We need to convert our times into local time to account for BST to ensure that our rate is valid between the target times. + from_date_time = as_local(parse_datetime(rate_utc_valid_from.strftime(f"%Y-%m-%dT{target_from_time}Z"))) + to_date_time = as_local(parse_datetime(rate_utc_valid_from.strftime(f"%Y-%m-%dT{target_to_time}Z"))) + else: + local_now = now() + # We need to convert our times into local time to account for BST to ensure that our rate is valid between the target times. + from_date_time = as_local(parse_datetime(rate_local_valid_from.strftime(f"%Y-%m-%dT{target_from_time}{local_now.strftime('%z')}"))) + to_date_time = as_local(parse_datetime(rate_local_valid_from.strftime(f"%Y-%m-%dT{target_to_time}{local_now.strftime('%z')}"))) + + _LOGGER.debug('is_valid: %s; from_date_time: %s; to_date_time: %s; rate_local_valid_from: %s; rate_local_valid_to: %s', rate_local_valid_from >= from_date_time and rate_local_valid_from < to_date_time, from_date_time, to_date_time, rate_local_valid_from, rate_local_valid_to) + + return rate_local_valid_from >= from_date_time and rate_local_valid_from < to_date_time + + def __process_consumption(self, item): + return { + "consumption": float(item["consumption"]), + "interval_start": as_utc(parse_datetime(item["interval_start"])), + "interval_end": as_utc(parse_datetime(item["interval_end"])) + } + + async def __async_read_response__(self, response, url): + """Reads the response, logging any json errors""" + + text = await response.text() + + if response.status >= 400: + if response.status >= 500: + msg = f'DO NOT REPORT - Octopus Energy server error ({url}): {response.status}; {text}' + _LOGGER.debug(msg) + raise ServerError(msg) + elif response.status not in [401, 403, 404]: + msg = f'Failed to send request ({url}): {response.status}; {text}' + _LOGGER.debug(msg) + raise RequestError(msg) + return None + + data_as_json = None + try: + data_as_json = json.loads(text) + except: + raise Exception(f'Failed to extract response json: {url}; {text}') + + if ("graphql" in url and "errors" in data_as_json): + msg = f'Errors in request ({url}): {data_as_json["errors"]}' + _LOGGER.debug(msg) + raise RequestError(msg) + + return data_as_json diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py new file mode 100644 index 00000000..290242d8 --- /dev/null +++ b/custom_components/octopus_energy/binary_sensor.py @@ -0,0 +1,125 @@ +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.util.dt import (utcnow) + +from .saving_sessions.saving_sessions import OctopusEnergySavingSessions +from .target_rates.target_rate import OctopusEnergyTargetRate +from .intelligent.dispatching import OctopusEnergyIntelligentDispatching +from .api_client import OctopusEnergyApiClient +from .intelligent import async_mock_intelligent_data, is_intelligent_tariff, mock_intelligent_device +from .utils import get_active_tariff_code + +from .const import ( + DATA_ACCOUNT_ID, + DATA_CLIENT, + DATA_INTELLIGENT_DISPATCHES_COORDINATOR, + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_TARGET_NAME, + CONFIG_TARGET_MPAN, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_saving_session_sensors(hass, entry, async_add_entities) + await async_setup_intelligent_sensors(hass, async_add_entities) + elif CONFIG_TARGET_NAME in entry.data: + await async_setup_target_sensors(hass, entry, async_add_entities) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "update_target_config", + vol.All( + vol.Schema( + { + vol.Required("target_hours"): str, + vol.Optional("target_start_time"): str, + vol.Optional("target_end_time"): str, + vol.Optional("target_offset"): str, + }, + extra=vol.ALLOW_EXTRA, + ), + cv.has_at_least_one_key( + "target_hours", "target_start_time", "target_end_time", "target_offset" + ), + ), + "async_update_config", + ) + + return True + +async def async_setup_saving_session_sensors(hass, entry, async_add_entities): + _LOGGER.debug('Setting up Saving Session entities') + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + saving_session_coordinator = hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] + + await saving_session_coordinator.async_config_entry_first_refresh() + + async_add_entities([OctopusEnergySavingSessions(hass, saving_session_coordinator)], True) + +async def async_setup_intelligent_sensors(hass, async_add_entities): + _LOGGER.debug('Setting up intelligent sensors') + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + has_intelligent_tariff = False + if len(account_info["electricity_meter_points"]) > 0: + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if is_intelligent_tariff(tariff_code): + has_intelligent_tariff = True + break + + should_mock_intelligent_data = await async_mock_intelligent_data(hass) + if has_intelligent_tariff or should_mock_intelligent_data: + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + if should_mock_intelligent_data: + device = mock_intelligent_device() + else: + device = await client.async_get_intelligent_device(account_id) + + async_add_entities([OctopusEnergyIntelligentDispatching(hass, coordinator, device)], True) + +async def async_setup_target_sensors(hass, entry, async_add_entities): + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + mpan = config[CONFIG_TARGET_MPAN] + + is_export = False + for point in account_info["electricity_meter_points"]: + if point["mpan"] == mpan: + for meter in point["meters"]: + is_export = meter["is_export"] + + entities = [OctopusEnergyTargetRate(hass, coordinator, config, is_export)] + async_add_entities(entities, True) diff --git a/custom_components/octopus_energy/binary_sensors/__init__.py b/custom_components/octopus_energy/binary_sensors/__init__.py new file mode 100644 index 00000000..b0a56502 --- /dev/null +++ b/custom_components/octopus_energy/binary_sensors/__init__.py @@ -0,0 +1,236 @@ +from datetime import datetime, timedelta +import math +from homeassistant.util.dt import (as_utc, parse_datetime) +from ..utils import (apply_offset) +import logging + +_LOGGER = logging.getLogger(__name__) + +def __get_applicable_rates(current_date: datetime, target_start_time: str, target_end_time: str, rates, is_rolling_target: bool): + if (target_start_time is not None): + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z")) + else: + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + + if (target_end_time is not None): + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_end_time}:00%z")) + else: + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + timedelta(days=1) + + target_start = as_utc(target_start) + target_end = as_utc(target_end) + + if (target_start >= target_end): + _LOGGER.debug(f'{target_start} is after {target_end}, so setting target end to tomorrow') + if target_start > current_date: + target_start = target_start - timedelta(days=1) + else: + target_end = target_end + timedelta(days=1) + + # If our start date has passed, reset it to current_date to avoid picking a slot in the past + if (is_rolling_target == True and target_start < current_date and current_date < target_end): + _LOGGER.debug(f'Rolling target and {target_start} is in the past. Setting start to {current_date}') + target_start = current_date + + # If our start and end are both in the past, then look to the next day + if (target_start < current_date and target_end < current_date): + target_start = target_start + timedelta(days=1) + target_end = target_end + timedelta(days=1) + + _LOGGER.debug(f'Finding rates between {target_start} and {target_end}') + + # Retrieve the rates that are applicable for our target rate + applicable_rates = [] + if rates != None: + for rate in rates: + if rate["valid_from"] >= target_start and (target_end == None or rate["valid_to"] <= target_end): + applicable_rates.append(rate) + + # Make sure that we have enough rates that meet our target period + date_diff = target_end - target_start + hours = (date_diff.days * 24) + (date_diff.seconds // 3600) + periods = hours * 2 + if len(applicable_rates) < periods: + _LOGGER.debug(f'Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_rates)}') + return None + + return applicable_rates + +def __get_rate(rate): + return rate["value_inc_vat"] + +def __get_valid_to(rate): + return rate["valid_to"] + +def calculate_continuous_times(current_date: datetime, target_start_time: str, target_end_time: str, target_hours: float, rates, is_rolling_target = True, search_for_highest_rate = False): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + + applicable_rates_count = len(applicable_rates) + total_required_rates = math.ceil(target_hours * 2) + + best_continuous_rates = None + best_continuous_rates_total = None + + _LOGGER.debug(f'{applicable_rates_count} applicable rates found') + + # Loop through our rates and try and find the block of time that meets our desired + # hours and has the lowest combined rates + for index, rate in enumerate(applicable_rates): + continuous_rates = [rate] + continuous_rates_total = rate["value_inc_vat"] + + for offset in range(1, total_required_rates): + if (index + offset) < applicable_rates_count: + offset_rate = applicable_rates[(index + offset)] + continuous_rates.append(offset_rate) + continuous_rates_total += offset_rate["value_inc_vat"] + else: + break + + if ((best_continuous_rates == None or (search_for_highest_rate == False and continuous_rates_total < best_continuous_rates_total) or (search_for_highest_rate and continuous_rates_total > best_continuous_rates_total)) and len(continuous_rates) == total_required_rates): + best_continuous_rates = continuous_rates + best_continuous_rates_total = continuous_rates_total + else: + _LOGGER.debug(f'Total rates for current block {continuous_rates_total}. Total rates for best block {best_continuous_rates_total}') + + if best_continuous_rates is not None: + # Make sure our rates are in ascending order before returning + best_continuous_rates.sort(key=__get_valid_to) + return best_continuous_rates + + return [] + +def calculate_intermittent_times(current_date: datetime, target_start_time: str, target_end_time: str, target_hours: float, rates, is_rolling_target = True, search_for_highest_rate = False): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + + total_required_rates = math.ceil(target_hours * 2) + + applicable_rates.sort(key=__get_rate, reverse=search_for_highest_rate) + applicable_rates = applicable_rates[:total_required_rates] + + _LOGGER.debug(f'{len(applicable_rates)} applicable rates found') + + if (len(applicable_rates) < total_required_rates): + return [] + + # Make sure our rates are in ascending order before returning + applicable_rates.sort(key=__get_valid_to) + return applicable_rates + +def get_target_rate_info(current_date: datetime, applicable_rates, offset: str = None): + is_active = False + next_time = None + current_duration_in_hours = 0 + next_duration_in_hours = 0 + total_applicable_rates = len(applicable_rates) + + overall_total_cost = 0 + overall_min_cost = None + overall_max_cost = None + + current_average_cost = None + current_min_cost = None + current_max_cost = None + + next_average_cost = None + next_min_cost = None + next_max_cost = None + + if (total_applicable_rates > 0): + + # Find the applicable rates that when combine become a continuous block. This is more for + # intermittent rates. + applicable_rates.sort(key=__get_valid_to) + applicable_rate_blocks = list() + block_valid_from = applicable_rates[0]["valid_from"] + + total_cost = 0 + min_cost = None + max_cost = None + + for index, rate in enumerate(applicable_rates): + if (index > 0 and applicable_rates[index - 1]["valid_to"] != rate["valid_from"]): + diff = applicable_rates[index - 1]["valid_to"] - block_valid_from + minutes = diff.total_seconds() / 60 + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[index - 1]["valid_to"], + "duration_in_hours": minutes / 60, + "average_cost": total_cost / (minutes / 30), + "min_cost": min_cost, + "max_cost": max_cost + }) + + block_valid_from = rate["valid_from"] + total_cost = 0 + min_cost = None + max_cost = None + + total_cost += rate["value_inc_vat"] + if min_cost is None or min_cost > rate["value_inc_vat"]: + min_cost = rate["value_inc_vat"] + + if max_cost is None or max_cost < rate["value_inc_vat"]: + max_cost = rate["value_inc_vat"] + + overall_total_cost += rate["value_inc_vat"] + if overall_min_cost is None or overall_min_cost > rate["value_inc_vat"]: + overall_min_cost = rate["value_inc_vat"] + + if overall_max_cost is None or overall_max_cost < rate["value_inc_vat"]: + overall_max_cost = rate["value_inc_vat"] + + # Make sure our final block is added + diff = applicable_rates[-1]["valid_to"] - block_valid_from + minutes = diff.total_seconds() / 60 + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[-1]["valid_to"], + "duration_in_hours": minutes / 60, + "average_cost": total_cost / (minutes / 30), + "min_cost": min_cost, + "max_cost": max_cost + }) + + # Find out if we're within an active block, or find the next block + for index, rate in enumerate(applicable_rate_blocks): + if (offset != None): + valid_from = apply_offset(rate["valid_from"], offset) + valid_to = apply_offset(rate["valid_to"], offset) + else: + valid_from = rate["valid_from"] + valid_to = rate["valid_to"] + + if current_date >= valid_from and current_date < valid_to: + current_duration_in_hours = rate["duration_in_hours"] + current_average_cost = rate["average_cost"] + current_min_cost = rate["min_cost"] + current_max_cost = rate["max_cost"] + is_active = True + elif current_date < valid_from: + next_time = valid_from + next_duration_in_hours = rate["duration_in_hours"] + next_average_cost = rate["average_cost"] + next_min_cost = rate["min_cost"] + next_max_cost = rate["max_cost"] + break + + return { + "is_active": is_active, + "overall_average_cost": round(overall_total_cost / total_applicable_rates, 5) if total_applicable_rates > 0 else 0, + "overall_min_cost": overall_min_cost, + "overall_max_cost": overall_max_cost, + "current_duration_in_hours": current_duration_in_hours, + "current_average_cost": current_average_cost, + "current_min_cost": current_min_cost, + "current_max_cost": current_max_cost, + "next_time": next_time, + "next_duration_in_hours": next_duration_in_hours, + "next_average_cost": next_average_cost, + "next_min_cost": next_min_cost, + "next_max_cost": next_max_cost, + } diff --git a/custom_components/octopus_energy/binary_sensors/saving_sessions.py b/custom_components/octopus_energy/binary_sensors/saving_sessions.py new file mode 100644 index 00000000..1f4780da --- /dev/null +++ b/custom_components/octopus_energy/binary_sensors/saving_sessions.py @@ -0,0 +1,100 @@ +import logging + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from ..sensors import ( + current_saving_sessions_event, + get_next_saving_sessions_event +) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergySavingSessions(CoordinatorEntity, BinarySensorEntity, RestoreEntity): + """Sensor for determining if a saving session is active.""" + + def __init__(self, coordinator): + """Init sensor.""" + + super().__init__(coordinator) + + self._state = None + self._events = [] + self._attributes = { + "joined_events": [], + "next_joined_event_start": None + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_saving_sessions" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Saving Session" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:leaf" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """The state of the sensor.""" + saving_session = self.coordinator.data + if (saving_session is not None and "events" in saving_session): + self._events = saving_session["events"] + else: + self._events = [] + + self._attributes = { + "joined_events": self._events, + "next_joined_event_start": None, + "next_joined_event_end": None, + "next_joined_event_duration_in_minutes": None + } + + current_date = now() + current_event = current_saving_sessions_event(current_date, self._events) + if (current_event is not None): + self._state = True + self._attributes["current_joined_event_start"] = current_event["start"] + self._attributes["current_joined_event_end"] = current_event["end"] + self._attributes["current_joined_event_duration_in_minutes"] = current_event["duration_in_minutes"] + else: + self._state = False + + next_event = get_next_saving_sessions_event(current_date, self._events) + if (next_event is not None): + self._attributes["next_joined_event_start"] = next_event["start"] + self._attributes["next_joined_event_end"] = next_event["end"] + self._attributes["next_joined_event_duration_in_minutes"] = next_event["duration_in_minutes"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None: + self._state = state.state + + if (self._state is None): + self._state = False + + _LOGGER.debug(f'Restored state: {self._state}') diff --git a/custom_components/octopus_energy/binary_sensors/target_rate.py b/custom_components/octopus_energy/binary_sensors/target_rate.py new file mode 100644 index 00000000..9eb54c3f --- /dev/null +++ b/custom_components/octopus_energy/binary_sensors/target_rate.py @@ -0,0 +1,233 @@ +import logging +from custom_components.octopus_energy.utils import apply_offset + +import re +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.util.dt import (utcnow, now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) +from ..const import ( + CONFIG_TARGET_OFFSET, + + CONFIG_TARGET_NAME, + CONFIG_TARGET_HOURS, + CONFIG_TARGET_TYPE, + CONFIG_TARGET_START_TIME, + CONFIG_TARGET_END_TIME, + CONFIG_TARGET_MPAN, + CONFIG_TARGET_ROLLING_TARGET, + + REGEX_HOURS, + REGEX_TIME, + REGEX_OFFSET_PARTS, +) + +from . import ( + calculate_continuous_times, + calculate_intermittent_times, + get_target_rate_info +) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyTargetRate(CoordinatorEntity, BinarySensorEntity): + """Sensor for calculating when a target should be turned on or off.""" + + def __init__(self, coordinator, config, is_export): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + + self._config = config + self._is_export = is_export + self._attributes = self._config.copy() + self._is_export = is_export + self._attributes["is_target_export"] = is_export + is_rolling_target = True + if CONFIG_TARGET_ROLLING_TARGET in self._config: + is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET] + self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target + self._target_rates = [] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_target_{self._config[CONFIG_TARGET_NAME]}" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Target {self._config[CONFIG_TARGET_NAME]}" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:camera-timer" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """The state of the sensor.""" + + if CONFIG_TARGET_OFFSET in self._config: + offset = self._config[CONFIG_TARGET_OFFSET] + else: + offset = None + + # Find the current rate. Rates change a maximum of once every 30 minutes. + current_date = utcnow() + if (current_date.minute % 30) == 0 or len(self._target_rates) == 0: + _LOGGER.debug(f'Updating OctopusEnergyTargetRate {self._config[CONFIG_TARGET_NAME]}') + + # If all of our target times have passed, it's time to recalculate the next set + all_rates_in_past = True + for rate in self._target_rates: + if rate["valid_to"] > current_date: + all_rates_in_past = False + break + + if all_rates_in_past: + if self.coordinator.data != None: + all_rates = self.coordinator.data + + # Retrieve our rates. For backwards compatibility, if CONFIG_TARGET_MPAN is not set, then pick the first set + if CONFIG_TARGET_MPAN not in self._config: + _LOGGER.debug(f"'CONFIG_TARGET_MPAN' not set.'{len(all_rates)}' rates available. Retrieving the first rate.") + all_rates = next(iter(all_rates.values())) + else: + _LOGGER.debug(f"Retrieving rates for '{self._config[CONFIG_TARGET_MPAN]}'") + all_rates = all_rates.get(self._config[CONFIG_TARGET_MPAN]) + else: + _LOGGER.debug(f"Rate data missing. Setting to empty array") + all_rates = [] + + _LOGGER.debug(f'{len(all_rates) if all_rates != None else None} rate periods found') + + start_time = None + if CONFIG_TARGET_START_TIME in self._config: + start_time = self._config[CONFIG_TARGET_START_TIME] + + end_time = None + if CONFIG_TARGET_END_TIME in self._config: + end_time = self._config[CONFIG_TARGET_END_TIME] + + # True by default for backwards compatibility + is_rolling_target = True + if CONFIG_TARGET_ROLLING_TARGET in self._config: + is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET] + + target_hours = float(self._config[CONFIG_TARGET_HOURS]) + + if (self._config[CONFIG_TARGET_TYPE] == "Continuous"): + self._target_rates = calculate_continuous_times( + now(), + start_time, + end_time, + target_hours, + all_rates, + is_rolling_target, + self._is_export + ) + elif (self._config[CONFIG_TARGET_TYPE] == "Intermittent"): + self._target_rates = calculate_intermittent_times( + now(), + start_time, + end_time, + target_hours, + all_rates, + is_rolling_target, + self._is_export + ) + else: + _LOGGER.error(f"Unexpected target type: {self._config[CONFIG_TARGET_TYPE]}") + + self._attributes["target_times"] = self._target_rates + + active_result = get_target_rate_info(current_date, self._target_rates, offset) + + self._attributes["overall_average_cost"] = f'{active_result["overall_average_cost"]}p' if active_result["overall_average_cost"] is not None else None + self._attributes["overall_min_cost"] = f'{active_result["overall_min_cost"]}p' if active_result["overall_min_cost"] is not None else None + self._attributes["overall_max_cost"] = f'{active_result["overall_max_cost"]}p' if active_result["overall_max_cost"] is not None else None + + self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"] + self._attributes["current_average_cost"] = f'{active_result["current_average_cost"]}p' if active_result["current_average_cost"] is not None else None + self._attributes["current_min_cost"] = f'{active_result["current_min_cost"]}p' if active_result["current_min_cost"] is not None else None + self._attributes["current_max_cost"] = f'{active_result["current_max_cost"]}p' if active_result["current_max_cost"] is not None else None + + self._attributes["next_time"] = active_result["next_time"] + self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"] + self._attributes["next_average_cost"] = f'{active_result["next_average_cost"]}p' if active_result["next_average_cost"] is not None else None + self._attributes["next_min_cost"] = f'{active_result["next_min_cost"]}p' if active_result["next_min_cost"] is not None else None + self._attributes["next_max_cost"] = f'{active_result["next_max_cost"]}p' if active_result["next_max_cost"] is not None else None + + return active_result["is_active"] + + @callback + def async_update_config(self, target_start_time=None, target_end_time=None, target_hours=None, target_offset=None): + """Update sensors config""" + + config = dict(self._config) + + if target_hours is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_hours = target_hours.strip('\"') + matches = re.search(REGEX_HOURS, trimmed_target_hours) + if matches == None: + raise vol.Invalid(f"Target hours of '{trimmed_target_hours}' must be in half hour increments.") + else: + trimmed_target_hours = float(trimmed_target_hours) + if trimmed_target_hours % 0.5 != 0: + raise vol.Invalid(f"Target hours of '{trimmed_target_hours}' must be in half hour increments.") + else: + config.update({ + CONFIG_TARGET_HOURS: trimmed_target_hours + }) + + if target_start_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_start_time = target_start_time.strip('\"') + matches = re.search(REGEX_TIME, trimmed_target_start_time) + if matches == None: + raise vol.Invalid("Start time must be in the format HH:MM") + else: + config.update({ + CONFIG_TARGET_START_TIME: trimmed_target_start_time + }) + + if target_end_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_end_time = target_end_time.strip('\"') + matches = re.search(REGEX_TIME, trimmed_target_end_time) + if matches == None: + raise vol.Invalid("End time must be in the format HH:MM") + else: + config.update({ + CONFIG_TARGET_END_TIME: trimmed_target_end_time + }) + + if target_offset is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_offset = target_offset.strip('\"') + matches = re.search(REGEX_OFFSET_PARTS, trimmed_target_offset) + if matches == None: + raise vol.Invalid("Offset must be in the form of HH:MM:SS with an optional negative symbol") + else: + config.update({ + CONFIG_TARGET_OFFSET: trimmed_target_offset + }) + + self._config = config + self._attributes = self._config.copy() + self._attributes["is_target_export"] = self._is_export + self._target_rates = [] + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/config_flow.py b/custom_components/octopus_energy/config_flow.py new file mode 100644 index 00000000..2bb1c5cb --- /dev/null +++ b/custom_components/octopus_energy/config_flow.py @@ -0,0 +1,322 @@ + +import voluptuous as vol +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.config_entries import (ConfigFlow, OptionsFlow) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .target_rates.config import validate_target_rate_config +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION, + CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES, + CONFIG_MAIN_CALORIFIC_VALUE, + CONFIG_MAIN_ELECTRICITY_PRICE_CAP, + CONFIG_MAIN_CLEAR_ELECTRICITY_PRICE_CAP, + CONFIG_MAIN_GAS_PRICE_CAP, + CONFIG_MAIN_CLEAR_GAS_PRICE_CAP, + + CONFIG_TARGET_NAME, + CONFIG_TARGET_HOURS, + CONFIG_TARGET_START_TIME, + CONFIG_TARGET_END_TIME, + CONFIG_TARGET_TYPE, + CONFIG_TARGET_MPAN, + CONFIG_TARGET_OFFSET, + CONFIG_TARGET_ROLLING_TARGET, + CONFIG_TARGET_LAST_RATES, + CONFIG_TARGET_INVERT_TARGET_RATES, + + DATA_SCHEMA_ACCOUNT, + DATA_CLIENT, + DATA_ACCOUNT_ID, +) + +from .api_client import OctopusEnergyApiClient + +from .utils import get_active_tariff_code + +_LOGGER = logging.getLogger(__name__) + +def get_target_rate_meters(account_info, now): + meters = {} + if account_info is not None and len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + active_tariff_code = get_active_tariff_code(now, point["agreements"]) + + is_export = False + for meter in point["meters"]: + if meter["is_export"] == True: + is_export = True + break + + if active_tariff_code is not None: + meters[point["mpan"]] = f'{point["mpan"]} ({"Export" if is_export == True else "Import"})' + + return meters + +class OctopusEnergyConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow.""" + + VERSION = 1 + + async def async_setup_initial_account(self, user_input): + """Setup the initial account based on the provided user input""" + errors = {} + + electricity_price_cap = None + if CONFIG_MAIN_ELECTRICITY_PRICE_CAP in user_input: + electricity_price_cap = user_input[CONFIG_MAIN_ELECTRICITY_PRICE_CAP] + + gas_price_cap = None + if CONFIG_MAIN_GAS_PRICE_CAP in user_input: + gas_price_cap = user_input[CONFIG_MAIN_GAS_PRICE_CAP] + + client = OctopusEnergyApiClient(user_input[CONFIG_MAIN_API_KEY], electricity_price_cap, gas_price_cap) + account_info = await client.async_get_account(user_input[CONFIG_MAIN_ACCOUNT_ID]) + if (account_info is None): + errors[CONFIG_MAIN_ACCOUNT_ID] = "account_not_found" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_ACCOUNT, errors=errors + ) + + # Setup our basic sensors + return self.async_create_entry( + title="Account", + data=user_input + ) + + async def async_setup_target_rate_schema(self): + client = self.hass.data[DOMAIN][DATA_CLIENT] + account_info = await client.async_get_account(self.hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + now = utcnow() + meters = get_target_rate_meters(account_info, now) + + return vol.Schema({ + vol.Required(CONFIG_TARGET_NAME): str, + vol.Required(CONFIG_TARGET_HOURS): str, + vol.Required(CONFIG_TARGET_TYPE, default="Continuous"): vol.In({ + "Continuous": "Continuous", + "Intermittent": "Intermittent" + }), + vol.Required(CONFIG_TARGET_MPAN): vol.In( + meters + ), + vol.Optional(CONFIG_TARGET_START_TIME): str, + vol.Optional(CONFIG_TARGET_END_TIME): str, + vol.Optional(CONFIG_TARGET_OFFSET): str, + vol.Optional(CONFIG_TARGET_ROLLING_TARGET, default=False): bool, + vol.Optional(CONFIG_TARGET_LAST_RATES, default=False): bool, + vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES, default=False): bool, + }) + + async def async_step_target_rate(self, user_input): + """Setup a target based on the provided user input""" + client = self.hass.data[DOMAIN][DATA_CLIENT] + account_info = await client.async_get_account(self.hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + now = utcnow() + errors = validate_target_rate_config(user_input, account_info, now) + + if len(errors) < 1: + # Setup our targets sensor + return self.async_create_entry( + title=f"{user_input[CONFIG_TARGET_NAME]} (target)", + data=user_input + ) + + # Reshow our form with raised logins + data_Schema = await self.async_setup_target_rate_schema() + return self.async_show_form( + step_id="target_rate", data_schema=data_Schema, errors=errors + ) + + async def async_step_user(self, user_input): + """Setup based on user config""" + + is_account_setup = False + for entry in self._async_current_entries(include_ignore=False): + if CONFIG_MAIN_API_KEY in entry.data: + is_account_setup = True + break + + if user_input is not None: + # We are setting up our initial stage + if CONFIG_MAIN_API_KEY in user_input: + return await self.async_setup_initial_account(user_input) + + # We are setting up a target + if CONFIG_TARGET_NAME in user_input: + return await self.async_step_target_rate(user_input) + + if is_account_setup: + data_Schema = await self.async_setup_target_rate_schema() + return self.async_show_form( + step_id="target_rate", data_schema=data_Schema + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_ACCOUNT + ) + + @staticmethod + @callback + def async_get_options_flow(entry): + return OptionsFlowHandler(entry) + +class OptionsFlowHandler(OptionsFlow): + """Handles options flow for the component.""" + + def __init__(self, entry) -> None: + self._entry = entry + + async def __async_setup_target_rate_schema(self, config, errors): + client = self.hass.data[DOMAIN][DATA_CLIENT] + account_info = await client.async_get_account(self.hass.data[DOMAIN][DATA_ACCOUNT_ID]) + if account_info is None: + errors[CONFIG_TARGET_MPAN] = "account_not_found" + + now = utcnow() + meters = get_target_rate_meters(account_info, now) + + if (CONFIG_TARGET_MPAN not in config): + config[CONFIG_TARGET_MPAN] = meters[0] + + start_time_key = vol.Optional(CONFIG_TARGET_START_TIME) + if (CONFIG_TARGET_START_TIME in config): + start_time_key = vol.Optional(CONFIG_TARGET_START_TIME, default=config[CONFIG_TARGET_START_TIME]) + + end_time_key = vol.Optional(CONFIG_TARGET_END_TIME) + if (CONFIG_TARGET_END_TIME in config): + end_time_key = vol.Optional(CONFIG_TARGET_END_TIME, default=config[CONFIG_TARGET_END_TIME]) + + offset_key = vol.Optional(CONFIG_TARGET_OFFSET) + if (CONFIG_TARGET_OFFSET in config): + offset_key = vol.Optional(CONFIG_TARGET_OFFSET, default=config[CONFIG_TARGET_OFFSET]) + + # True by default for backwards compatibility + is_rolling_target = True + if (CONFIG_TARGET_ROLLING_TARGET in config): + is_rolling_target = config[CONFIG_TARGET_ROLLING_TARGET] + + find_last_rates = False + if (CONFIG_TARGET_LAST_RATES in config): + find_last_rates = config[CONFIG_TARGET_LAST_RATES] + + invert_target_rates = False + if (CONFIG_TARGET_INVERT_TARGET_RATES in config): + invert_target_rates = config[CONFIG_TARGET_INVERT_TARGET_RATES] + + return self.async_show_form( + step_id="target_rate", + data_schema=vol.Schema({ + vol.Required(CONFIG_TARGET_NAME, default=config[CONFIG_TARGET_NAME]): str, + vol.Required(CONFIG_TARGET_HOURS, default=f'{config[CONFIG_TARGET_HOURS]}'): str, + vol.Required(CONFIG_TARGET_TYPE, default=config[CONFIG_TARGET_TYPE]): vol.In({ + "Continuous": "Continuous", + "Intermittent": "Intermittent" + }), + vol.Required(CONFIG_TARGET_MPAN, default=config[CONFIG_TARGET_MPAN]): vol.In( + meters + ), + start_time_key: str, + end_time_key: str, + offset_key: str, + vol.Optional(CONFIG_TARGET_ROLLING_TARGET, default=is_rolling_target): bool, + vol.Optional(CONFIG_TARGET_LAST_RATES, default=find_last_rates): bool, + vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES, default=invert_target_rates): bool, + }), + errors=errors + ) + + async def async_step_init(self, user_input): + """Manage the options for the custom component.""" + + if CONFIG_MAIN_API_KEY in self._entry.data: + config = dict(self._entry.data) + if self._entry.options is not None: + config.update(self._entry.options) + + supports_live_consumption = False + if CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION in config: + supports_live_consumption = config[CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION] + + live_consumption_refresh_in_minutes = 1 + if CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES in config: + live_consumption_refresh_in_minutes = config[CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES] + + calorific_value = 40 + if CONFIG_MAIN_CALORIFIC_VALUE in config: + calorific_value = config[CONFIG_MAIN_CALORIFIC_VALUE] + + electricity_price_cap_key = vol.Optional(CONFIG_MAIN_ELECTRICITY_PRICE_CAP) + if (CONFIG_MAIN_ELECTRICITY_PRICE_CAP in config): + electricity_price_cap_key = vol.Optional(CONFIG_MAIN_ELECTRICITY_PRICE_CAP, default=config[CONFIG_MAIN_ELECTRICITY_PRICE_CAP]) + + gas_price_cap_key = vol.Optional(CONFIG_MAIN_GAS_PRICE_CAP) + if (CONFIG_MAIN_GAS_PRICE_CAP in config): + gas_price_cap_key = vol.Optional(CONFIG_MAIN_GAS_PRICE_CAP, default=config[CONFIG_MAIN_GAS_PRICE_CAP]) + + return self.async_show_form( + step_id="user", data_schema=vol.Schema({ + vol.Required(CONFIG_MAIN_API_KEY, default=config[CONFIG_MAIN_API_KEY]): str, + vol.Required(CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION, default=supports_live_consumption): bool, + vol.Required(CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES, default=live_consumption_refresh_in_minutes): cv.positive_int, + vol.Required(CONFIG_MAIN_CALORIFIC_VALUE, default=calorific_value): cv.positive_float, + electricity_price_cap_key: cv.positive_float, + vol.Required(CONFIG_MAIN_CLEAR_ELECTRICITY_PRICE_CAP): bool, + gas_price_cap_key: cv.positive_float, + vol.Required(CONFIG_MAIN_CLEAR_GAS_PRICE_CAP): bool, + }) + ) + elif CONFIG_TARGET_TYPE in self._entry.data: + config = dict(self._entry.data) + if self._entry.options is not None: + config.update(self._entry.options) + + return await self.__async_setup_target_rate_schema(config, {}) + + return self.async_abort(reason="not_supported") + + async def async_step_user(self, user_input): + """Manage the options for the custom component.""" + + if user_input is not None: + config = dict(self._entry.data) + config.update(user_input) + + if config[CONFIG_MAIN_CLEAR_ELECTRICITY_PRICE_CAP] == True: + del config[CONFIG_MAIN_ELECTRICITY_PRICE_CAP] + + if config[CONFIG_MAIN_CLEAR_GAS_PRICE_CAP] == True: + del config[CONFIG_MAIN_GAS_PRICE_CAP] + + return self.async_create_entry(title="", data=config) + + return self.async_abort(reason="not_supported") + + async def async_step_target_rate(self, user_input): + """Manage the options for the custom component.""" + + if user_input is not None: + config = dict(self._entry.data) + config.update(user_input) + + client = self.hass.data[DOMAIN][DATA_CLIENT] + account_info = await client.async_get_account(self.hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + now = utcnow() + errors = validate_target_rate_config(user_input, account_info, now) + + if (len(errors) > 0): + return await self.__async_setup_target_rate_schema(config, errors) + + return self.async_create_entry(title="", data=config) + + return self.async_abort(reason="not_supported") \ No newline at end of file diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py new file mode 100644 index 00000000..8901934d --- /dev/null +++ b/custom_components/octopus_energy/const.py @@ -0,0 +1,71 @@ +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +DOMAIN = "octopus_energy" + +CONFIG_MAIN_API_KEY = "Api key" +CONFIG_MAIN_ACCOUNT_ID = "Account Id" +CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION = "supports_live_consumption" +CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES = "live_consumption_refresh_in_minutes" +CONFIG_MAIN_CALORIFIC_VALUE = "calorific_value" +CONFIG_MAIN_ELECTRICITY_PRICE_CAP = "electricity_price_cap" +CONFIG_MAIN_CLEAR_ELECTRICITY_PRICE_CAP = "clear_electricity_price_cap" +CONFIG_MAIN_GAS_PRICE_CAP = "gas_price_cap" +CONFIG_MAIN_CLEAR_GAS_PRICE_CAP = "clear_gas_price_cap" + +CONFIG_TARGET_NAME = "Name" +CONFIG_TARGET_HOURS = "Hours" +CONFIG_TARGET_TYPE = "Type" +CONFIG_TARGET_START_TIME = "Start time" +CONFIG_TARGET_END_TIME = "End time" +CONFIG_TARGET_MPAN = "MPAN" +CONFIG_TARGET_OFFSET = "offset" +CONFIG_TARGET_ROLLING_TARGET = "rolling_target" +CONFIG_TARGET_LAST_RATES = "last_rates" +CONFIG_TARGET_INVERT_TARGET_RATES = "target_invert_target_rates" + +DATA_CONFIG = "CONFIG" +DATA_ELECTRICITY_RATES_COORDINATOR = "ELECTRICITY_RATES_COORDINATOR" +DATA_ELECTRICITY_RATES = "ELECTRICITY_RATES" +DATA_CLIENT = "CLIENT" +DATA_GAS_TARIFF_CODE = "GAS_TARIFF_CODE" +DATA_ACCOUNT_ID = "ACCOUNT_ID" +DATA_ACCOUNT = "ACCOUNT" +DATA_ACCOUNT_COORDINATOR = "ACCOUNT_COORDINATOR" +DATA_SAVING_SESSIONS = "SAVING_SESSIONS" +DATA_SAVING_SESSIONS_COORDINATOR = "SAVING_SESSIONS_COORDINATOR" +DATA_KNOWN_TARIFF = "KNOWN_TARIFF" +DATA_GAS_RATES_COORDINATOR = "DATA_GAS_RATES_COORDINATOR" +DATA_GAS_RATES = "GAS_RATES" +DATA_INTELLIGENT_DISPATCHES = "INTELLIGENT_DISPATCHES" +DATA_INTELLIGENT_DISPATCHES_COORDINATOR = "INTELLIGENT_DISPATCHES_COORDINATOR" +DATA_INTELLIGENT_SETTINGS = "INTELLIGENT_SETTINGS" +DATA_INTELLIGENT_SETTINGS_COORDINATOR = "INTELLIGENT_SETTINGS_COORDINATOR" + +DATA_ELECTRICITY_STANDING_CHARGES_COORDINATOR = "ELECTRICITY_STANDING_CHARGES_COORDINATOR" +DATA_ELECTRICITY_STANDING_CHARGES = "ELECTRICITY_STANDING_CHARGES" + +DATA_GAS_STANDING_CHARGES_COORDINATOR = "GAS_STANDING_CHARGES_COORDINATOR" +DATA_GAS_STANDING_CHARGES = "GAS_STANDING_CHARGES" + +STORAGE_COMPLETED_DISPATCHES_NAME = "octopus_energy.{}-completed-intelligent-dispatches.json" + +STORAGE_COMPLETED_DISPATCHES_NAME = "octopus_energy.{}-completed-intelligent-dispatches.json" + +REGEX_HOURS = "^[0-9]+(\\.[0-9]+)*$" +REGEX_TIME = "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$" +REGEX_ENTITY_NAME = "^[a-z0-9_]+$" +# According to https://www.guylipman.com/octopus/api_guide.html#s1b, this part should indicate the types of tariff +# However it looks like there are some tariffs that don't fit this mold +REGEX_TARIFF_PARTS = "^((?P[A-Z])-(?P[0-9A-Z]+)-)?(?P[A-Z0-9-]+)-(?P[A-Z])$" +REGEX_OFFSET_PARTS = "^(-)?([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$" + +DATA_SCHEMA_ACCOUNT = vol.Schema({ + vol.Required(CONFIG_MAIN_API_KEY): str, + vol.Required(CONFIG_MAIN_ACCOUNT_ID): str, + vol.Required(CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION): bool, + vol.Required(CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES, default=1): cv.positive_int, + vol.Required(CONFIG_MAIN_CALORIFIC_VALUE, default=40.0): cv.positive_float, + vol.Optional(CONFIG_MAIN_ELECTRICITY_PRICE_CAP): cv.positive_float, + vol.Optional(CONFIG_MAIN_GAS_PRICE_CAP): cv.positive_float +}) diff --git a/custom_components/octopus_energy/coordinators/__init__.py b/custom_components/octopus_energy/coordinators/__init__.py new file mode 100644 index 00000000..edd2347b --- /dev/null +++ b/custom_components/octopus_energy/coordinators/__init__.py @@ -0,0 +1,92 @@ +from datetime import datetime +import logging + +from homeassistant.helpers import issue_registry as ir + +from homeassistant.util.dt import (now) + +from ..const import ( + DOMAIN, + DATA_ACCOUNT, +) + +from ..api_client import OctopusEnergyApiClient + +from ..utils import ( + get_active_tariff_code, + get_tariff_parts +) + +from ..const import ( + DOMAIN, + DATA_KNOWN_TARIFF, +) + +_LOGGER = logging.getLogger(__name__) + +async def async_check_valid_tariff(hass, client: OctopusEnergyApiClient, tariff_code: str, is_electricity: bool): + tariff_key = f'{DATA_KNOWN_TARIFF}_{tariff_code}' + if (tariff_key not in hass.data[DOMAIN]): + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + ir.async_create_issue( + hass, + DOMAIN, + f"unknown_tariff_format_{tariff_code}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/unknown_tariff_format.md", + translation_key="unknown_tariff_format", + translation_placeholders={ "type": "Electricity" if is_electricity else "Gas", "tariff_code": tariff_code }, + ) + else: + try: + _LOGGER.debug(f"Retrieving product information for '{tariff_parts.product_code}'") + product = await client.async_get_product(tariff_parts.product_code) + if product is None: + ir.async_create_issue( + hass, + DOMAIN, + f"unknown_tariff_{tariff_code}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/unknown_tariff.md", + translation_key="unknown_tariff", + translation_placeholders={ "type": "Electricity" if is_electricity else "Gas", "tariff_code": tariff_code }, + ) + else: + hass.data[DOMAIN][tariff_key] = True + except: + _LOGGER.debug(f"Failed to retrieve product info for '{tariff_parts.product_code}'") + +def get_current_electricity_agreement_tariff_codes(current: datetime, account_info): + tariff_codes = {} + if account_info is not None and len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + active_tariff_code = get_active_tariff_code(current, point["agreements"]) + # The type of meter (ie smart vs dumb) can change the tariff behaviour, so we + # have to enumerate the different meters being used for each tariff as well. + for meter in point["meters"]: + is_smart_meter = meter["is_smart_meter"] + if active_tariff_code is not None: + key = (point["mpan"], is_smart_meter) + if key not in tariff_codes: + tariff_codes[(point["mpan"], is_smart_meter)] = active_tariff_code + + return tariff_codes + +def get_current_gas_agreement_tariff_codes(current: datetime, account_info): + tariff_codes = {} + if account_info is not None and len(account_info["gas_meter_points"]) > 0: + for point in account_info["gas_meter_points"]: + active_tariff_code = get_active_tariff_code(current, point["agreements"]) + # The type of meter (ie smart vs dumb) can change the tariff behaviour, so we + # have to enumerate the different meters being used for each tariff as well. + for meter in point["meters"]: + is_smart_meter = meter["is_smart_meter"] + if active_tariff_code is not None: + key = (point["mprn"], is_smart_meter) + if key not in tariff_codes: + tariff_codes[(point["mprn"], is_smart_meter)] = active_tariff_code + + return tariff_codes \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/account.py b/custom_components/octopus_energy/coordinators/account.py new file mode 100644 index 00000000..cc02e62c --- /dev/null +++ b/custom_components/octopus_energy/coordinators/account.py @@ -0,0 +1,80 @@ +import logging +from datetime import timedelta + +from . import async_check_valid_tariff +from ..utils import get_active_tariff_code + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from homeassistant.helpers import issue_registry as ir + +from ..const import ( + DOMAIN, + + DATA_CLIENT, + DATA_ACCOUNT, + DATA_ACCOUNT_COORDINATOR, +) + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_account_info_coordinator(hass, account_id: str): + async def async_update_account_data(): + """Fetch data from API endpoint.""" + # Only get data every half hour or if we don't have any data + current = now() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + if (DATA_ACCOUNT not in hass.data[DOMAIN] or (current.minute % 30) == 0): + account_info = None + try: + account_info = await client.async_get_account(account_id) + + if account_info is None: + ir.async_create_issue( + hass, + DOMAIN, + f"account_not_found_{account_id}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/account_not_found.md", + translation_key="account_not_found", + translation_placeholders={ "account_id": account_id }, + ) + else: + _LOGGER.debug('Account information retrieved') + + ir.async_delete_issue(hass, DOMAIN, f"account_not_found_{account_id}") + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + + if account_info is not None and len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + active_tariff_code = get_active_tariff_code(current, point["agreements"]) + await async_check_valid_tariff(hass, client, active_tariff_code, True) + + if account_info is not None and len(account_info["gas_meter_points"]) > 0: + for point in account_info["gas_meter_points"]: + active_tariff_code = get_active_tariff_code(current, point["agreements"]) + await async_check_valid_tariff(hass, client, active_tariff_code, False) + + except: + # count exceptions as failure to retrieve account + _LOGGER.debug('Failed to retrieve account information') + + return hass.data[DOMAIN][DATA_ACCOUNT] + + hass.data[DOMAIN][DATA_ACCOUNT_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="update_account", + update_method=async_update_account_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await hass.data[DOMAIN][DATA_ACCOUNT_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/current_consumption.py b/custom_components/octopus_energy/coordinators/current_consumption.py new file mode 100644 index 00000000..b3da7721 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/current_consumption.py @@ -0,0 +1,49 @@ +from datetime import (datetime, timedelta) +import logging + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, +) + +from ..api_client import (OctopusEnergyApiClient) + +_LOGGER = logging.getLogger(__name__) + +async def async_get_live_consumption(client: OctopusEnergyApiClient, device_id, current_date: datetime): + period_from = current_date.replace(hour=0, minute=0, second=0, microsecond=0) + period_to = current_date + timedelta(days=1) + + try: + result = await client.async_get_smart_meter_consumption(device_id, period_from, period_to) + if result is not None: + _LOGGER.debug(f'Current Home Mini consumption retrieved') + return result + + except: + _LOGGER.debug('Failed to retrieve smart meter consumption data') + + return None + +async def async_create_current_consumption_coordinator(hass, client: OctopusEnergyApiClient, device_id: str, is_electricity: bool, refresh_rate_in_minutes: int): + """Create current consumption coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + return await async_get_live_consumption(client, device_id, now()) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"current_consumption_{device_id}", + update_method=async_update_data, + update_interval=timedelta(minutes=refresh_rate_in_minutes), + ) + + await coordinator.async_config_entry_first_refresh() + + return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/electricity_rates.py b/custom_components/octopus_energy/coordinators/electricity_rates.py new file mode 100644 index 00000000..c79b99e3 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/electricity_rates.py @@ -0,0 +1,104 @@ +import logging +from datetime import datetime, timedelta + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + DATA_CLIENT, + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_ELECTRICITY_RATES, + DATA_ACCOUNT, + DATA_INTELLIGENT_DISPATCHES, +) + +from ..api_client import OctopusEnergyApiClient + +from . import get_current_electricity_agreement_tariff_codes +from ..intelligent import adjust_intelligent_rates + +_LOGGER = logging.getLogger(__name__) + +async def async_refresh_electricity_rates_data( + current: datetime, + client: OctopusEnergyApiClient, + account_info, + existing_rates: list, + dispatches: list + ): + if (account_info is not None): + tariff_codes = get_current_electricity_agreement_tariff_codes(current, account_info) + + period_from = as_utc((current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = as_utc((current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)) + + rates = {} + for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): + key = meter_point + + new_rates = None + if ((current.minute % 30) == 0 or + existing_rates is None or + key not in existing_rates or + existing_rates[key][-1]["valid_from"] < period_from): + try: + new_rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + _LOGGER.debug(f'Electricity rates retrieved for {tariff_code}') + except: + _LOGGER.debug('Failed to retrieve electricity rates') + else: + new_rates = existing_rates[key] + + if new_rates is not None: + if dispatches is not None: + rates[key] = adjust_intelligent_rates(new_rates, + dispatches["planned"] if "planned" in dispatches else [], + dispatches["completed"] if "completed" in dispatches else []) + + _LOGGER.debug(f"Rates adjusted: {rates[key]}; dispatches: {dispatches}") + else: + rates[key] = new_rates + elif (existing_rates is not None and key in existing_rates): + _LOGGER.debug(f"Failed to retrieve new electricity rates for {tariff_code}, so using cached rates") + rates[key] = existing_rates[key] + + return rates + + return existing_rates + +async def async_setup_electricity_rates_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_ELECTRICITY_RATES] = [] + + async def async_update_electricity_rates_data(): + """Fetch data from API endpoint.""" + current = now() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None + dispatches = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN] else None + rates = hass.data[DOMAIN][DATA_ELECTRICITY_RATES] if DATA_ELECTRICITY_RATES in hass.data[DOMAIN] else {} + + hass.data[DOMAIN][DATA_ELECTRICITY_RATES] = await async_refresh_electricity_rates_data( + current, + client, + account_info, + rates, + dispatches + ) + + return hass.data[DOMAIN][DATA_ELECTRICITY_RATES] + + hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="electricity_rates", + update_method=async_update_electricity_rates_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/electricity_standing_charges.py b/custom_components/octopus_energy/coordinators/electricity_standing_charges.py new file mode 100644 index 00000000..2fcafc22 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/electricity_standing_charges.py @@ -0,0 +1,93 @@ +import logging +from datetime import datetime, timedelta + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + DATA_CLIENT, + DATA_ELECTRICITY_STANDING_CHARGES, + DATA_ACCOUNT, +) + +from ..api_client import OctopusEnergyApiClient + +from . import get_current_electricity_agreement_tariff_codes + +_LOGGER = logging.getLogger(__name__) + +async def async_refresh_electricity_standing_charges_data( + current: datetime, + client: OctopusEnergyApiClient, + account_info, + existing_standing_charges: list + ): + if (account_info is not None): + tariff_codes = get_current_electricity_agreement_tariff_codes(current, account_info) + + period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = period_from + timedelta(days=1) + + standing_charges = {} + for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): + key = meter_point + + new_standing_charges = None + if ((current.minute % 30) == 0 or + existing_standing_charges is None or + key not in existing_standing_charges or + (existing_standing_charges[key]["valid_from"] is not None and existing_standing_charges[key]["valid_from"] < period_from)): + try: + new_standing_charges = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + _LOGGER.debug(f'Electricity standing charges retrieved for {tariff_code}') + except: + _LOGGER.debug(f'Failed to retrieve electricity standing charges for {tariff_code}') + else: + new_standing_charges = existing_standing_charges[key] + + if new_standing_charges is not None: + standing_charges[key] = new_standing_charges + elif (existing_standing_charges is not None and key in existing_standing_charges): + _LOGGER.debug(f"Failed to retrieve new electricity standing charges for {tariff_code}, so using cached standing charges") + standing_charges[key] = existing_standing_charges[key] + + return standing_charges + + return existing_standing_charges + +async def async_setup_electricity_standing_charges_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] = [] + + async def async_update_electricity_standing_charges_data(): + """Fetch data from API endpoint.""" + current = now() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None + standing_charges = hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] if DATA_ELECTRICITY_STANDING_CHARGES in hass.data[DOMAIN] else {} + + hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + standing_charges, + ) + + return hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="electricity_standing_charges", + update_method=async_update_electricity_standing_charges_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await coordinator.async_config_entry_first_refresh() + + return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/gas_rates.py b/custom_components/octopus_energy/coordinators/gas_rates.py new file mode 100644 index 00000000..d978c82f --- /dev/null +++ b/custom_components/octopus_energy/coordinators/gas_rates.py @@ -0,0 +1,90 @@ +from datetime import datetime, timedelta +import logging + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DATA_ACCOUNT, + DOMAIN, + DATA_GAS_RATES +) + +from ..api_client import (OctopusEnergyApiClient) + +from . import get_current_gas_agreement_tariff_codes + +_LOGGER = logging.getLogger(__name__) + +async def async_refresh_gas_rates_data( + current: datetime, + client: OctopusEnergyApiClient, + account_info, + existing_rates: list + ): + if (account_info is not None): + tariff_codes = get_current_gas_agreement_tariff_codes(current, account_info) + + period_from = as_utc((current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = as_utc((current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)) + + rates = {} + for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): + key = meter_point + + new_rates = None + if ((current.minute % 30) == 0 or + existing_rates is None or + key not in existing_rates or + existing_rates[key][-1]["valid_from"] < period_from): + try: + new_rates = await client.async_get_gas_rates(tariff_code, period_from, period_to) + _LOGGER.debug(f'Gas rates retrieved for {tariff_code}') + except: + _LOGGER.debug('Failed to retrieve gas rates') + else: + new_rates = existing_rates[key] + + if new_rates is not None: + rates[key] = new_rates + elif (existing_rates is not None and key in existing_rates): + _LOGGER.debug(f"Failed to retrieve new gas rates for {tariff_code}, so using cached rates") + rates[key] = existing_rates[key] + + return rates + + return existing_rates + +async def async_create_gas_rate_coordinator(hass, client: OctopusEnergyApiClient): + """Create gas rate coordinator""" + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_GAS_RATES] = [] + + async def async_update_data(): + """Fetch data from API endpoint.""" + current = now() + account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None + rates = hass.data[DOMAIN][DATA_GAS_RATES] if DATA_GAS_RATES in hass.data[DOMAIN] else {} + + hass.data[DOMAIN][DATA_GAS_RATES] = await async_refresh_gas_rates_data( + current, + client, + account_info, + rates + ) + + return hass.data[DOMAIN][DATA_GAS_RATES] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"gas_rates", + update_method=async_update_data, + update_interval=timedelta(minutes=1), + ) + + await coordinator.async_config_entry_first_refresh() + + return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/gas_standing_charges.py b/custom_components/octopus_energy/coordinators/gas_standing_charges.py new file mode 100644 index 00000000..fdcbefb1 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/gas_standing_charges.py @@ -0,0 +1,93 @@ +import logging +from datetime import datetime, timedelta + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + DATA_CLIENT, + DATA_GAS_STANDING_CHARGES, + DATA_ACCOUNT, +) + +from ..api_client import OctopusEnergyApiClient + +from . import get_current_gas_agreement_tariff_codes + +_LOGGER = logging.getLogger(__name__) + +async def async_refresh_gas_standing_charges_data( + current: datetime, + client: OctopusEnergyApiClient, + account_info, + existing_standing_charges: list + ): + if (account_info is not None): + tariff_codes = get_current_gas_agreement_tariff_codes(current, account_info) + + period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = period_from + timedelta(days=1) + + standing_charges = {} + for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): + key = meter_point + + new_standing_charges = None + if ((current.minute % 30) == 0 or + existing_standing_charges is None or + key not in existing_standing_charges or + (existing_standing_charges[key]["valid_from"] is not None and existing_standing_charges[key]["valid_from"] < period_from)): + try: + new_standing_charges = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to) + _LOGGER.debug(f'Gas standing charges retrieved for {tariff_code}') + except: + _LOGGER.debug(f'Failed to retrieve gas standing charges for {tariff_code}') + else: + new_standing_charges = existing_standing_charges[key] + + if new_standing_charges is not None: + standing_charges[key] = new_standing_charges + elif (existing_standing_charges is not None and key in existing_standing_charges): + _LOGGER.debug(f"Failed to retrieve new gas standing charges for {tariff_code}, so using cached standing charges") + standing_charges[key] = existing_standing_charges[key] + + return standing_charges + + return existing_standing_charges + +async def async_setup_gas_standing_charges_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] = [] + + async def async_update_gas_standing_charges_data(): + """Fetch data from API endpoint.""" + current = now() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None + standing_charges = hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] if DATA_GAS_STANDING_CHARGES in hass.data[DOMAIN] else {} + + hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + standing_charges, + ) + + return hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="gas_standing_charges", + update_method=async_update_gas_standing_charges_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await coordinator.async_config_entry_first_refresh() + + return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/intelligent_dispatches.py b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py new file mode 100644 index 00000000..be4adfa7 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py @@ -0,0 +1,90 @@ +import logging +from datetime import timedelta + +from ..coordinators import get_current_electricity_agreement_tariff_codes +from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, is_intelligent_tariff, mock_intelligent_dispatches + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) +from homeassistant.helpers import storage + +from ..const import ( + DOMAIN, + + DATA_CLIENT, + DATA_ACCOUNT, + DATA_ACCOUNT_COORDINATOR, + DATA_INTELLIGENT_DISPATCHES, + DATA_INTELLIGENT_DISPATCHES_COORDINATOR, + + STORAGE_COMPLETED_DISPATCHES_NAME +) + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +async def async_merge_dispatch_data(hass, account_id: str, completed_dispatches): + storage_key = STORAGE_COMPLETED_DISPATCHES_NAME.format(account_id) + store = storage.Store(hass, "1", storage_key) + + saved_completed_dispatches = await store.async_load() + + new_data = clean_previous_dispatches(utcnow(), (saved_completed_dispatches if saved_completed_dispatches is not None else []) + completed_dispatches) + + await store.async_save(new_data) + return new_data + +async def async_setup_intelligent_dispatches_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] = None + + async def async_update_intelligent_dispatches_data(): + """Fetch data from API endpoint.""" + # Request our account data to be refreshed + account_coordinator = hass.data[DOMAIN][DATA_ACCOUNT_COORDINATOR] + if account_coordinator is not None: + await account_coordinator.async_request_refresh() + + # Only get data every half hour or if we don't have any data + current = utcnow() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + if (DATA_ACCOUNT in hass.data[DOMAIN]): + + tariff_codes = get_current_electricity_agreement_tariff_codes(current, hass.data[DOMAIN][DATA_ACCOUNT]) + + dispatches = None + for ((meter_point), tariff_code) in tariff_codes.items(): + if is_intelligent_tariff(tariff_code): + try: + dispatches = await client.async_get_intelligent_dispatches(account_id) + _LOGGER.debug(f'Intelligent dispatches retrieved for {tariff_code}') + except: + _LOGGER.debug('Failed to retrieve intelligent dispatches') + break + + if await async_mock_intelligent_data(hass): + dispatches = mock_intelligent_dispatches() + + if dispatches is not None: + dispatches["completed"] = await async_merge_dispatch_data(hass, account_id, dispatches["completed"]) + hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] = dispatches + hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES]["last_updated"] = utcnow() + elif (DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN]): + _LOGGER.debug(f"Failed to retrieve new dispatches, so using cached dispatches") + + return hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] + + hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="intelligent_dispatches", + update_method=async_update_intelligent_dispatches_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/intelligent_settings.py b/custom_components/octopus_energy/coordinators/intelligent_settings.py new file mode 100644 index 00000000..b324c130 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/intelligent_settings.py @@ -0,0 +1,74 @@ +import logging +from datetime import timedelta + +from . import get_current_electricity_agreement_tariff_codes +from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, is_intelligent_tariff, mock_intelligent_settings + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) +from homeassistant.helpers import storage + +from ..const import ( + DOMAIN, + + DATA_CLIENT, + DATA_ACCOUNT, + DATA_ACCOUNT_COORDINATOR, + DATA_INTELLIGENT_SETTINGS, + DATA_INTELLIGENT_SETTINGS_COORDINATOR, +) + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_intelligent_settings_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS] = None + + async def async_update_intelligent_settings_data(): + """Fetch data from API endpoint.""" + # Request our account data to be refreshed + account_coordinator = hass.data[DOMAIN][DATA_ACCOUNT_COORDINATOR] + if account_coordinator is not None: + await account_coordinator.async_request_refresh() + + current = utcnow() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + if (DATA_ACCOUNT in hass.data[DOMAIN]): + + tariff_codes = get_current_electricity_agreement_tariff_codes(current, hass.data[DOMAIN][DATA_ACCOUNT]) + _LOGGER.debug(f'tariff_codes: {tariff_codes}') + + settings = None + for ((meter_point), tariff_code) in tariff_codes.items(): + if is_intelligent_tariff(tariff_code): + try: + settings = await client.async_get_intelligent_settings(account_id) + _LOGGER.debug(f'Intelligent settings retrieved for {tariff_code}') + except: + _LOGGER.debug('Failed to retrieve intelligent dispatches') + break + + if await async_mock_intelligent_data(hass): + settings = mock_intelligent_settings() + + if settings is not None: + hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS] = settings + hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS]["last_updated"] = utcnow() + elif (DATA_INTELLIGENT_SETTINGS in hass.data[DOMAIN]): + _LOGGER.debug(f"Failed to retrieve intelligent settings, so using cached settings") + + return hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS] + + hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="intelligent_settings", + update_method=async_update_intelligent_settings_data, + update_interval=timedelta(minutes=1), + ) + + await hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py b/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py new file mode 100644 index 00000000..9e217758 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py @@ -0,0 +1,139 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, now, as_utc) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + DATA_INTELLIGENT_DISPATCHES +) + +from ..api_client import (OctopusEnergyApiClient) + +from ..intelligent import adjust_intelligent_rates + +_LOGGER = logging.getLogger(__name__) + +def __get_interval_end(item): + return item["interval_end"] + +def __sort_consumption(consumption_data): + sorted = consumption_data.copy() + sorted.sort(key=__get_interval_end) + return sorted + +async def async_fetch_consumption_and_rates( + previous_data, + utc_now, + client: OctopusEnergyApiClient, + period_from, + period_to, + identifier: str, + serial_number: str, + is_electricity: bool, + tariff_code: str, + is_smart_meter: bool, + intelligent_dispatches = None + +): + """Fetch the previous consumption and rates""" + + if (previous_data == None or + ((len(previous_data["consumption"]) < 1 or + previous_data["consumption"][-1]["interval_end"] < period_to) and + utc_now.minute % 30 == 0)): + + try: + if (is_electricity == True): + consumption_data = await client.async_get_electricity_consumption(identifier, serial_number, period_from, period_to) + rate_data = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + if intelligent_dispatches is not None: + rate_data = adjust_intelligent_rates(rate_data, + intelligent_dispatches["planned"] if "planned" in intelligent_dispatches else [], + intelligent_dispatches["completed"] if "completed" in intelligent_dispatches else []) + + _LOGGER.debug(f"Tariff: {tariff_code}; dispatches: {intelligent_dispatches}") + standing_charge = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + + _LOGGER.debug(f'Previous Electricity consumption, rates and standing charges retrieved for {tariff_code}') + else: + consumption_data = await client.async_get_gas_consumption(identifier, serial_number, period_from, period_to) + rate_data = await client.async_get_gas_rates(tariff_code, period_from, period_to) + standing_charge = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to) + + _LOGGER.debug(f'Previous Gas consumption, rates and standing charges retrieved for {tariff_code}') + + if consumption_data is not None and len(consumption_data) > 0 and rate_data is not None and len(rate_data) > 0 and standing_charge is not None: + consumption_data = __sort_consumption(consumption_data) + + return { + "consumption": consumption_data, + "rates": rate_data, + "standing_charge": standing_charge["value_inc_vat"] + } + except: + _LOGGER.debug(f"Failed to retrieve {'electricity' if is_electricity else 'gas'} previous consumption and rate data") + + return previous_data + +async def async_create_previous_consumption_and_rates_coordinator( + hass, + client: OctopusEnergyApiClient, + identifier: str, + serial_number: str, + is_electricity: bool, + tariff_code: str, + is_smart_meter: bool): + """Create reading coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + + previous_consumption_key = f'{identifier}_{serial_number}_previous_consumption_and_rates' + period_from = as_utc((now() - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = as_utc(now().replace(hour=0, minute=0, second=0, microsecond=0)) + result = await async_fetch_consumption_and_rates( + hass.data[DOMAIN][previous_consumption_key] + if previous_consumption_key in hass.data[DOMAIN] and + "rates" in hass.data[DOMAIN][previous_consumption_key] and + "consumption" in hass.data[DOMAIN][previous_consumption_key] and + "standing_charge" in hass.data[DOMAIN][previous_consumption_key] + else None, + utcnow(), + client, + period_from, + period_to, + identifier, + serial_number, + is_electricity, + tariff_code, + is_smart_meter, + hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN] else None + ) + + if (result is not None): + hass.data[DOMAIN][previous_consumption_key] = result + + if previous_consumption_key in hass.data[DOMAIN] and "rates" in hass.data[DOMAIN][previous_consumption_key] and "consumption" in hass.data[DOMAIN][previous_consumption_key] and "standing_charge" in hass.data[DOMAIN][previous_consumption_key]: + return hass.data[DOMAIN][previous_consumption_key] + else: + return None + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"rates_{identifier}_{serial_number}", + update_method=async_update_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + hass.data[DOMAIN][f'{identifier}_{serial_number}_previous_consumption_and_cost_coordinator'] = coordinator + + await coordinator.async_config_entry_first_refresh() + + return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/saving_sessions.py b/custom_components/octopus_energy/coordinators/saving_sessions.py new file mode 100644 index 00000000..248644ec --- /dev/null +++ b/custom_components/octopus_energy/coordinators/saving_sessions.py @@ -0,0 +1,47 @@ +import logging +from datetime import timedelta + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + DATA_CLIENT, + DATA_ACCOUNT_ID, + DATA_SAVING_SESSIONS, + DATA_SAVING_SESSIONS_COORDINATOR, +) + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_saving_sessions_coordinators(hass): + async def async_update_saving_sessions(): + """Fetch data from API endpoint.""" + # Only get data every half hour or if we don't have any data + current = now() + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + if DATA_SAVING_SESSIONS not in hass.data[DOMAIN] or current.minute % 30 == 0: + + try: + savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) + hass.data[DOMAIN][DATA_SAVING_SESSIONS] = savings + except: + _LOGGER.debug('Failed to retrieve saving session information') + + return hass.data[DOMAIN][DATA_SAVING_SESSIONS] + + hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="saving_sessions", + update_method=async_update_saving_sessions, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(minutes=1), + ) + + await hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/diagnostics.py b/custom_components/octopus_energy/diagnostics.py new file mode 100644 index 00000000..23943d95 --- /dev/null +++ b/custom_components/octopus_energy/diagnostics.py @@ -0,0 +1,42 @@ +"""Diagnostics support.""" +import logging + +from homeassistant.components.diagnostics import async_redact_data + +from .const import ( + DOMAIN, + + DATA_ACCOUNT_ID, + DATA_CLIENT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_get_device_diagnostics(hass, config_entry, device): + """Return diagnostics for a device.""" + + client = hass.data[DOMAIN][DATA_CLIENT] + + _LOGGER.info('Retrieving account details for diagnostics...') + + account_info = await client.async_get_account(hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + points_length = account_info is not None and len(account_info["electricity_meter_points"]) + if account_info is not None and points_length > 0: + for point_index in range(points_length): + account_info["electricity_meter_points"][point_index] = async_redact_data(account_info["electricity_meter_points"][point_index], { "mpan" }) + meters_length = len(account_info["electricity_meter_points"][point_index]["meters"]) + for meter_index in range(meters_length): + account_info["electricity_meter_points"][point_index]["meters"][meter_index] = async_redact_data(account_info["electricity_meter_points"][point_index]["meters"][meter_index], { "serial_number", "device_id" }) + + points_length = account_info is not None and len(account_info["gas_meter_points"]) + if account_info is not None and points_length > 0: + for point_index in range(points_length): + account_info["gas_meter_points"][point_index] = async_redact_data(account_info["gas_meter_points"][point_index], { "mprn" }) + meters_length = len(account_info["gas_meter_points"][point_index]["meters"]) + for meter_index in range(meters_length): + account_info["gas_meter_points"][point_index]["meters"][meter_index] = async_redact_data(account_info["gas_meter_points"][point_index]["meters"][meter_index], { "serial_number", "device_id" }) + + _LOGGER.info(f'Returning diagnostic details; {len(account_info["electricity_meter_points"])} electricity meter point(s), {len(account_info["gas_meter_points"])} gas meter point(s)') + + return account_info \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/__init__.py b/custom_components/octopus_energy/electricity/__init__.py new file mode 100644 index 00000000..a1856a91 --- /dev/null +++ b/custom_components/octopus_energy/electricity/__init__.py @@ -0,0 +1,91 @@ +from ..utils import get_off_peak_cost + +def __get_interval_end(item): + return item["interval_end"] + +def __sort_consumption(consumption_data): + sorted = consumption_data.copy() + sorted.sort(key=__get_interval_end) + return sorted + +def calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + last_reset, + tariff_code, + minimum_consumption_records = 0 + ): + if (consumption_data is not None and len(consumption_data) >= minimum_consumption_records and rate_data is not None and len(rate_data) > 0 and standing_charge is not None): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_reset is None or last_reset < sorted_consumption_data[0]["interval_start"]): + + charges = [] + total_cost_in_pence = 0 + total_consumption = 0 + + off_peak_cost = get_off_peak_cost(rate_data) + total_cost_off_peak = 0 + total_cost_peak = 0 + total_consumption_off_peak = 0 + total_consumption_peak = 0 + + for consumption in sorted_consumption_data: + consumption_value = consumption["consumption"] + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + total_consumption = total_consumption + consumption_value + + try: + rate = next(r for r in rate_data if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {tariff_code}") + + value = rate["value_inc_vat"] + cost = (value * consumption_value) + total_cost_in_pence = total_cost_in_pence + cost + + if value == off_peak_cost: + total_consumption_off_peak = total_consumption_off_peak + consumption_value + total_cost_off_peak = total_cost_off_peak + cost + else: + total_consumption_peak = total_consumption_peak + consumption_value + total_cost_peak = total_cost_peak + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": value, + "consumption": consumption_value, + "cost": round(cost / 100, 2) + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standing_charge) / 100, 2) + + last_reset = sorted_consumption_data[0]["interval_start"] + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + result = { + "standing_charge": standing_charge, + "total_cost_without_standing_charge": total_cost, + "total_cost": total_cost_plus_standing_charge, + "total_consumption": total_consumption, + "last_reset": last_reset, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + + if off_peak_cost is not None: + result["total_cost_off_peak"] = round(total_cost_off_peak / 100, 2) + result["total_cost_peak"] = round(total_cost_peak / 100, 2) + result["total_consumption_off_peak"] = total_consumption_off_peak + result["total_consumption_peak"] = total_consumption_peak + + return result + +def get_electricity_tariff_override_key(serial_number: str, mpan: str) -> str: + return f'electricity_previous_consumption_tariff_{serial_number}_{mpan}' \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/base.py b/custom_components/octopus_energy/electricity/base.py new file mode 100644 index 00000000..5fe72b54 --- /dev/null +++ b/custom_components/octopus_energy/electricity/base.py @@ -0,0 +1,43 @@ +from homeassistant.core import HomeAssistant + +from homeassistant.components.sensor import ( + SensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from homeassistant.helpers.entity import generate_entity_id, DeviceInfo + +from ..const import ( + DOMAIN, +) + +class OctopusEnergyElectricitySensor(SensorEntity, RestoreEntity): + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor""" + self._point = point + self._meter = meter + + self._mpan = point["mpan"] + self._serial_number = meter["serial_number"] + self._is_export = meter["is_export"] + self._is_smart_meter = meter["is_smart_meter"] + self._export_id_addition = "_export" if self._is_export == True else "" + self._export_name_addition = " Export" if self._is_export == True else "" + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter + } + + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"electricity_{self._serial_number}_{self._mpan}")}, + name=f"Electricity Meter{self._export_name_addition}", + connections=set(), + manufacturer=self._meter["manufacturer"], + model=self._meter["model"], + sw_version=self._meter["firmware"] + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption.py new file mode 100644 index 00000000..fb83750c --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption.py @@ -0,0 +1,123 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyElectricitySensor) + +from . import calculate_electricity_consumption_and_cost + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current accumulative electricity consumption.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._last_reset = None + + self._tariff_code = tariff_code + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan} Current Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the current days accumulative consumption""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption for '{self._mpan}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption"] + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "total": consumption_and_cost["total_consumption"], + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "consumption": charge["consumption"] + }, consumption_and_cost["charges"])) + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeElectricityConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py new file mode 100644 index 00000000..db4ef670 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py @@ -0,0 +1,128 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current days accumulative electricity reading during off peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + + self._tariff_code = tariff_code + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_accumulative_consumption_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Accumulative Consumption (Off Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the current days accumulative consumption""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current electricity consumption off peak for '{self._mpan}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption_off_peak"] if "total_consumption_off_peak" in consumption_and_cost else 0 + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py new file mode 100644 index 00000000..b9db9c58 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py @@ -0,0 +1,128 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current days accumulative electricity reading during peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + + self._tariff_code = tariff_code + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_accumulative_consumption_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Accumulative Consumption (Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the current days accumulative consumption""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current electricity consumption peak for '{self._mpan}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption_peak"] if "total_consumption_peak" in consumption_and_cost else 0 + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost.py b/custom_components/octopus_energy/electricity/current_accumulative_cost.py new file mode 100644 index 00000000..6812768b --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost.py @@ -0,0 +1,143 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current days accumulative electricity cost.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the currently calculated state""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current electricity consumption cost for '{self._mpan}/{self._serial_number}'...") + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption"]} kWh', + "consumption_raw": charge["consumption"], + "cost": f'£{charge["cost"]}', + "cost_raw": charge["cost"], + }, consumption_and_cost["charges"])) + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeElectricityCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py new file mode 100644 index 00000000..3b19e08a --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py @@ -0,0 +1,124 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current days accumulative electricity cost during off peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + + self._tariff_code = tariff_code + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_accumulative_cost_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Accumulative Cost (Off Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the currently calculated state""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current electricity consumption cost off peak for '{self._mpan}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost_off_peak"] if "total_cost_off_peak" in consumption_and_cost else 0 + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeElectricityCostOffPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py new file mode 100644 index 00000000..6b9839f6 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py @@ -0,0 +1,124 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current days accumulative electricity cost during peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + + self._tariff_code = tariff_code + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_accumulative_cost_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Accumulative Cost (Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the currently calculated state""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current electricity consumption cost peak for '{self._mpan}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost_peak"] if "total_cost_peak" in consumption_and_cost else 0 + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeElectricityCostPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_consumption.py b/custom_components/octopus_energy/electricity/current_consumption.py new file mode 100644 index 00000000..bc50afe2 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_consumption.py @@ -0,0 +1,109 @@ +from homeassistant.util.dt import (now) +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyElectricitySensor) + +from ..utils.consumption import (get_current_consumption_delta, get_total_consumption) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current electricity consumption.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + self._previous_total_consumption = None + self._attributes = { + "last_updated_timestamp": None + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan} Current Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the latest electricity consumption""" + _LOGGER.debug('Updating OctopusEnergyCurrentElectricityConsumption') + consumption_result = self.coordinator.data if self.coordinator is not None else None + + current_date = now() + if (consumption_result is not None): + total_consumption = get_total_consumption(consumption_result) + self._state = get_current_consumption_delta(current_date, + total_consumption, + self._attributes["last_updated_timestamp"] if self._attributes["last_updated_timestamp"] is not None else current_date, + self._previous_total_consumption) + if (self._state is not None): + self._latest_date = current_date + self._attributes["last_updated_timestamp"] = current_date + + # Store the total consumption ready for the next run + self._previous_total_consumption = total_consumption + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentElectricityConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_demand.py b/custom_components/octopus_energy/electricity/current_demand.py new file mode 100644 index 00000000..d94b73b6 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_demand.py @@ -0,0 +1,88 @@ +from homeassistant.util.dt import (now) +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentElectricityDemand(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current electricity demand.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + self._attributes = { + "last_updated_timestamp": None + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_demand" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan} Current Demand" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.POWER + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.MEASUREMENT + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "W" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Handle updated data from the coordinator.""" + _LOGGER.debug('Updating OctopusEnergyCurrentElectricityConsumption') + consumption_result = self.coordinator.data if self.coordinator is not None else None + + if (consumption_result is not None): + self._state = consumption_result[-1]["demand"] + self._attributes["last_updated_timestamp"] = now() + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentElectricityDemand state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_rate.py b/custom_components/octopus_energy/electricity/current_rate.py new file mode 100644 index 00000000..0b5b7d37 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_rate.py @@ -0,0 +1,155 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyElectricitySensor) + +from ..utils.rate_information import (get_current_rate_information) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point, tariff_code, electricity_price_cap): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._last_updated = None + self._electricity_price_cap = electricity_price_cap + self._tariff_code = tariff_code + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "all_rates": [], + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + "is_capped": None, + "is_intelligent_adjusted": None, + "current_day_min_rate": None, + "current_day_max_rate": None, + "current_day_average_rate": None + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Rate" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the current rate for the sensor.""" + # Find the current rate. We only need to do this every half an hour + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityCurrentRate for '{self._mpan}/{self._serial_number}'") + + rate_information = get_current_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, current) + + if rate_information is not None: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "valid_from": rate_information["current_rate"]["valid_from"], + "valid_to": rate_information["current_rate"]["valid_to"], + "is_capped": rate_information["current_rate"]["is_capped"], + "is_intelligent_adjusted": rate_information["current_rate"]["is_intelligent_adjusted"], + "current_day_min_rate": rate_information["min_rate_today"], + "current_day_max_rate": rate_information["max_rate_today"], + "current_day_average_rate": rate_information["average_rate_today"], + "all_rates": rate_information["all_rates"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["current_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "valid_from": None, + "valid_to": None, + "is_capped": None, + "is_intelligent_adjusted": None, + "current_day_min_rate": None, + "current_day_max_rate": None, + "current_day_average_rate": None, + "all_rates": [], + "applicable_rates": [], + } + + self._state = None + + if self._electricity_price_cap is not None: + self._attributes["price_cap"] = self._electricity_price_cap + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/next_rate.py b/custom_components/octopus_energy/electricity/next_rate.py new file mode 100644 index 00000000..8d4a3fbd --- /dev/null +++ b/custom_components/octopus_energy/electricity/next_rate.py @@ -0,0 +1,129 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyElectricitySensor) +from ..utils.rate_information import (get_next_rate_information) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the next rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._last_updated = None + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_next_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Next Rate" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the next rate for the sensor.""" + # Find the next rate. We only need to do this every half an hour + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityNextRate for '{self._mpan}/{self._serial_number}'") + + target = current + rate_information = get_next_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, target) + + if rate_information is not None: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "valid_from": rate_information["next_rate"]["valid_from"], + "valid_to": rate_information["next_rate"]["valid_to"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["next_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "valid_from": None, + "valid_to": None, + "applicable_rates": [], + } + + self._state = None + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityNextRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py new file mode 100644 index 00000000..3e7d7e8c --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py @@ -0,0 +1,162 @@ +import logging +from datetime import datetime +from ..statistics.consumption import async_import_external_statistics_from_consumption + +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import (utcnow) + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._tariff_code = tariff_code + self._last_reset = None + self._hass = hass + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + return self._state + + @property + def should_poll(self) -> bool: + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption for '{self._mpan}/{self._serial_number}'...") + + await async_import_external_statistics_from_consumption( + self._hass, + f"electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption", + self.name, + consumption_and_cost["charges"], + rate_data, + ENERGY_KILO_WATT_HOUR, + "consumption" + ) + + self._state = consumption_and_cost["total_consumption"] + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "total": consumption_and_cost["total_consumption"], + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "consumption": charge["consumption"] + }, consumption_and_cost["charges"])) + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py new file mode 100644 index 00000000..0e766599 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py @@ -0,0 +1,128 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading during off peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._tariff_code = tariff_code + self._last_reset = None + self._hass = hass + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Consumption (Off Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption off peak for '{self._mpan}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption_off_peak"] if "total_consumption_off_peak" in consumption_and_cost else 0 + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py new file mode 100644 index 00000000..46e5a9c0 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py @@ -0,0 +1,128 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading during peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._tariff_code = tariff_code + self._last_reset = None + self._hass = hass + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Consumption (Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption peak for '{self._mpan}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption_peak"] if "total_consumption_peak" in consumption_and_cost else 0 + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py new file mode 100644 index 00000000..daa9d5fe --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py @@ -0,0 +1,165 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +from ..statistics.cost import async_import_external_statistics_from_cost + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + @property + def should_poll(self): + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption cost for '{self._mpan}/{self._serial_number}'...") + await async_import_external_statistics_from_cost( + self._hass, + f"electricity_{self._serial_number}_{self._mpan}_previous_accumulative_cost", + self.name, + consumption_and_cost["charges"], + rate_data, + "GBP", + "consumption" + ) + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption"]} kWh', + "consumption_raw": charge["consumption"], + "cost": f'£{charge["cost"]}', + "cost_raw": charge["cost"], + }, consumption_and_cost["charges"])) + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py new file mode 100644 index 00000000..c711c552 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py @@ -0,0 +1,125 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost during off peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost_off_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Cost (Off Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption cost off peak for '{self._mpan}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost_off_peak"] if "total_cost_off_peak" in consumption_and_cost else 0 + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCostOffPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py new file mode 100644 index 00000000..084a4939 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py @@ -0,0 +1,175 @@ +import logging +from datetime import (datetime) + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +from ..api_client import (OctopusEnergyApiClient) + +from ..const import (DOMAIN) + +from . import get_electricity_tariff_override_key + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCostOverride(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost for a different tariff.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + self._client = client + + self._state = None + self._last_reset = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost_override" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Cost Override" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + @property + def should_poll(self): + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + + tariff_override_key = get_electricity_tariff_override_key(self._serial_number, self._mpan) + + is_old_data = self._last_reset is None or self._last_reset < consumption_data[-1]["interval_end"] + is_tariff_present = tariff_override_key in self._hass.data[DOMAIN] + has_tariff_changed = is_tariff_present and self._hass.data[DOMAIN][tariff_override_key] != self._tariff_code + + if (consumption_data is not None and len(consumption_data) > 0 and is_tariff_present and (is_old_data or has_tariff_changed)): + _LOGGER.debug(f"Calculating previous electricity consumption cost override for '{self._mpan}/{self._serial_number}'...") + + tariff_override = self._hass.data[DOMAIN][tariff_override_key] + period_from = consumption_data[0]["interval_start"] + period_to = consumption_data[-1]["interval_end"] + rate_data = await self._client.async_get_electricity_rates(tariff_override, self._is_smart_meter, period_from, period_to) + standing_charge = await self._client.async_get_electricity_standing_charge(tariff_override, period_from, period_to) + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge["value_inc_vat"] if standing_charge is not None else None, + None if has_tariff_changed else self._last_reset, + tariff_override, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + self._tariff_code = tariff_override + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption cost override for '{self._mpan}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption"]} kWh', + "cost": charge["cost"] + }, consumption_and_cost["charges"])) + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCostOverride state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py new file mode 100644 index 00000000..9c849ea3 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py @@ -0,0 +1,112 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.components.text import TextEntity + +from homeassistant.helpers.restore_state import RestoreEntity + +from homeassistant.helpers.entity import generate_entity_id, DeviceInfo + +from ..const import (DOMAIN, REGEX_TARIFF_PARTS) + +from . import get_electricity_tariff_override_key + +from ..utils.tariff_check import check_tariff_override_valid + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride(TextEntity, RestoreEntity): + """Sensor for the tariff for the previous days accumulative electricity cost looking at a different tariff.""" + + _attr_pattern = REGEX_TARIFF_PARTS + + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + + self._point = point + self._meter = meter + + self._mpan = point["mpan"] + self._serial_number = meter["serial_number"] + self._is_export = meter["is_export"] + self._is_smart_meter = meter["is_smart_meter"] + self._export_id_addition = "_export" if self._is_export == True else "" + self._export_name_addition = " Export" if self._is_export == True else "" + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter + } + + self.entity_id = generate_entity_id("text.{}", self.unique_id, hass=hass) + + self._hass = hass + + self._client = client + self._tariff_code = tariff_code + self._attr_native_value = tariff_code + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"electricity_{self._serial_number}_{self._mpan}")}, + name=f"Electricity Meter{self._export_name_addition}", + connections=set(), + manufacturer=self._meter["manufacturer"], + model=self._meter["model"], + sw_version=self._meter["firmware"] + ) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost_override_tariff" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Cost Override Tariff" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + async def async_set_value(self, value: str) -> None: + """Update the value.""" + result = await check_tariff_override_valid(self._client, self._tariff_code, value) + if (result is not None): + raise Exception(result) + + self._attr_native_value = value + self._hass.data[DOMAIN][get_electricity_tariff_override_key(self._serial_number, self._mpan)] = value + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None: + if state.state is not None: + self._attr_native_value = state.state + self._attr_state = state.state + self._hass.data[DOMAIN][get_electricity_tariff_override_key(self._serial_number, self._mpan)] = self._attr_native_value + + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride state: {self._attr_state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py new file mode 100644 index 00000000..2c05fdd1 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py @@ -0,0 +1,125 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_electricity_consumption_and_cost, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost during peak hours.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost_peak" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Cost (Peak)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous electricity consumption cost peak for '{self._mpan}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost_peak"] if "total_cost_peak" in consumption_and_cost else 0 + + self._attributes["last_calculated_timestamp"] = consumption_and_cost["last_calculated_timestamp"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCostPeak state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/previous_rate.py b/custom_components/octopus_energy/electricity/previous_rate.py new file mode 100644 index 00000000..8d2808bc --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_rate.py @@ -0,0 +1,129 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyElectricitySensor) +from ..utils.rate_information import (get_previous_rate_information) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._state = None + self._last_updated = None + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Rate" + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the previous rate.""" + # Find the previous rate. We only need to do this every half an hour + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityPreviousRate for '{self._mpan}/{self._serial_number}'") + + target = current + rate_information = get_previous_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, target) + + if rate_information is not None: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "valid_from": rate_information["previous_rate"]["valid_from"], + "valid_to": rate_information["previous_rate"]["valid_to"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["previous_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "valid_from": None, + "valid_to": None, + "applicable_rates": [], + } + + self._state = None + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityPreviousRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/standing_charge.py b/custom_components/octopus_energy/electricity/standing_charge.py new file mode 100644 index 00000000..e1dc3b2f --- /dev/null +++ b/custom_components/octopus_energy/electricity/standing_charge.py @@ -0,0 +1,90 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.components.sensor import ( + SensorDeviceClass +) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentStandingCharge(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._tariff_code = tariff_code + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_standing_charge' + + @property + def name(self): + """Name of the sensor.""" + return f'Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Standing Charge' + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the latest electricity standing charge""" + _LOGGER.debug('Updating OctopusEnergyElectricityCurrentStandingCharge') + + standard_charge_result = self.coordinator.data[self._mpan] if self.coordinator is not None and self.coordinator.data is not None and self._mpan in self.coordinator.data else None + + if standard_charge_result is not None: + self._latest_date = standard_charge_result["valid_from"] + self._state = standard_charge_result["value_inc_vat"] / 100 + + # Adjust our period, as our gas only changes on a daily basis + self._attributes["valid_from"] = standard_charge_result["valid_from"] + self._attributes["valid_to"] = standard_charge_result["valid_to"] + else: + self._state = None + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentStandingCharge state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/__init__.py b/custom_components/octopus_energy/gas/__init__.py new file mode 100644 index 00000000..8c5ecef9 --- /dev/null +++ b/custom_components/octopus_energy/gas/__init__.py @@ -0,0 +1,96 @@ +def __get_interval_end(item): + return item["interval_end"] + +def __sort_consumption(consumption_data): + sorted = consumption_data.copy() + sorted.sort(key=__get_interval_end) + return sorted + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_m3_to_kwh(value, calorific_value): + kwh_value = value * 1.02264 # Volume correction factor + kwh_value = kwh_value * calorific_value # Calorific value + return round(kwh_value / 3.6, 3) # kWh Conversion factor + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_kwh_to_m3(value, calorific_value): + m3_value = value * 3.6 # kWh Conversion factor + m3_value = m3_value / calorific_value # Calorific value + return round(m3_value / 1.02264, 3) # Volume correction factor + +def calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + last_reset, + tariff_code, + consumption_units, + calorific_value, + minimum_consumption_records = 0 + ): + if (consumption_data is not None and len(consumption_data) >= minimum_consumption_records and rate_data is not None and len(rate_data) > 0 and standing_charge is not None): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_reset == None or last_reset < sorted_consumption_data[0]["interval_start"]): + + charges = [] + total_cost_in_pence = 0 + total_consumption_m3 = 0 + total_consumption_kwh = 0 + for consumption in sorted_consumption_data: + current_consumption_m3 = 0 + current_consumption_kwh = 0 + + current_consumption = consumption["consumption"] + + if consumption_units == "m³": + current_consumption_m3 = current_consumption + current_consumption_kwh = convert_m3_to_kwh(current_consumption, calorific_value) + else: + current_consumption_m3 = convert_kwh_to_m3(current_consumption, calorific_value) + current_consumption_kwh = current_consumption + + total_consumption_m3 = total_consumption_m3 + current_consumption_m3 + total_consumption_kwh = total_consumption_kwh + current_consumption_kwh + + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + + try: + rate = next(r for r in rate_data if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {tariff_code}") + + value = rate["value_inc_vat"] + cost = (value * current_consumption_kwh) + total_cost_in_pence = total_cost_in_pence + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": value, + "consumption_m3": current_consumption_m3, + "consumption_kwh": current_consumption_kwh, + "cost": round(cost / 100, 2) + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standing_charge) / 100, 2) + last_reset = sorted_consumption_data[0]["interval_start"] + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "standing_charge": standing_charge, + "total_cost_without_standing_charge": total_cost, + "total_cost": total_cost_plus_standing_charge, + "total_consumption_m3": total_consumption_m3, + "total_consumption_kwh": total_consumption_kwh, + "last_reset": last_reset, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + +def get_gas_tariff_override_key(serial_number: str, mprn: str) -> str: + return f'gas_previous_consumption_tariff_{serial_number}_{mprn}' \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/base.py b/custom_components/octopus_energy/gas/base.py new file mode 100644 index 00000000..7430d6a6 --- /dev/null +++ b/custom_components/octopus_energy/gas/base.py @@ -0,0 +1,38 @@ +from homeassistant.core import HomeAssistant + +from homeassistant.components.sensor import ( + SensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from homeassistant.helpers.entity import generate_entity_id, DeviceInfo + +from ..const import ( + DOMAIN, +) + +class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor""" + self._point = point + self._meter = meter + + self._mprn = point["mprn"] + self._serial_number = meter["serial_number"] + self._is_smart_meter = meter["is_smart_meter"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number + } + + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"gas_{self._serial_number}_{self._mprn}")}, + name="Gas Meter", + connections=set(), + manufacturer=self._meter["manufacturer"], + model=self._meter["model"], + sw_version=self._meter["firmware"] + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/current_accumulative_consumption.py b/custom_components/octopus_energy/gas/current_accumulative_consumption.py new file mode 100644 index 00000000..6c6ebd25 --- /dev/null +++ b/custom_components/octopus_energy/gas/current_accumulative_consumption.py @@ -0,0 +1,126 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyGasSensor) + +from ..gas import calculate_gas_consumption_and_cost + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current accumulative gas consumption.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_current_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Current Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:fire" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the current days accumulative consumption""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mprn] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mprn in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mprn]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mprn in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mprn] else None + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code, + "kwh", # Our current sensor always reports in kwh + self._calorific_value + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous gas consumption for '{self._mprn}/{self._serial_number}'...") + + self._state = consumption_and_cost["total_consumption_kwh"] + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "total": consumption_and_cost["total_consumption_kwh"], + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "consumption": charge["consumption_kwh"] + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeGasConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/current_accumulative_cost.py b/custom_components/octopus_energy/gas/current_accumulative_cost.py new file mode 100644 index 00000000..788e1bf7 --- /dev/null +++ b/custom_components/octopus_energy/gas/current_accumulative_cost.py @@ -0,0 +1,147 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_gas_consumption_and_cost, +) + +from .base import (OctopusEnergyGasSensor) + +from ..statistics.cost import async_import_external_statistics_from_cost + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current days accumulative gas cost.""" + + def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + self._rates_coordinator = rates_coordinator + self._standing_charge_coordinator = standing_charge_coordinator + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_current_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Current Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the currently calculated state""" + consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None + rate_data = self._rates_coordinator.data[self._mprn] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mprn in self._rates_coordinator.data else None + standing_charge = self._standing_charge_coordinator.data[self._mprn]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mprn in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mprn] else None + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + None, # We want to always recalculate + self._tariff_code, + "kwh", + self._calorific_value + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated current gas consumption cost for '{self._mprn}/{self._serial_number}'...") + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption_kwh"]} kWh', + "consumption_raw": charge["consumption_kwh"], + "cost": f'£{charge["cost"]}', + "cost_raw": charge["cost"], + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyCurrentAccumulativeGasCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/current_consumption.py b/custom_components/octopus_energy/gas/current_consumption.py new file mode 100644 index 00000000..9c71344d --- /dev/null +++ b/custom_components/octopus_energy/gas/current_consumption.py @@ -0,0 +1,109 @@ +from homeassistant.util.dt import (now) +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyGasSensor) + +from ..utils.consumption import (get_current_consumption_delta, get_total_consumption) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current gas consumption.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._state = None + self._latest_date = None + self._previous_total_consumption = None + self._attributes = { + "last_updated_timestamp": None + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_current_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Current Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:fire" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """The current consumption for the meter.""" + _LOGGER.debug('Updating OctopusEnergyCurrentGasConsumption') + consumption_result = self.coordinator.data if self.coordinator is not None else None + + current_date = now() + if (consumption_result is not None): + total_consumption = get_total_consumption(consumption_result) + self._state = get_current_consumption_delta(current_date, + total_consumption, + self._attributes["last_updated_timestamp"] if self._attributes["last_updated_timestamp"] is not None else current_date, + self._previous_total_consumption) + if (self._state is not None): + self._latest_date = current_date + self._attributes["last_updated_timestamp"] = current_date + + # Store the total consumption ready for the next run + self._previous_total_consumption = total_consumption + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentGasConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/current_rate.py b/custom_components/octopus_energy/gas/current_rate.py new file mode 100644 index 00000000..462bf94c --- /dev/null +++ b/custom_components/octopus_energy/gas/current_rate.py @@ -0,0 +1,138 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyGasSensor) +from ..utils.rate_information import get_current_rate_information + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasCurrentRate(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, gas_price_cap): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._tariff_code = tariff_code + self._gas_price_cap = gas_price_cap + + self._state = None + self._last_updated = None + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "all_rates": [], + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + "is_capped": None, + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_current_rate'; + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Current Rate' + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the current rate for the sensor.""" + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyGasCurrentRate for '{self._mprn}/{self._serial_number}'") + + rate_information = get_current_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + + if rate_information is not None: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "valid_from": rate_information["current_rate"]["valid_from"], + "valid_to": rate_information["current_rate"]["valid_to"], + "is_capped": rate_information["current_rate"]["is_capped"], + "all_rates": rate_information["all_rates"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["current_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "tariff": self._tariff_code, + "valid_from": None, + "valid_to": None, + "is_capped": None, + "all_rates": [], + "applicable_rates": [], + } + + self._state = None + + if self._gas_price_cap is not None: + self._attributes["price_cap"] = self._gas_price_cap + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasCurrentRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/next_rate.py b/custom_components/octopus_energy/gas/next_rate.py new file mode 100644 index 00000000..6a2d21be --- /dev/null +++ b/custom_components/octopus_energy/gas/next_rate.py @@ -0,0 +1,124 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyGasSensor) +from ..utils.rate_information import get_next_rate_information + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasNextRate(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the next rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._state = None + self._last_updated = None + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "all_rates": [], + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_next_rate' + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Next Rate' + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the next rate for the sensor.""" + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyGasNextRate for '{self._mprn}/{self._serial_number}'") + + rate_information = get_next_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + + if rate_information is not None: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "valid_from": rate_information["next_rate"]["valid_from"], + "valid_to": rate_information["next_rate"]["valid_to"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["next_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "valid_from": None, + "valid_to": None, + "applicable_rates": [], + } + + self._state = None + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasNextRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py new file mode 100644 index 00000000..d33828a2 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py @@ -0,0 +1,167 @@ +import logging +from datetime import datetime +from ..statistics.consumption import async_import_external_statistics_from_consumption + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + VOLUME_CUBIC_METERS +) + +from . import ( + calculate_gas_consumption_and_cost, +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + self._native_consumption_units = meter["consumption_units"] + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.GAS + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return VOLUME_CUBIC_METERS + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:fire" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + return self._state + + @property + def should_poll(self) -> bool: + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous gas consumption for '{self._mprn}/{self._serial_number}'...") + + await async_import_external_statistics_from_consumption( + self._hass, + f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption", + self.name, + consumption_and_cost["charges"], + rate_data, + VOLUME_CUBIC_METERS, + "consumption_m3", + False + ) + + self._state = consumption_and_cost["total_consumption_m3"] + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_estimated": self._native_consumption_units != "m³", + "total_kwh": consumption_and_cost["total_consumption_kwh"], + "total_m3": consumption_and_cost["total_consumption_m3"], + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "consumption_m3": charge["consumption_m3"], + "consumption_kwh": charge["consumption_kwh"] + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py new file mode 100644 index 00000000..6c6d037f --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py @@ -0,0 +1,165 @@ +import logging +from datetime import datetime +from ..statistics.consumption import async_import_external_statistics_from_consumption + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from . import ( + calculate_gas_consumption_and_cost, +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasConsumptionKwh(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas consumption in kwh.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + self._native_consumption_units = meter["consumption_units"] + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption_kwh" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption (kWh)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + return self._state + + @property + def should_poll(self) -> bool: + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous gas consumption for '{self._mprn}/{self._serial_number}'...") + + await async_import_external_statistics_from_consumption( + self._hass, + f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption_kwh", + self.name, + consumption_and_cost["charges"], + rate_data, + ENERGY_KILO_WATT_HOUR, + "consumption_kwh", + False + ) + + self._state = consumption_and_cost["total_consumption_kwh"] + self._last_reset = consumption_and_cost["last_reset"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_estimated": self._native_consumption_units == "m³", + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "consumption_m3": charge["consumption_m3"], + "consumption_kwh": charge["consumption_kwh"] + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumptionKwh state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost.py b/custom_components/octopus_energy/gas/previous_accumulative_cost.py new file mode 100644 index 00000000..96c74a73 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost.py @@ -0,0 +1,170 @@ +import logging +from datetime import datetime + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from . import ( + calculate_gas_consumption_and_cost, +) + +from .base import (OctopusEnergyGasSensor) + +from ..statistics.cost import async_import_external_statistics_from_cost + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas cost.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._tariff_code = tariff_code + self._native_consumption_units = meter["consumption_units"] + + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return self._is_smart_meter + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + @property + def should_poll(self): + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator is not None and self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator is not None and self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous gas consumption cost for '{self._mprn}/{self._serial_number}'...") + + await async_import_external_statistics_from_cost( + self._hass, + f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost", + self.name, + consumption_and_cost["charges"], + rate_data, + "GBP", + "consumption_kwh", + False + ) + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption_kwh"]} kWh', + "consumption_raw": charge["consumption_kwh"], + "cost": f'£{charge["cost"]}', + "cost_raw": charge["cost"], + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py new file mode 100644 index 00000000..5c4bf079 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py @@ -0,0 +1,176 @@ +import logging +from datetime import (datetime) + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from . import ( + calculate_gas_consumption_and_cost, + get_gas_tariff_override_key, +) + +from ..api_client import (OctopusEnergyApiClient) + +from .base import (OctopusEnergyGasSensor) + +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasCostOverride(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas cost for a different tariff.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._client = client + self._tariff_code = tariff_code + self._native_consumption_units = meter["consumption_units"] + + self._state = None + self._last_reset = None + self._calorific_value = calorific_value + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost_override" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Cost Override" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + @property + def should_poll(self): + return True + + async def async_update(self): + await super().async_update() + + if not self.enabled: + return + + consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None + + tariff_override_key = get_gas_tariff_override_key(self._serial_number, self._mprn) + is_old_data = self._last_reset is None or self._last_reset < consumption_data[-1]["interval_end"] + is_tariff_present = tariff_override_key in self._hass.data[DOMAIN] + has_tariff_changed = is_tariff_present and self._hass.data[DOMAIN][tariff_override_key] != self._tariff_code + + if (consumption_data is not None and len(consumption_data) > 0 and is_tariff_present and (is_old_data or has_tariff_changed)): + _LOGGER.debug(f"Calculating previous gas consumption cost override for '{self._mprn}/{self._serial_number}'...") + + tariff_override = self._hass.data[DOMAIN][tariff_override_key] + period_from = consumption_data[0]["interval_start"] + period_to = consumption_data[-1]["interval_end"] + rate_data = await self._client.async_get_gas_rates(tariff_override, period_from, period_to) + standing_charge = await self._client.async_get_gas_standing_charge(tariff_override, period_from, period_to) + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge["value_inc_vat"] if standing_charge is not None else None, + None if has_tariff_changed else self._last_reset, + tariff_override, + self._native_consumption_units, + self._calorific_value, + # During BST, two records are returned before the rest of the data is available + 3 + ) + + self._tariff_code = tariff_override + + if (consumption_and_cost is not None): + _LOGGER.debug(f"Calculated previous gas consumption cost override for '{self._mprn}/{self._serial_number}'...") + + self._last_reset = consumption_and_cost["last_reset"] + self._state = consumption_and_cost["total_cost"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_and_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_and_cost["total_cost_without_standing_charge"]}', + "total": f'£{consumption_and_cost["total_cost"]}', + "last_calculated_timestamp": consumption_and_cost["last_calculated_timestamp"], + "charges": list(map(lambda charge: { + "from": charge["from"], + "to": charge["to"], + "rate": f'{charge["rate"]}p', + "consumption": f'{charge["consumption_kwh"]} kWh', + "cost": charge["cost"] + }, consumption_and_cost["charges"])), + "calorific_value": self._calorific_value + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if x == "last_reset": + self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasCostOverride state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py b/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py new file mode 100644 index 00000000..ff5c276f --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py @@ -0,0 +1,107 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.components.text import TextEntity + +from homeassistant.helpers.restore_state import RestoreEntity + +from homeassistant.helpers.entity import generate_entity_id, DeviceInfo + +from ..const import (DOMAIN, REGEX_TARIFF_PARTS) + +from . import get_gas_tariff_override_key + +from ..utils.tariff_check import check_tariff_override_valid + +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasCostTariffOverride(TextEntity, RestoreEntity): + """Sensor for the tariff for the previous days accumulative gas cost looking at a different tariff.""" + + _attr_pattern = REGEX_TARIFF_PARTS + + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + + self._point = point + self._meter = meter + + self._mprn = point["mprn"] + self._serial_number = meter["serial_number"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number + } + + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + self._hass = hass + + self._client = client + self._tariff_code = tariff_code + self._attr_native_value = tariff_code + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"gas_{self._serial_number}_{self._mprn}")}, + name="Gas Meter", + connections=set(), + manufacturer=self._meter["manufacturer"], + model=self._meter["model"], + sw_version=self._meter["firmware"] + ) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost_override_tariff" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Cost Override Tariff" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + async def async_set_value(self, value: str) -> None: + """Update the value.""" + result = await check_tariff_override_valid(self._client, self._tariff_code, value) + if (result is not None): + raise Exception(result) + + self._attr_native_value = value + self._hass.data[DOMAIN][get_gas_tariff_override_key(self._serial_number, self._mprn)] = value + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None: + + if state.state is not None: + self._attr_native_value = state.state + self._attr_state = state.state + self._hass.data[DOMAIN][get_gas_tariff_override_key(self._serial_number, self._mprn)] = self._attr_native_value + + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasCostTariffOverride state: {self._attr_state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/previous_rate.py b/custom_components/octopus_energy/gas/previous_rate.py new file mode 100644 index 00000000..53721a32 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_rate.py @@ -0,0 +1,124 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyGasSensor) +from ..utils.rate_information import get_previous_rate_information + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasPreviousRate(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._state = None + self._last_updated = None + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "all_rates": [], + "applicable_rates": [], + "valid_from": None, + "valid_to": None, + } + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_rate' + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Previous Rate' + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the previous rate for the sensor.""" + current = now() + if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyGasPreviousRate for '{self._mprn}/{self._serial_number}'") + + rate_information = get_previous_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + + if rate_information is not None: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "valid_from": rate_information["previous_rate"]["valid_from"], + "valid_to": rate_information["previous_rate"]["valid_to"], + "applicable_rates": rate_information["applicable_rates"], + } + + self._state = rate_information["previous_rate"]["value_inc_vat"] / 100 + else: + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_smart_meter": self._is_smart_meter, + "valid_from": None, + "valid_to": None, + "applicable_rates": [], + } + + self._state = None + + self._last_updated = current + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasPreviousRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/standing_charge.py b/custom_components/octopus_energy/gas/standing_charge.py new file mode 100644 index 00000000..88e077d5 --- /dev/null +++ b/custom_components/octopus_energy/gas/standing_charge.py @@ -0,0 +1,96 @@ +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasCurrentStandingCharge(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._tariff_code = tariff_code + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_current_standing_charge'; + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Current Standing Charge' + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the latest gas standing charge""" + _LOGGER.debug('Updating OctopusEnergyGasCurrentStandingCharge') + + standard_charge_result = self.coordinator.data[self._mprn] if self.coordinator is not None and self.coordinator.data is not None and self._mprn in self.coordinator.data else None + + if standard_charge_result is not None: + self._latest_date = standard_charge_result["valid_from"] + self._state = standard_charge_result["value_inc_vat"] / 100 + + # Adjust our period, as our gas only changes on a daily basis + self._attributes["valid_from"] = standard_charge_result["valid_from"] + self._attributes["valid_to"] = standard_charge_result["valid_to"] + else: + self._state = None + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasCurrentStandingCharge state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/__init__.py b/custom_components/octopus_energy/intelligent/__init__.py new file mode 100644 index 00000000..4848617b --- /dev/null +++ b/custom_components/octopus_energy/intelligent/__init__.py @@ -0,0 +1,143 @@ +import logging +from datetime import (datetime, timedelta, time) + +from homeassistant.util.dt import (utcnow, parse_datetime) + +from homeassistant.helpers import storage + +from ..utils import get_tariff_parts + +from ..const import DOMAIN + +mock_intelligent_data_key = "MOCK_INTELLIGENT_DATA" + +_LOGGER = logging.getLogger(__name__) + +async def async_mock_intelligent_data(hass): + mock_data = hass.data[DOMAIN][mock_intelligent_data_key] if mock_intelligent_data_key in hass.data[DOMAIN] else None + if mock_data is None: + store = storage.Store(hass, "1", "octopus_energy.mock_intelligent_responses") + hass.data[DOMAIN][mock_intelligent_data_key] = await store.async_load() is not None + + _LOGGER.debug(f'MOCK_INTELLIGENT_DATA: {hass.data[DOMAIN][mock_intelligent_data_key]}') + + return hass.data[DOMAIN][mock_intelligent_data_key] + +def mock_intelligent_dispatches(): + planned = [] + completed = [] + + dispatches = [ + { + "start": utcnow().replace(hour=19, minute=0, second=0, microsecond=0), + "end": utcnow().replace(hour=20, minute=0, second=0, microsecond=0), + "charge_in_kwh": 1, + "source": "smart-charge" + }, + { + "start": utcnow().replace(hour=6, minute=0, second=0, microsecond=0), + "end": utcnow().replace(hour=7, minute=0, second=0, microsecond=0), + "charge_in_kwh": 1.2, + "source": "smart-charge" + }, + { + "start": utcnow().replace(hour=7, minute=0, second=0, microsecond=0), + "end": utcnow().replace(hour=8, minute=0, second=0, microsecond=0), + "charge_in_kwh": 4.6, + "source": "smart-charge" + } + ] + + for dispatch in dispatches: + if (dispatch["end"] > utcnow()): + planned.append(dispatch) + else: + completed.append(dispatch) + + return { + "planned": planned, + "completed": completed + } + +def mock_intelligent_settings(): + return { + "smart_charge": True, + "charge_limit_weekday": 90, + "charge_limit_weekend": 80, + "ready_time_weekday": time(7,30), + "ready_time_weekend": time(9,10), + } + +def mock_intelligent_device(): + return { + "krakenflexDeviceId": "1", + "vehicleMake": "Tesla", + "vehicleModel": "Model Y", + "vehicleBatterySizeInKwh": 75.0, + "chargePointMake": "MyEnergi", + "chargePointModel": "Zappi", + "chargePointPowerInKw": 6.5 + } + +def is_intelligent_tariff(tariff_code: str): + parts = get_tariff_parts(tariff_code.upper()) + + return parts is not None and "INTELLI" in parts.product_code + +def __get_dispatch(rate, dispatches, expected_source: str): + for dispatch in dispatches: + if (expected_source is None or dispatch["source"] == expected_source) and dispatch["start"] <= rate["valid_from"] and dispatch["end"] >= rate["valid_to"]: + return dispatch + + return None + +def adjust_intelligent_rates(rates, planned_dispatches, completed_dispatches): + off_peak_rate = min(rates, key = lambda x: x["value_inc_vat"]) + adjusted_rates = [] + + for rate in rates: + if rate["value_inc_vat"] == off_peak_rate["value_inc_vat"]: + adjusted_rates.append(rate) + continue + + if __get_dispatch(rate, planned_dispatches, "smart-charge") is not None or __get_dispatch(rate, completed_dispatches, None) is not None: + adjusted_rates.append({ + "valid_from": rate["valid_from"], + "valid_to": rate["valid_to"], + "value_inc_vat": off_peak_rate["value_inc_vat"], + "is_capped": rate["is_capped"] if "is_capped" in rate else False, + "is_intelligent_adjusted": True + }) + else: + adjusted_rates.append(rate) + + return adjusted_rates + +def is_in_planned_dispatch(current_date: datetime, dispatches) -> bool: + for event in dispatches: + if (event["start"] <= current_date and event["end"] >= current_date): + return True + + return False + +def is_in_bump_charge(current_date: datetime, dispatches) -> bool: + for event in dispatches: + if (event["source"] == "bump-charge" and event["start"] <= current_date and event["end"] >= current_date): + return True + + return False + +def clean_previous_dispatches(time: datetime, dispatches): + min_time = (time - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + + new_dispatches = {} + for dispatch in dispatches: + # Some of our dispatches will be strings when loaded from cache, so convert + start = parse_datetime(dispatch["start"]) if type(dispatch["start"]) == str else dispatch["start"] + end = parse_datetime(dispatch["end"]) if type(dispatch["end"]) == str else dispatch["end"] + if (start >= min_time): + new_dispatches[(start, end)] = dispatch + new_dispatches[(start, end)]["start"] = start + new_dispatches[(start, end)]["end"] = end + + return list(new_dispatches.values()) \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/base.py b/custom_components/octopus_energy/intelligent/base.py new file mode 100644 index 00000000..d656134a --- /dev/null +++ b/custom_components/octopus_energy/intelligent/base.py @@ -0,0 +1,20 @@ +from homeassistant.helpers.entity import DeviceInfo + +from ..const import ( + DOMAIN, +) + +class OctopusEnergyIntelligentSensor: + def __init__(self, device): + """Init sensor""" + + self._device = device + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, self._device["krakenflexDeviceId"] if "krakenflexDeviceId" in self._device and self._device["krakenflexDeviceId"] is not None else "charger-1") + }, + name="Charger", + connections=set(), + manufacturer=self._device["chargePointMake"], + model=self._device["chargePointModel"] + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/bump_charge.py b/custom_components/octopus_energy/intelligent/bump_charge.py new file mode 100644 index 00000000..41000109 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/bump_charge.py @@ -0,0 +1,80 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.switch import SwitchEntity +from homeassistant.util.dt import (utcnow) + +from .base import OctopusEnergyIntelligentSensor +from ..api_client import OctopusEnergyApiClient +from . import is_in_bump_charge + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentBumpCharge(CoordinatorEntity, SwitchEntity, OctopusEnergyIntelligentSensor): + """Switch for turning intelligent bump charge on and off.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = False + self._last_updated = None + self._client = client + self._account_id = account_id + self._attributes = {} + self.entity_id = generate_entity_id("switch.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_intelligent_bump_charge" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Intelligent Bump Charge" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:ev-plug-type2" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """Determine if the bump charge is on.""" + if self.coordinator is None or self.coordinator.data is None or (self._last_updated is not None and "last_updated" in self.coordinator.data and self._last_updated > self.coordinator.data["last_updated"]): + return self._state + + self._state = is_in_bump_charge(utcnow(), self.coordinator.data["planned"]) + + return self._state + + async def async_turn_on(self): + """Turn on the switch.""" + await self._client.async_turn_on_intelligent_bump_charge( + self._account_id + ) + self._state = True + self._last_updated = utcnow() + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn off the switch.""" + await self._client.async_turn_off_intelligent_bump_charge( + self._account_id + ) + self._state = False + self._last_updated = utcnow() + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/charge_limit.py b/custom_components/octopus_energy/intelligent/charge_limit.py new file mode 100644 index 00000000..18c90410 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/charge_limit.py @@ -0,0 +1,88 @@ +import logging + +from datetime import time + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.number import RestoreNumber, NumberDeviceClass +from homeassistant.util.dt import (utcnow) + +from .base import OctopusEnergyIntelligentSensor +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentChargeLimit(CoordinatorEntity, RestoreNumber, OctopusEnergyIntelligentSensor): + """Sensor for setting the target percentage for car charging.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = None + self._last_updated = None + self._client = client + self._account_id = account_id + self._attributes = {} + self.entity_id = generate_entity_id("number.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_intelligent_charge_limit" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Intelligent Charge Limit" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:battery-charging" + + @property + def device_class(self): + """The type of sensor""" + return NumberDeviceClass.BATTERY + + @property + def native_unit_of_measurement(self): + """The unit of measurement of sensor""" + return "%" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def native_value(self) -> float: + """The value of the charge limit.""" + if self.coordinator is None or self.coordinator.data is None or (self._last_updated is not None and "last_updated" in self.coordinator.data and self._last_updated > self.coordinator.data["last_updated"]): + self._attributes["last_updated_timestamp"] = self._last_updated + return self._state + + self._attributes["last_updated_timestamp"] = self.coordinator.data["last_updated"] + self._state = self.coordinator.data["charge_limit_weekday"] + + return self._state + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._client.async_update_intelligent_car_preferences( + self._account_id, + int(value), + int(value), + self.coordinator.data["ready_time_weekday"] if self.coordinator is not None and self.coordinator.data is not None else time(9,0), + self.coordinator.data["ready_time_weekend"] if self.coordinator is not None and self.coordinator.data is not None else time(9,0), + ) + self._state = value + self._last_updated = utcnow() + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/dispatching.py b/custom_components/octopus_energy/intelligent/dispatching.py new file mode 100644 index 00000000..fa35d084 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/dispatching.py @@ -0,0 +1,94 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from ..intelligent import ( + is_in_planned_dispatch +) + +from .base import OctopusEnergyIntelligentSensor + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentDispatching(CoordinatorEntity, BinarySensorEntity, OctopusEnergyIntelligentSensor, RestoreEntity): + """Sensor for determining if an intelligent is dispatching.""" + + def __init__(self, hass: HomeAssistant, coordinator, device): + """Init sensor.""" + + super().__init__(coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = None + self._attributes = { + "planned_dispatches": [], + "completed_dispatches": [], + "last_updated_timestamp": None, + "vehicle_battery_size_in_kwh": device["vehicleBatterySizeInKwh"], + "charge_point_power_in_kw": device["chargePointPowerInKw"] + } + + self.entity_id = generate_entity_id("binary_sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_intelligent_dispatching" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Intelligent Dispatching" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:power-plug-battery" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """Determine if OE is currently dispatching energy.""" + dispatches = self.coordinator.data if self.coordinator is not None else None + if (dispatches is not None): + self._attributes["planned_dispatches"] = self.coordinator.data["planned"] + self._attributes["completed_dispatches"] = self.coordinator.data["completed"] + + if "last_updated" in self.coordinator.data: + self._attributes["last_updated_timestamp"] = self.coordinator.data["last_updated"] + else: + self._attributes["planned_dispatches"] = [] + self._attributes["completed_dispatches"] = [] + + current_date = now() + self._state = is_in_planned_dispatch(current_date, self._attributes["planned_dispatches"]) + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None: + self._state = state.state + + if (self._state is None): + self._state = False + + _LOGGER.debug(f'Restored OctopusEnergyIntelligentDispatching state: {self._state}') diff --git a/custom_components/octopus_energy/intelligent/ready_time.py b/custom_components/octopus_energy/intelligent/ready_time.py new file mode 100644 index 00000000..99983b12 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/ready_time.py @@ -0,0 +1,77 @@ +import logging +from datetime import time + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.time import TimeEntity +from homeassistant.util.dt import (utcnow) + +from .base import OctopusEnergyIntelligentSensor +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentReadyTime(CoordinatorEntity, TimeEntity, OctopusEnergyIntelligentSensor): + """Sensor for setting the target time to charge the car to the desired percentage.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = time() + self._last_updated = None + self._client = client + self._account_id = account_id + self._attributes = {} + self.entity_id = generate_entity_id("time.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_intelligent_ready_time" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Intelligent Ready Time" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:battery-clock" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def native_value(self) -> time: + """The time that the car should be ready by.""" + if self.coordinator is None or self.coordinator.data is None or (self._last_updated is not None and "last_updated" in self.coordinator.data and self._last_updated > self.coordinator.data["last_updated"]): + self._attributes["last_updated_timestamp"] = self._last_updated + return self._state + + self._attributes["last_updated_timestamp"] = self.coordinator.data["last_updated"] + self._state = self.coordinator.data["ready_time_weekday"] + + return self._state + + async def async_set_value(self, value: time) -> None: + """Set new value.""" + await self._client.async_update_intelligent_car_preferences( + self._account_id, + self.coordinator.data["charge_limit_weekday"] if self.coordinator is not None and self.coordinator.data is not None else 100, + self.coordinator.data["charge_limit_weekend"] if self.coordinator is not None and self.coordinator.data is not None else 100, + value, + value, + ) + self._state = value + self._last_updated = utcnow() + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/intelligent/smart_charge.py b/custom_components/octopus_energy/intelligent/smart_charge.py new file mode 100644 index 00000000..28b87d04 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/smart_charge.py @@ -0,0 +1,79 @@ +import logging + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.switch import SwitchEntity +from homeassistant.util.dt import (utcnow) + +from .base import OctopusEnergyIntelligentSensor +from ..api_client import OctopusEnergyApiClient + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyIntelligentSmartCharge(CoordinatorEntity, SwitchEntity, OctopusEnergyIntelligentSensor): + """Switch for turning intelligent smart charge on and off.""" + + def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyIntelligentSensor.__init__(self, device) + + self._state = False + self._last_updated = None + self._client = client + self._account_id = account_id + self._attributes = {} + self.entity_id = generate_entity_id("switch.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_intelligent_smart_charge" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Intelligent Smart Charge" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:ev-station" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """Determines if smart charge is currently on.""" + if self.coordinator is None or self.coordinator.data is None or (self._last_updated is not None and "last_updated" in self.coordinator.data and self._last_updated > self.coordinator.data["last_updated"]): + return self._state + + self._state = self.coordinator.data["smart_charge"] + + return self._state + + async def async_turn_on(self): + """Turn on the switch.""" + await self._client.async_turn_on_intelligent_smart_charge( + self._account_id + ) + self._state = True + self._last_updated = utcnow() + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn off the switch.""" + await self._client.async_turn_off_intelligent_smart_charge( + self._account_id + ) + self._state = False + self._last_updated = utcnow() + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/manifest.json b/custom_components/octopus_energy/manifest.json new file mode 100644 index 00000000..71e3e6ce --- /dev/null +++ b/custom_components/octopus_energy/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "octopus_energy", + "name": "Octopus Energy", + "codeowners": [ + "@bottlecapdave" + ], + "config_flow": true, + "dependencies": [ + "repairs", + "recorder" + ], + "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/", + "homekit": {}, + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", + "ssdp": [], + "version": "8.2.0", + "zeroconf": [] +} \ No newline at end of file diff --git a/custom_components/octopus_energy/number.py b/custom_components/octopus_energy/number.py new file mode 100644 index 00000000..a0fcc67f --- /dev/null +++ b/custom_components/octopus_energy/number.py @@ -0,0 +1,61 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) + +from .intelligent.charge_limit import OctopusEnergyIntelligentChargeLimit +from .api_client import OctopusEnergyApiClient +from .intelligent import async_mock_intelligent_data, is_intelligent_tariff, mock_intelligent_device +from .utils import get_active_tariff_code + +from .const import ( + DATA_ACCOUNT_ID, + DATA_CLIENT, + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_INTELLIGENT_SETTINGS_COORDINATOR, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_intelligent_sensors(hass, async_add_entities) + + return True + +async def async_setup_intelligent_sensors(hass, async_add_entities): + _LOGGER.debug('Setting up intelligent sensors') + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + has_intelligent_tariff = False + if len(account_info["electricity_meter_points"]) > 0: + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if is_intelligent_tariff(tariff_code): + has_intelligent_tariff = True + break + + should_mock_intelligent_data = await async_mock_intelligent_data(hass) + if has_intelligent_tariff or should_mock_intelligent_data: + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + if should_mock_intelligent_data: + device = mock_intelligent_device() + else: + device = await client.async_get_intelligent_device(account_id) + + async_add_entities([ + OctopusEnergyIntelligentChargeLimit(hass, coordinator, client, device, account_id), + ], True) \ No newline at end of file diff --git a/custom_components/octopus_energy/saving_sessions/__init__.py b/custom_components/octopus_energy/saving_sessions/__init__.py new file mode 100644 index 00000000..7ecf5681 --- /dev/null +++ b/custom_components/octopus_energy/saving_sessions/__init__.py @@ -0,0 +1,28 @@ +def current_saving_sessions_event(current_date, events): + current_event = None + + if events is not None: + for event in events: + if (event["start"] <= current_date and event["end"] >= current_date): + current_event = { + "start": event["start"], + "end": event["end"], + "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 + } + break + + return current_event + +def get_next_saving_sessions_event(current_date, events): + next_event = None + + if events is not None: + for event in events: + if event["start"] > current_date and (next_event == None or event["start"] < next_event["start"]): + next_event = { + "start": event["start"], + "end": event["end"], + "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 + } + + return next_event \ No newline at end of file diff --git a/custom_components/octopus_energy/saving_sessions/points.py b/custom_components/octopus_energy/saving_sessions/points.py new file mode 100644 index 00000000..09b81d6b --- /dev/null +++ b/custom_components/octopus_energy/saving_sessions/points.py @@ -0,0 +1,78 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorEntity, + SensorStateClass +) +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergySavingSessionPoints(CoordinatorEntity, SensorEntity, RestoreEntity): + """Sensor for determining saving session points""" + + def __init__(self, hass: HomeAssistant, coordinator): + """Init sensor.""" + + super().__init__(coordinator) + + self._state = None + self._attributes = {} + + self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_saving_session_points" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Saving Session Points" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:leaf" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL_INCREASING + + @property + def state(self): + """Update the points based on data.""" + saving_session = self.coordinator.data if self.coordinator is not None else None + if (saving_session is not None and "points" in saving_session): + self._state = saving_session["points"] + else: + self._state = 0 + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergySavingSessionPoints state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/saving_sessions/saving_sessions.py b/custom_components/octopus_energy/saving_sessions/saving_sessions.py new file mode 100644 index 00000000..c73dc819 --- /dev/null +++ b/custom_components/octopus_energy/saving_sessions/saving_sessions.py @@ -0,0 +1,105 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.util.dt import (now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from . import ( + current_saving_sessions_event, + get_next_saving_sessions_event +) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergySavingSessions(CoordinatorEntity, BinarySensorEntity, RestoreEntity): + """Sensor for determining if a saving session is active.""" + + def __init__(self, hass: HomeAssistant, coordinator): + """Init sensor.""" + + super().__init__(coordinator) + + self._state = None + self._events = [] + self._attributes = { + "joined_events": [], + "next_joined_event_start": None + } + + self.entity_id = generate_entity_id("binary_sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_saving_sessions" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Saving Session" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:leaf" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """Determine if the user is in a saving session.""" + saving_session = self.coordinator.data if self.coordinator is not None else None + if (saving_session is not None and "events" in saving_session): + self._events = saving_session["events"] + else: + self._events = [] + + self._attributes = { + "joined_events": self._events, + "next_joined_event_start": None, + "next_joined_event_end": None, + "next_joined_event_duration_in_minutes": None + } + + current_date = now() + current_event = current_saving_sessions_event(current_date, self._events) + if (current_event is not None): + self._state = True + self._attributes["current_joined_event_start"] = current_event["start"] + self._attributes["current_joined_event_end"] = current_event["end"] + self._attributes["current_joined_event_duration_in_minutes"] = current_event["duration_in_minutes"] + else: + self._state = False + + next_event = get_next_saving_sessions_event(current_date, self._events) + if (next_event is not None): + self._attributes["next_joined_event_start"] = next_event["start"] + self._attributes["next_joined_event_end"] = next_event["end"] + self._attributes["next_joined_event_duration_in_minutes"] = next_event["duration_in_minutes"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None: + self._state = state.state + + if (self._state is None): + self._state = False + + _LOGGER.debug(f'Restored state: {self._state}') diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py new file mode 100644 index 00000000..2dee8abd --- /dev/null +++ b/custom_components/octopus_energy/sensor.py @@ -0,0 +1,200 @@ +import logging +from homeassistant.util.dt import (utcnow) +from homeassistant.core import HomeAssistant + +from .electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption +from .electricity.current_accumulative_consumption import OctopusEnergyCurrentAccumulativeElectricityConsumption +from .electricity.current_accumulative_cost import OctopusEnergyCurrentAccumulativeElectricityCost +from .electricity.current_accumulative_consumption_off_peak import OctopusEnergyCurrentAccumulativeElectricityConsumptionOffPeak +from .electricity.current_accumulative_consumption_peak import OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak +from .electricity.current_accumulative_cost_off_peak import OctopusEnergyCurrentAccumulativeElectricityCostOffPeak +from .electricity.current_accumulative_cost_peak import OctopusEnergyCurrentAccumulativeElectricityCostPeak +from .electricity.current_demand import OctopusEnergyCurrentElectricityDemand +from .electricity.current_rate import OctopusEnergyElectricityCurrentRate +from .electricity.next_rate import OctopusEnergyElectricityNextRate +from .electricity.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeElectricityConsumption +from .electricity.previous_accumulative_consumption_off_peak import OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak +from .electricity.previous_accumulative_consumption_peak import OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak +from .electricity.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeElectricityCost +from .electricity.previous_accumulative_cost_off_peak import OctopusEnergyPreviousAccumulativeElectricityCostOffPeak +from .electricity.previous_accumulative_cost_peak import OctopusEnergyPreviousAccumulativeElectricityCostPeak +from .electricity.previous_accumulative_cost_override import OctopusEnergyPreviousAccumulativeElectricityCostOverride +from .electricity.previous_rate import OctopusEnergyElectricityPreviousRate +from .electricity.standing_charge import OctopusEnergyElectricityCurrentStandingCharge +from .gas.current_rate import OctopusEnergyGasCurrentRate +from .gas.next_rate import OctopusEnergyGasNextRate +from .gas.previous_rate import OctopusEnergyGasPreviousRate +from .gas.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeGasConsumption +from .gas.previous_accumulative_consumption_kwh import OctopusEnergyPreviousAccumulativeGasConsumptionKwh +from .gas.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeGasCost +from .gas.current_consumption import OctopusEnergyCurrentGasConsumption +from .gas.current_accumulative_consumption import OctopusEnergyCurrentAccumulativeGasConsumption +from .gas.current_accumulative_cost import OctopusEnergyCurrentAccumulativeGasCost +from .gas.standing_charge import OctopusEnergyGasCurrentStandingCharge +from .gas.previous_accumulative_cost_override import OctopusEnergyPreviousAccumulativeGasCostOverride + +from .coordinators.current_consumption import async_create_current_consumption_coordinator +from .coordinators.gas_rates import async_create_gas_rate_coordinator +from .coordinators.previous_consumption_and_rates import async_create_previous_consumption_and_rates_coordinator +from .coordinators.electricity_standing_charges import async_setup_electricity_standing_charges_coordinator +from .coordinators.gas_standing_charges import async_setup_gas_standing_charges_coordinator + +from .saving_sessions.points import OctopusEnergySavingSessionPoints + +from .utils import (get_active_tariff_code) +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION, + CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES, + CONFIG_MAIN_CALORIFIC_VALUE, + CONFIG_MAIN_ELECTRICITY_PRICE_CAP, + CONFIG_MAIN_GAS_PRICE_CAP, + CONFIG_MAIN_ACCOUNT_ID, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_CLIENT, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_default_sensors(hass, entry, async_add_entities) + +async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_entities): + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + client = hass.data[DOMAIN][DATA_CLIENT] + + saving_session_coordinator = hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] + await saving_session_coordinator.async_config_entry_first_refresh() + + entities = [OctopusEnergySavingSessionPoints(hass, saving_session_coordinator)] + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + + if len(account_info["electricity_meter_points"]) > 0: + electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] + await electricity_rate_coordinator.async_config_entry_first_refresh() + + electricity_standing_charges_coordinator = await async_setup_electricity_standing_charges_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + electricity_price_cap = None + if CONFIG_MAIN_ELECTRICITY_PRICE_CAP in config: + electricity_price_cap = config[CONFIG_MAIN_ELECTRICITY_PRICE_CAP] + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + electricity_tariff_code = get_active_tariff_code(now, point["agreements"]) + if electricity_tariff_code is not None: + for meter in point["meters"]: + _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + entities.append(OctopusEnergyElectricityCurrentRate(hass, electricity_rate_coordinator, meter, point, electricity_tariff_code, electricity_price_cap)) + entities.append(OctopusEnergyElectricityPreviousRate(hass, electricity_rate_coordinator, meter, point)) + entities.append(OctopusEnergyElectricityNextRate(hass, electricity_rate_coordinator, meter, point)) + entities.append(OctopusEnergyElectricityCurrentStandingCharge(hass, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + + previous_consumption_coordinator = await async_create_previous_consumption_and_rates_coordinator( + hass, + client, + point["mpan"], + meter["serial_number"], + True, + electricity_tariff_code, + meter["is_smart_meter"] + ) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCostPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCostOffPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCostOverride(hass, previous_consumption_coordinator, client, electricity_tariff_code, meter, point)) + + if meter["is_export"] == False and CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION in config and config[CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION] == True: + live_consumption_refresh_in_minutes = 1 + if CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES in config: + live_consumption_refresh_in_minutes = config[CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES] + + consumption_coordinator = await async_create_current_consumption_coordinator(hass, client, meter["device_id"], True, live_consumption_refresh_in_minutes) + entities.append(OctopusEnergyCurrentElectricityConsumption(hass, consumption_coordinator, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityConsumption(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityConsumptionOffPeak(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityCost(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityCostPeak(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeElectricityCostOffPeak(hass, consumption_coordinator, electricity_rate_coordinator, electricity_standing_charges_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyCurrentElectricityDemand(hass, consumption_coordinator, meter, point)) + else: + for meter in point["meters"]: + _LOGGER.info(f'Skipping electricity meter due to no active agreement; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + _LOGGER.info(f'agreements: {point["agreements"]}') + else: + _LOGGER.info('No electricity meters available') + + if len(account_info["gas_meter_points"]) > 0: + + calorific_value = 40 + if CONFIG_MAIN_CALORIFIC_VALUE in config: + calorific_value = config[CONFIG_MAIN_CALORIFIC_VALUE] + + gas_price_cap = None + if CONFIG_MAIN_GAS_PRICE_CAP in config: + gas_price_cap = config[CONFIG_MAIN_GAS_PRICE_CAP] + + gas_rate_coordinator = await async_create_gas_rate_coordinator(hass, client) + gas_standing_charges_coordinator = await async_setup_gas_standing_charges_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + for point in account_info["gas_meter_points"]: + # We only care about points that have active agreements + gas_tariff_code = get_active_tariff_code(now, point["agreements"]) + if gas_tariff_code is not None: + for meter in point["meters"]: + _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + entities.append(OctopusEnergyGasCurrentRate(hass, gas_rate_coordinator, gas_tariff_code, meter, point, gas_price_cap)) + entities.append(OctopusEnergyGasPreviousRate(hass, gas_rate_coordinator, meter, point)) + entities.append(OctopusEnergyGasNextRate(hass, gas_rate_coordinator, meter, point)) + entities.append(OctopusEnergyGasCurrentStandingCharge(hass, gas_standing_charges_coordinator, gas_tariff_code, meter, point)) + + previous_consumption_coordinator = await async_create_previous_consumption_and_rates_coordinator( + hass, + client, + point["mprn"], + meter["serial_number"], + False, + gas_tariff_code, + None + ) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumptionKwh(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasCost(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasCostOverride(hass, previous_consumption_coordinator, client, gas_tariff_code, meter, point, calorific_value)) + + if CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION in config and config[CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION] == True: + live_consumption_refresh_in_minutes = 1 + if CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES in config: + live_consumption_refresh_in_minutes = config[CONFIG_MAIN_LIVE_CONSUMPTION_REFRESH_IN_MINUTES] + + consumption_coordinator = await async_create_current_consumption_coordinator(hass, client, meter["device_id"], False, live_consumption_refresh_in_minutes) + entities.append(OctopusEnergyCurrentGasConsumption(hass, consumption_coordinator, meter, point)) + entities.append(OctopusEnergyCurrentAccumulativeGasConsumption(hass, consumption_coordinator, gas_rate_coordinator, gas_standing_charges_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyCurrentAccumulativeGasCost(hass, consumption_coordinator, gas_rate_coordinator, gas_standing_charges_coordinator, gas_tariff_code, meter, point, calorific_value)) + else: + for meter in point["meters"]: + _LOGGER.info(f'Skipping gas meter due to no active agreement; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + _LOGGER.info(f'agreements: {point["agreements"]}') + else: + _LOGGER.info('No gas meters available') + + async_add_entities(entities, True) diff --git a/custom_components/octopus_energy/sensor_utils.py b/custom_components/octopus_energy/sensor_utils.py new file mode 100644 index 00000000..b4e016b6 --- /dev/null +++ b/custom_components/octopus_energy/sensor_utils.py @@ -0,0 +1,241 @@ +from .api_client import OctopusEnergyApiClient + +minimum_consumption_records = 2 + +def __get_interval_end(item): + return item["interval_end"] + +def __sort_consumption(consumption_data): + sorted = consumption_data.copy() + sorted.sort(key=__get_interval_end) + return sorted + +async def async_get_consumption_data( + client: OctopusEnergyApiClient, + previous_data, + current_utc_timestamp, + period_from, + period_to, + sensor_identifier, + sensor_serial_number, + is_electricity: bool +): + if (previous_data == None or + ((len(previous_data) < 1 or previous_data[-1]["interval_end"] < period_to) and + current_utc_timestamp.minute % 30 == 0) + ): + if (is_electricity == True): + data = await client.async_get_electricity_consumption(sensor_identifier, sensor_serial_number, period_from, period_to) + else: + data = await client.async_get_gas_consumption(sensor_identifier, sensor_serial_number, period_from, period_to) + + if data != None and len(data) > 0: + data = __sort_consumption(data) + return data + + if previous_data != None: + return previous_data + else: + return [] + +def calculate_electricity_consumption(consumption_data, last_calculated_timestamp): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + total = 0 + + consumption_parts = [] + for consumption in sorted_consumption_data: + total = total + consumption["consumption"] + + current_consumption = consumption["consumption"] + + consumption_parts.append({ + "from": consumption["interval_start"], + "to": consumption["interval_end"], + "consumption": current_consumption, + }) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "total": total, + "last_calculated_timestamp": last_calculated_timestamp, + "consumptions": consumption_parts + } + +async def async_calculate_electricity_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, tariff_code, is_smart_meter): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + standard_charge_result = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + + if (rates != None and len(rates) > 0 and standard_charge_result != None): + standard_charge = standard_charge_result["value_inc_vat"] + + charges = [] + total_cost_in_pence = 0 + for consumption in sorted_consumption_data: + value = consumption["consumption"] + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {tariff_code}") + + cost = (rate["value_inc_vat"] * value) + total_cost_in_pence = total_cost_in_pence + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": f'{rate["value_inc_vat"]}p', + "consumption": f'{value} kWh', + "cost": f'£{round(cost / 100, 2)}' + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standard_charge) / 100, 2) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "standing_charge": standard_charge, + "total_without_standing_charge": total_cost, + "total": total_cost_plus_standing_charge, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_m3_to_kwh(value): + kwh_value = value * 1.02264 # Volume correction factor + kwh_value = kwh_value * 40.0 # Calorific value + return round(kwh_value / 3.6, 3) # kWh Conversion factor + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_kwh_to_m3(value): + m3_value = value * 3.6 # kWh Conversion factor + m3_value = m3_value / 40 # Calorific value + return round(m3_value / 1.02264, 3) # Volume correction factor + +def calculate_gas_consumption(consumption_data, last_calculated_timestamp, consumption_units): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + total_m3 = 0 + total_kwh = 0 + + consumption_parts = [] + for consumption in sorted_consumption_data: + current_consumption_m3 = 0 + current_consumption_kwh = 0 + + current_consumption = consumption["consumption"] + + if consumption_units == "m³": + current_consumption_m3 = current_consumption + current_consumption_kwh = convert_m3_to_kwh(current_consumption) + else: + current_consumption_m3 = convert_kwh_to_m3(current_consumption) + current_consumption_kwh = current_consumption + + total_m3 = total_m3 + current_consumption_m3 + total_kwh = total_kwh + current_consumption_kwh + + consumption_parts.append({ + "from": consumption["interval_start"], + "to": consumption["interval_end"], + "consumption_m3": current_consumption_m3, + "consumption_kwh": current_consumption_kwh, + }) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "total_m3": round(total_m3, 3), + "total_kwh": round(total_kwh, 3), + "last_calculated_timestamp": last_calculated_timestamp, + "consumptions": consumption_parts + } + +async def async_calculate_gas_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, sensor, consumption_units): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + rates = await client.async_get_gas_rates(sensor["tariff_code"], period_from, period_to) + standard_charge_result = await client.async_get_gas_standing_charge(sensor["tariff_code"], period_from, period_to) + + if (rates != None and len(rates) > 0 and standard_charge_result != None): + standard_charge = standard_charge_result["value_inc_vat"] + + charges = [] + total_cost_in_pence = 0 + for consumption in sorted_consumption_data: + value = consumption["consumption"] + + if consumption_units == "m³": + value = convert_m3_to_kwh(value) + + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {sensor['tariff_code']}") + + cost = (rate["value_inc_vat"] * value) + total_cost_in_pence = total_cost_in_pence + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": f'{rate["value_inc_vat"]}p', + "consumption": f'{value} kWh', + "cost": f'£{round(cost / 100, 2)}' + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standard_charge) / 100, 2) + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "standing_charge": standard_charge, + "total_without_standing_charge": total_cost, + "total": total_cost_plus_standing_charge, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + +def is_saving_sessions_event_active(current_date, events): + for event in events: + if (event["start"] <= current_date and event["end"] >= current_date): + return True + + return False + +def get_next_saving_sessions_event(current_date, events): + next_event = None + for event in events: + if event["start"] > current_date and (next_event == None or event["start"] < next_event["start"]): + next_event = { + "start": event["start"], + "end": event["end"], + "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 + } + + return next_event \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/__init__.py b/custom_components/octopus_energy/sensors/__init__.py new file mode 100644 index 00000000..3a808b46 --- /dev/null +++ b/custom_components/octopus_energy/sensors/__init__.py @@ -0,0 +1,276 @@ +from ..api_client import OctopusEnergyApiClient +from datetime import (timedelta) +from homeassistant.util.dt import (parse_datetime) + +minimum_consumption_records = 2 + +def __get_interval_end(item): + return item["interval_end"] + +def __sort_consumption(consumption_data): + sorted = consumption_data.copy() + sorted.sort(key=__get_interval_end) + return sorted + +async def async_get_consumption_data( + client: OctopusEnergyApiClient, + previous_data, + current_utc_timestamp, + period_from, + period_to, + sensor_identifier, + sensor_serial_number, + is_electricity: bool +): + if (previous_data == None or + ((len(previous_data) < 1 or previous_data[-1]["interval_end"] < period_to) and + current_utc_timestamp.minute % 30 == 0) + ): + if (is_electricity == True): + data = await client.async_get_electricity_consumption(sensor_identifier, sensor_serial_number, period_from, period_to) + else: + data = await client.async_get_gas_consumption(sensor_identifier, sensor_serial_number, period_from, period_to) + + if data != None and len(data) > 0: + data = __sort_consumption(data) + return data + + if previous_data != None: + return previous_data + else: + return [] + +def calculate_electricity_consumption(consumption_data, last_calculated_timestamp): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + total = 0 + + consumption_parts = [] + for consumption in sorted_consumption_data: + total = total + consumption["consumption"] + + current_consumption = consumption["consumption"] + + consumption_parts.append({ + "from": consumption["interval_start"], + "to": consumption["interval_end"], + "consumption": current_consumption, + }) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "total": total, + "last_calculated_timestamp": last_calculated_timestamp, + "consumptions": consumption_parts + } + +async def async_calculate_electricity_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, tariff_code, is_smart_meter): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + standard_charge_result = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + + if (rates != None and len(rates) > 0 and standard_charge_result != None): + standard_charge = standard_charge_result["value_inc_vat"] + + charges = [] + total_cost_in_pence = 0 + for consumption in sorted_consumption_data: + value = consumption["consumption"] + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {tariff_code}") + + cost = (rate["value_inc_vat"] * value) + total_cost_in_pence = total_cost_in_pence + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": f'{rate["value_inc_vat"]}p', + "consumption": f'{value} kWh', + "cost": f'£{round(cost / 100, 2)}' + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standard_charge) / 100, 2) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "standing_charge": standard_charge, + "total_without_standing_charge": total_cost, + "total": total_cost_plus_standing_charge, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_m3_to_kwh(value, calorific_value): + kwh_value = value * 1.02264 # Volume correction factor + kwh_value = kwh_value * calorific_value # Calorific value + return round(kwh_value / 3.6, 3) # kWh Conversion factor + +# Adapted from https://www.theenergyshop.com/guides/how-to-convert-gas-units-to-kwh +def convert_kwh_to_m3(value, calorific_value): + m3_value = value * 3.6 # kWh Conversion factor + m3_value = m3_value / calorific_value # Calorific value + return round(m3_value / 1.02264, 3) # Volume correction factor + +def calculate_gas_consumption(consumption_data, last_calculated_timestamp, consumption_units, calorific_value): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + total_m3 = 0 + total_kwh = 0 + + consumption_parts = [] + for consumption in sorted_consumption_data: + current_consumption_m3 = 0 + current_consumption_kwh = 0 + + current_consumption = consumption["consumption"] + + if consumption_units == "m³": + current_consumption_m3 = current_consumption + current_consumption_kwh = convert_m3_to_kwh(current_consumption, calorific_value) + else: + current_consumption_m3 = convert_kwh_to_m3(current_consumption, calorific_value) + current_consumption_kwh = current_consumption + + total_m3 = total_m3 + current_consumption_m3 + total_kwh = total_kwh + current_consumption_kwh + + consumption_parts.append({ + "from": consumption["interval_start"], + "to": consumption["interval_end"], + "consumption_m3": current_consumption_m3, + "consumption_kwh": current_consumption_kwh, + }) + + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "total_m3": round(total_m3, 3), + "total_kwh": round(total_kwh, 3), + "last_calculated_timestamp": last_calculated_timestamp, + "consumptions": consumption_parts + } + +async def async_calculate_gas_cost(client: OctopusEnergyApiClient, consumption_data, last_calculated_timestamp, period_from, period_to, sensor, consumption_units, calorific_value): + if (consumption_data != None and len(consumption_data) > minimum_consumption_records): + + sorted_consumption_data = __sort_consumption(consumption_data) + + # Only calculate our consumption if our data has changed + if (last_calculated_timestamp == None or last_calculated_timestamp < sorted_consumption_data[-1]["interval_end"]): + rates = await client.async_get_gas_rates(sensor["tariff_code"], period_from, period_to) + standard_charge_result = await client.async_get_gas_standing_charge(sensor["tariff_code"], period_from, period_to) + + if (rates != None and len(rates) > 0 and standard_charge_result != None): + standard_charge = standard_charge_result["value_inc_vat"] + + charges = [] + total_cost_in_pence = 0 + for consumption in sorted_consumption_data: + value = consumption["consumption"] + + if consumption_units == "m³": + value = convert_m3_to_kwh(value, calorific_value) + + consumption_from = consumption["interval_start"] + consumption_to = consumption["interval_end"] + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to} for tariff {sensor['tariff_code']}") + + cost = (rate["value_inc_vat"] * value) + total_cost_in_pence = total_cost_in_pence + cost + + charges.append({ + "from": rate["valid_from"], + "to": rate["valid_to"], + "rate": f'{rate["value_inc_vat"]}p', + "consumption": f'{value} kWh', + "cost": f'£{round(cost / 100, 2)}' + }) + + total_cost = round(total_cost_in_pence / 100, 2) + total_cost_plus_standing_charge = round((total_cost_in_pence + standard_charge) / 100, 2) + last_calculated_timestamp = sorted_consumption_data[-1]["interval_end"] + + return { + "standing_charge": standard_charge, + "total_without_standing_charge": total_cost, + "total": total_cost_plus_standing_charge, + "last_calculated_timestamp": last_calculated_timestamp, + "charges": charges + } + +def current_saving_sessions_event(current_date, events): + current_event = None + for event in events: + if (event["start"] <= current_date and event["end"] >= current_date): + current_event = { + "start": event["start"], + "end": event["end"], + "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 + } + break + + return current_event + +def get_next_saving_sessions_event(current_date, events): + next_event = None + for event in events: + if event["start"] > current_date and (next_event == None or event["start"] < next_event["start"]): + next_event = { + "start": event["start"], + "end": event["end"], + "duration_in_minutes": (event["end"] - event["start"]).total_seconds() / 60 + } + + return next_event + +async def async_get_live_consumption(client: OctopusEnergyApiClient, device_id, current_date, last_retrieval_date): + period_to = current_date.strftime("%Y-%m-%dT%H:%M:00Z") + if (last_retrieval_date is None): + period_from = (parse_datetime(period_to) - timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:00Z") + else: + period_from = (last_retrieval_date + timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:00Z") + + result = await client.async_get_smart_meter_consumption(device_id, period_from, period_to) + if result is not None: + + total_consumption = 0 + latest_date = None + demand = None + for item in result: + total_consumption += item["consumption"] + if (latest_date is None or latest_date < item["startAt"]): + latest_date = item["startAt"] + demand = item["demand"] + + return { + "consumption": total_consumption, + "startAt": latest_date, + "demand": demand + } + + return None \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/__init__.py b/custom_components/octopus_energy/sensors/electricity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/custom_components/octopus_energy/sensors/electricity/base.py b/custom_components/octopus_energy/sensors/electricity/base.py new file mode 100644 index 00000000..d3b0a7ec --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/base.py @@ -0,0 +1,41 @@ +from homeassistant.components.sensor import ( + SensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from ...const import ( + DOMAIN, +) + +class OctopusEnergyElectricitySensor(SensorEntity, RestoreEntity): + def __init__(self, meter, point): + """Init sensor""" + self._point = point + self._meter = meter + + self._mpan = point["mpan"] + self._serial_number = meter["serial_number"] + self._is_export = meter["is_export"] + self._is_smart_meter = meter["is_smart_meter"] + self._export_id_addition = "_export" if self._is_export == True else "" + self._export_name_addition = " Export" if self._is_export == True else "" + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter + } + + @property + def device_info(self): + return { + "identifiers": { + # Serial numbers/mpan are unique identifiers within a specific domain + (DOMAIN, f"electricity_{self._serial_number}_{self._mpan}") + }, + "default_name": f"Electricity Meter{self._export_name_addition}", + "manufacturer": self._meter["manufacturer"], + "model": self._meter["model"], + "sw_version": self._meter["firmware"] + } \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/current_consumption.py b/custom_components/octopus_energy/sensors/electricity/current_consumption.py new file mode 100644 index 00000000..d8fda062 --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/current_consumption.py @@ -0,0 +1,91 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current electricity consumption.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan} Current Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the latest electricity consumption""" + + _LOGGER.debug('Updating OctopusEnergyCurrentElectricityConsumption') + consumption_result = self.coordinator.data + + if (consumption_result is not None): + self._latest_date = consumption_result["startAt"] + self._state = consumption_result["consumption"] / 1000 + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentElectricityConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/current_demand.py b/custom_components/octopus_energy/sensors/electricity/current_demand.py new file mode 100644 index 00000000..666bd71b --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/current_demand.py @@ -0,0 +1,88 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentElectricityDemand(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current electricity demand.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_demand" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan} Current Demand" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.POWER + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.MEASUREMENT + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "W" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the latest electricity demand""" + + _LOGGER.debug('Updating OctopusEnergyCurrentElectricityConsumption') + consumption_result = self.coordinator.data + + if (consumption_result is not None): + self._latest_date = consumption_result["startAt"] + self._state = consumption_result["demand"] + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentElectricityDemand state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/current_rate.py b/custom_components/octopus_energy/sensors/electricity/current_rate.py new file mode 100644 index 00000000..f11a3c18 --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/current_rate.py @@ -0,0 +1,114 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current rate.""" + + def __init__(self, coordinator, meter, point, electricity_price_cap): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._last_updated = None + self._electricity_price_cap = electricity_price_cap + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Rate" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """The state of the sensor.""" + # Find the current rate. We only need to do this every half an hour + now = utcnow() + if (self._last_updated is None or self._last_updated < (now - timedelta(minutes=30)) or (now.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityCurrentRate for '{self._mpan}/{self._serial_number}'") + + current_rate = None + if self.coordinator.data != None: + rate = self.coordinator.data[self._mpan] if self._mpan in self.coordinator.data else None + if rate != None: + for period in rate: + if now >= period["valid_from"] and now <= period["valid_to"]: + current_rate = period + break + + if current_rate != None: + ratesAttributes = list(map(lambda x: { + "from": x["valid_from"], + "to": x["valid_to"], + "rate": x["value_inc_vat"], + "is_capped": x["is_capped"] + }, rate)) + self._attributes = { + "rate": current_rate, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "rates": ratesAttributes + } + + if self._electricity_price_cap is not None: + self._attributes["price_cap"] = self._electricity_price_cap + + self._state = current_rate["value_inc_vat"] / 100 + else: + self._state = None + self._attributes = {} + + self._last_updated = now + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/next_rate.py b/custom_components/octopus_energy/sensors/electricity/next_rate.py new file mode 100644 index 00000000..20afadc2 --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/next_rate.py @@ -0,0 +1,105 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the next rate.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._last_updated = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_next_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Next Rate" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """The state of the sensor.""" + # Find the next rate. We only need to do this every half an hour + now = utcnow() + if (self._last_updated is None or self._last_updated < (now - timedelta(minutes=30)) or (now.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityNextRate for '{self._mpan}/{self._serial_number}'") + + target = now + timedelta(minutes=30) + + next_rate = None + if self.coordinator.data != None: + rate = self.coordinator.data[self._mpan] if self._mpan in self.coordinator.data else None + if rate != None: + for period in rate: + if target >= period["valid_from"] and target <= period["valid_to"]: + next_rate = period + break + + if next_rate != None: + self._attributes = { + "rate": next_rate, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter + } + + self._state = next_rate["value_inc_vat"] / 100 + else: + self._state = None + self._attributes = {} + + self._last_updated = now + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityNextRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/previous_accumulative_consumption.py b/custom_components/octopus_energy/sensors/electricity/previous_accumulative_consumption.py new file mode 100644 index 00000000..33bf63d3 --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/previous_accumulative_consumption.py @@ -0,0 +1,110 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .. import ( + calculate_electricity_consumption, +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + consumption = calculate_electricity_consumption( + self.coordinator.data, + self._latest_date + ) + + if (consumption != None): + _LOGGER.debug(f"Calculated previous electricity consumption for '{self._mpan}/{self._serial_number}'...") + self._state = consumption["total"] + self._latest_date = consumption["last_calculated_timestamp"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "total": consumption["total"], + "last_calculated_timestamp": consumption["last_calculated_timestamp"], + "charges": consumption["consumptions"] + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/previous_accumulative_cost.py b/custom_components/octopus_energy/sensors/electricity/previous_accumulative_cost.py new file mode 100644 index 00000000..3e90346b --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/previous_accumulative_cost.py @@ -0,0 +1,130 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from .. import ( + async_calculate_electricity_cost, +) + +from ...api_client import (OctopusEnergyApiClient) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost.""" + + def __init__(self, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._client = client + self._tariff_code = tariff_code + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def should_poll(self): + return True + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + async def async_update(self): + current_datetime = now() + period_from = as_utc((current_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = as_utc(current_datetime.replace(hour=0, minute=0, second=0, microsecond=0)) + + consumption_cost = await async_calculate_electricity_cost( + self._client, + self.coordinator.data, + self._latest_date, + period_from, + period_to, + self._tariff_code, + self._is_smart_meter + ) + + if (consumption_cost != None): + _LOGGER.debug(f"Calculated previous electricity consumption cost for '{self._mpan}/{self._serial_number}'...") + self._latest_date = consumption_cost["last_calculated_timestamp"] + self._state = consumption_cost["total"] + + self._attributes = { + "mpan": self._mpan, + "serial_number": self._serial_number, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_cost["total_without_standing_charge"]}', + "total": f'£{consumption_cost["total"]}', + "last_calculated_timestamp": consumption_cost["last_calculated_timestamp"], + "charges": consumption_cost["charges"] + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/previous_rate.py b/custom_components/octopus_energy/sensors/electricity/previous_rate.py new file mode 100644 index 00000000..e06e175c --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/previous_rate.py @@ -0,0 +1,105 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous rate.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._state = None + self._last_updated = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Rate" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """The state of the sensor.""" + # Find the previous rate. We only need to do this every half an hour + now = utcnow() + if (self._last_updated is None or self._last_updated < (now - timedelta(minutes=30)) or (now.minute % 30) == 0): + _LOGGER.debug(f"Updating OctopusEnergyElectricityPreviousRate for '{self._mpan}/{self._serial_number}'") + + target = now - timedelta(minutes=30) + + previous_rate = None + if self.coordinator.data != None: + rate = self.coordinator.data[self._mpan] if self._mpan in self.coordinator.data else None + if rate != None: + for period in rate: + if target >= period["valid_from"] and target <= period["valid_to"]: + previous_rate = period + break + + if previous_rate != None: + self._attributes = { + "rate": previous_rate, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter + } + + self._state = previous_rate["value_inc_vat"] / 100 + else: + self._state = None + self._attributes = {} + + self._last_updated = now + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityPreviousRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/electricity/standing_charge.py b/custom_components/octopus_energy/sensors/electricity/standing_charge.py new file mode 100644 index 00000000..9ea25319 --- /dev/null +++ b/custom_components/octopus_energy/sensors/electricity/standing_charge.py @@ -0,0 +1,98 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, as_utc, parse_datetime) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from ...api_client import (OctopusEnergyApiClient) + +from .base import (OctopusEnergyElectricitySensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentStandingCharge(OctopusEnergyElectricitySensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + OctopusEnergyElectricitySensor.__init__(self, meter, point) + + self._client = client + self._tariff_code = tariff_code + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_standing_charge' + + @property + def name(self): + """Name of the sensor.""" + return f'Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Standing Charge' + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the latest electricity standing charge""" + return self._state + + async def async_update(self): + """Get the current price.""" + # Find the current rate. We only need to do this every day + + utc_now = utcnow() + if (self._latest_date == None or (self._latest_date + timedelta(days=1)) < utc_now): + _LOGGER.debug('Updating OctopusEnergyElectricityCurrentStandingCharge') + + period_from = as_utc(parse_datetime(utc_now.strftime("%Y-%m-%dT00:00:00Z"))) + period_to = as_utc(parse_datetime((utc_now + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00Z"))) + + standard_charge_result = await self._client.async_get_electricity_standing_charge(self._tariff_code, period_from, period_to) + + if standard_charge_result != None: + self._latest_date = period_from + self._state = standard_charge_result["value_inc_vat"] / 100 + + # Adjust our period, as our gas only changes on a daily basis + self._attributes["valid_from"] = period_from + self._attributes["valid_to"] = period_to + else: + self._state = None + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentStandingCharge state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/__init__.py b/custom_components/octopus_energy/sensors/gas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/custom_components/octopus_energy/sensors/gas/base.py b/custom_components/octopus_energy/sensors/gas/base.py new file mode 100644 index 00000000..eca9c6a5 --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/base.py @@ -0,0 +1,35 @@ +from homeassistant.components.sensor import ( + SensorEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from ...const import ( + DOMAIN, +) + +class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): + def __init__(self, meter, point): + """Init sensor""" + self._point = point + self._meter = meter + + self._mprn = point["mprn"] + self._serial_number = meter["serial_number"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number + } + + @property + def device_info(self): + return { + "identifiers": { + # Serial numbers/mpan are unique identifiers within a specific domain + (DOMAIN, f"electricity_{self._serial_number}_{self._mprn}") + }, + "default_name": "Gas Meter", + "manufacturer": self._meter["manufacturer"], + "model": self._meter["model"], + "sw_version": self._meter["firmware"] + } \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/current_consumption.py b/custom_components/octopus_energy/sensors/gas/current_consumption.py new file mode 100644 index 00000000..ab8f23b9 --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/current_consumption.py @@ -0,0 +1,91 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyCurrentGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current gas consumption.""" + + def __init__(self, coordinator, meter, point): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_current_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Current Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.GAS + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the latest gas consumption""" + + _LOGGER.debug('Updating OctopusEnergyCurrentGasConsumption') + consumption_result = self.coordinator.data + + if (consumption_result is not None): + self._latest_date = consumption_result["startAt"] + self._state = consumption_result["consumption"] / 1000 + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + + _LOGGER.debug(f'Restored OctopusEnergyCurrentGasConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/current_rate.py b/custom_components/octopus_energy/sensors/gas/current_rate.py new file mode 100644 index 00000000..346eeb83 --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/current_rate.py @@ -0,0 +1,106 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasCurrentRate(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the current rate.""" + + def __init__(self, coordinator, tariff_code, meter, point, gas_price_cap): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._tariff_code = tariff_code + self._gas_price_cap = gas_price_cap + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_current_rate'; + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Current Rate' + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP/kWh" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the latest gas price""" + + utc_now = utcnow() + if (self._latest_date is None or (self._latest_date + timedelta(days=1)) < utc_now) or self._state is None: + _LOGGER.debug('Updating OctopusEnergyGasCurrentRate') + + rates = self.coordinator.data + + current_rate = None + if rates is not None: + for period in rates: + if utc_now >= period["valid_from"] and utc_now <= period["valid_to"]: + current_rate = period + break + + if current_rate is not None: + self._latest_date = rates[0]["valid_from"] + self._state = current_rate["value_inc_vat"] / 100 + + # Adjust our period, as our gas only changes on a daily basis + current_rate["valid_from"] = rates[0]["valid_from"] + current_rate["valid_to"] = rates[-1]["valid_to"] + self._attributes = current_rate + + if self._gas_price_cap is not None: + self._attributes["price_cap"] = self._gas_price_cap + else: + self._state = None + self._attributes = {} + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasCurrentRate state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption.py new file mode 100644 index 00000000..15bb3f9d --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption.py @@ -0,0 +1,115 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + VOLUME_CUBIC_METERS +) + +from .. import ( + calculate_gas_consumption, +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading.""" + + def __init__(self, coordinator, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._native_consumption_units = meter["consumption_units"] + self._state = None + self._latest_date = None + self._calorific_value = calorific_value + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.GAS + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return VOLUME_CUBIC_METERS + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:fire" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + consumption = calculate_gas_consumption( + self.coordinator.data, + self._latest_date, + self._native_consumption_units, + self._calorific_value + ) + + if (consumption != None): + _LOGGER.debug(f"Calculated previous gas consumption for '{self._mprn}/{self._serial_number}'...") + self._state = consumption["total_m3"] + self._latest_date = consumption["last_calculated_timestamp"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_estimated": self._native_consumption_units != "m³", + "total_kwh": consumption["total_kwh"], + "total_m3": consumption["total_m3"], + "last_calculated_timestamp": consumption["last_calculated_timestamp"], + "charges": consumption["consumptions"], + "calorific_value": self._calorific_value + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption_kwh.py b/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption_kwh.py new file mode 100644 index 00000000..1adb7762 --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption_kwh.py @@ -0,0 +1,113 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR +) + +from .. import ( + calculate_gas_consumption, +) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasConsumptionKwh(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas consumption in kwh.""" + + def __init__(self, coordinator, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._native_consumption_units = meter["consumption_units"] + self._state = None + self._latest_date = None + self._calorific_value = calorific_value + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption_kwh" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption (kWh)" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.ENERGY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return ENERGY_KILO_WATT_HOUR + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:fire" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the previous days accumulative consumption""" + consumption = calculate_gas_consumption( + self.coordinator.data, + self._latest_date, + self._native_consumption_units, + self._calorific_value + ) + + if (consumption != None): + _LOGGER.debug(f"Calculated previous gas consumption for '{self._mprn}/{self._serial_number}'...") + self._state = consumption["total_kwh"] + self._latest_date = consumption["last_calculated_timestamp"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "is_estimated": self._native_consumption_units == "m³", + "last_calculated_timestamp": consumption["last_calculated_timestamp"], + "charges": consumption["consumptions"], + "calorific_value": self._calorific_value + } + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumptionKwh state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/previous_accumulative_cost.py b/custom_components/octopus_energy/sensors/gas/previous_accumulative_cost.py new file mode 100644 index 00000000..69309003 --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/previous_accumulative_cost.py @@ -0,0 +1,134 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) +from .. import ( + async_calculate_gas_cost, +) + +from ...api_client import (OctopusEnergyApiClient) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas cost.""" + + def __init__(self, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point, calorific_value): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._client = client + self._tariff_code = tariff_code + self._native_consumption_units = meter["consumption_units"] + + self._state = None + self._latest_date = None + self._calorific_value = calorific_value + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Accumulative Cost" + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL + + @property + def unit_of_measurement(self): + """The unit of measurement of sensor""" + return "GBP" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def should_poll(self): + return True + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if any.""" + return self._latest_date + + @property + def state(self): + """Retrieve the previously calculated state""" + return self._state + + async def async_update(self): + current_datetime = now() + period_from = as_utc((current_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = as_utc(current_datetime.replace(hour=0, minute=0, second=0, microsecond=0)) + + consumption_cost = await async_calculate_gas_cost( + self._client, + self.coordinator.data, + self._latest_date, + period_from, + period_to, + { + "tariff_code": self._tariff_code, + }, + self._native_consumption_units, + self._calorific_value + ) + + if (consumption_cost != None): + _LOGGER.debug(f"Calculated previous gas consumption cost for '{self._mprn}/{self._serial_number}'...") + self._latest_date = consumption_cost["last_calculated_timestamp"] + self._state = consumption_cost["total"] + + self._attributes = { + "mprn": self._mprn, + "serial_number": self._serial_number, + "tariff_code": self._tariff_code, + "standing_charge": f'{consumption_cost["standing_charge"]}p', + "total_without_standing_charge": f'£{consumption_cost["total_without_standing_charge"]}', + "total": f'£{consumption_cost["total"]}', + "last_calculated_timestamp": consumption_cost["last_calculated_timestamp"], + "charges": consumption_cost["charges"], + "calorific_value": self._calorific_value + } + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasCost state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/gas/standing_charge.py b/custom_components/octopus_energy/sensors/gas/standing_charge.py new file mode 100644 index 00000000..f791f7cf --- /dev/null +++ b/custom_components/octopus_energy/sensors/gas/standing_charge.py @@ -0,0 +1,98 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, as_utc, parse_datetime) +from homeassistant.components.sensor import ( + SensorDeviceClass +) + +from ...api_client import (OctopusEnergyApiClient) + +from .base import (OctopusEnergyGasSensor) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasCurrentStandingCharge(OctopusEnergyGasSensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, meter, point) + + self._client = client + self._tariff_code = tariff_code + + self._state = None + self._latest_date = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f'octopus_energy_gas_{self._serial_number}_{self._mprn}_current_standing_charge'; + + @property + def name(self): + """Name of the sensor.""" + return f'Gas {self._serial_number} {self._mprn} Current Standing Charge' + + @property + def device_class(self): + """The type of sensor""" + return SensorDeviceClass.MONETARY + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:currency-gbp" + + @property + def unit_of_measurement(self): + """Unit of measurement of the sensor.""" + return "GBP" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state(self): + """Retrieve the latest gas standing charge""" + return self._state + + async def async_update(self): + """Get the current price.""" + # Find the current rate. We only need to do this every day + + utc_now = utcnow() + if (self._latest_date == None or (self._latest_date + timedelta(days=1)) < utc_now): + _LOGGER.debug('Updating OctopusEnergyGasCurrentStandingCharge') + + period_from = as_utc(parse_datetime(utc_now.strftime("%Y-%m-%dT00:00:00Z"))) + period_to = as_utc(parse_datetime((utc_now + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00Z"))) + + standard_charge_result = await self._client.async_get_gas_standing_charge(self._tariff_code, period_from, period_to) + + if standard_charge_result != None: + self._latest_date = period_from + self._state = standard_charge_result["value_inc_vat"] / 100 + + # Adjust our period, as our gas only changes on a daily basis + self._attributes["valid_from"] = period_from + self._attributes["valid_to"] = period_to + else: + self._state = None + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasCurrentStandingCharge state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/sensors/saving_sessions/__init__.py b/custom_components/octopus_energy/sensors/saving_sessions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/custom_components/octopus_energy/sensors/saving_sessions/points.py b/custom_components/octopus_energy/sensors/saving_sessions/points.py new file mode 100644 index 00000000..afc89318 --- /dev/null +++ b/custom_components/octopus_energy/sensors/saving_sessions/points.py @@ -0,0 +1,73 @@ +import logging + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import ( + SensorEntity, + SensorStateClass +) +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergySavingSessionPoints(CoordinatorEntity, SensorEntity, RestoreEntity): + """Sensor for determining saving session points""" + + def __init__(self, coordinator): + """Init sensor.""" + + super().__init__(coordinator) + + self._state = None + self._attributes = {} + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_saving_session_points" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Saving Session Points" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:leaf" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def state_class(self): + """The state class of sensor""" + return SensorStateClass.TOTAL_INCREASING + + @property + def state(self): + """Retrieve the previously calculated state""" + saving_session = self.coordinator.data + if (saving_session is not None and "points" in saving_session): + self._state = saving_session["points"] + else: + self._state = 0 + + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergySavingSessionPoints state: {self._state}') \ No newline at end of file diff --git a/custom_components/octopus_energy/services.yaml b/custom_components/octopus_energy/services.yaml new file mode 100644 index 00000000..1d7e8768 --- /dev/null +++ b/custom_components/octopus_energy/services.yaml @@ -0,0 +1,32 @@ +update_target_config: + name: Update target rate config + description: Updates a given target rate's config. Please note this is temporary and will not persist between restarts. + target: + entity: + integration: octopus_energy + domain: binary_sensor + fields: + target_hours: + name: Hours + description: The optional number of hours the target rate sensor should come on during a 24 hour period. + example: '1.5' + selector: + text: + target_start_time: + name: Start time + description: The optional time the evaluation period should start. + example: '06:00' + selector: + text: + target_end_time: + name: End time + description: The optional time the evaluation period should end. + example: '19:00' + selector: + text: + target_offset: + name: Offset + description: + The optional offset to apply to the target rate when it starts + selector: + text: \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/__init__.py b/custom_components/octopus_energy/statistics/__init__.py new file mode 100644 index 00000000..727a2514 --- /dev/null +++ b/custom_components/octopus_energy/statistics/__init__.py @@ -0,0 +1,181 @@ +import logging +from datetime import (datetime, timedelta) +from homeassistant.core import HomeAssistant +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import StatisticData +from homeassistant.components.recorder.statistics import ( + statistics_during_period +) + +from ..utils import get_off_peak_cost + +_LOGGER = logging.getLogger(__name__) + +def build_consumption_statistics(consumptions, rates, consumption_key: str, latest_total_sum: float, latest_peak_sum: float, latest_off_peak_sum: float): + last_reset = consumptions[0]["from"].replace(minute=0, second=0, microsecond=0) + sums = { + "total": latest_total_sum, + "peak": latest_peak_sum, + "off_peak": latest_off_peak_sum + } + states = { + "total": 0, + "peak": 0, + "off_peak": 0 + } + + total_statistics = [] + off_peak_statistics = [] + peak_statistics = [] + off_peak_cost = get_off_peak_cost(rates) + + _LOGGER.debug(f'total_sum: {latest_total_sum}; latest_peak_sum: {latest_peak_sum}; latest_off_peak_sum: {latest_off_peak_sum}; last_reset: {last_reset}; off_peak_cost: {off_peak_cost}') + + for index in range(len(consumptions)): + consumption = consumptions[index] + consumption_from = consumption["from"] + consumption_to = consumption["to"] + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to}") + + if rate["value_inc_vat"] == off_peak_cost: + sums["off_peak"] += consumption[consumption_key] + states["off_peak"] += consumption[consumption_key] + else: + sums["peak"] += consumption[consumption_key] + states["peak"] += consumption[consumption_key] + + start = consumption["from"].replace(minute=0, second=0, microsecond=0) + sums["total"] += consumption[consumption_key] + states["total"] += consumption[consumption_key] + + _LOGGER.debug(f'index: {index}; start: {start}; sums: {sums}; states: {states}; added: {(index) % 2 == 1}') + + if index % 2 == 1: + total_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["total"], + state=states["total"] + ) + ) + + off_peak_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["off_peak"], + state=states["off_peak"] + ) + ) + + peak_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["peak"], + state=states["peak"] + ) + ) + + return { + "total": total_statistics, + "peak": peak_statistics, + "off_peak": off_peak_statistics + } + +def build_cost_statistics(consumptions, rates, consumption_key: str, latest_total_sum: float, latest_peak_sum: float, latest_off_peak_sum: float): + last_reset = consumptions[0]["from"].replace(minute=0, second=0, microsecond=0) + sums = { + "total": latest_total_sum, + "peak": latest_peak_sum, + "off_peak": latest_off_peak_sum + } + states = { + "total": 0, + "peak": 0, + "off_peak": 0 + } + + total_statistics = [] + off_peak_statistics = [] + peak_statistics = [] + off_peak_cost = get_off_peak_cost(rates) + + _LOGGER.debug(f'total_sum: {latest_total_sum}; latest_peak_sum: {latest_peak_sum}; latest_off_peak_sum: {latest_off_peak_sum}; last_reset: {last_reset}; off_peak_cost: {off_peak_cost}') + + for index in range(len(consumptions)): + consumption = consumptions[index] + consumption_from = consumption["from"] + consumption_to = consumption["to"] + start = consumption["from"].replace(minute=0, second=0, microsecond=0) + + try: + rate = next(r for r in rates if r["valid_from"] == consumption_from and r["valid_to"] == consumption_to) + except StopIteration: + raise Exception(f"Failed to find rate for consumption between {consumption_from} and {consumption_to}") + + if rate["value_inc_vat"] == off_peak_cost: + sums["off_peak"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + states["off_peak"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + else: + sums["peak"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + states["peak"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + + sums["total"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + states["total"] += round((consumption[consumption_key] * rate["value_inc_vat"]) / 100, 2) + + _LOGGER.debug(f'index: {index}; start: {start}; sums: {sums}; states: {states}; added: {(index) % 2 == 1}') + + if index % 2 == 1: + total_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["total"], + state=states["total"] + ) + ) + + off_peak_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["off_peak"], + state=states["off_peak"] + ) + ) + + peak_statistics.append( + StatisticData( + start=start, + last_reset=last_reset, + sum=sums["peak"], + state=states["peak"] + ) + ) + + return { + "total": total_statistics, + "peak": peak_statistics, + "off_peak": off_peak_statistics + } + +async def async_get_last_sum(hass: HomeAssistant, latest_date: datetime, statistic_id: str) -> float: + last_total_stat = await get_instance(hass).async_add_executor_job( + statistics_during_period, + hass, + latest_date - timedelta(days=7), + latest_date, + {statistic_id}, + "hour", + None, + {"sum"} + ) + total_sum = last_total_stat[statistic_id][-1]["sum"] if statistic_id in last_total_stat and len(last_total_stat[statistic_id]) > 0 else 0 + + return total_sum \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/consumption.py b/custom_components/octopus_energy/statistics/consumption.py new file mode 100644 index 00000000..f9089e10 --- /dev/null +++ b/custom_components/octopus_energy/statistics/consumption.py @@ -0,0 +1,78 @@ +import logging +from . import (build_consumption_statistics, async_get_last_sum) + +from homeassistant.core import HomeAssistant +from homeassistant.components.recorder.models import StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics +) + +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_import_external_statistics_from_consumption( + hass: HomeAssistant, + unique_id: str, + name: str, + consumptions, + rates, + unit_of_measurement: str, + consumption_key: str, + include_peak_off_peak: bool = True + ): + if (consumptions is None or len(consumptions) < 1 or rates is None or len(rates) < 1): + return + + statistic_id = f"{DOMAIN}:{unique_id}".lower() + + # Our sum needs to be based from the last total, so we need to grab the last record from the previous day + total_sum = await async_get_last_sum(hass, consumptions[0]["from"], statistic_id) + + peak_statistic_id = f'{statistic_id}_peak' + peak_sum = await async_get_last_sum(hass, consumptions[0]["from"], peak_statistic_id) + + off_peak_statistic_id = f'{statistic_id}_off_peak' + off_peak_sum = await async_get_last_sum(hass, consumptions[0]["from"], off_peak_statistic_id) + + statistics = build_consumption_statistics(consumptions, rates, consumption_key, total_sum, peak_sum, off_peak_sum) + + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=name, + source=DOMAIN, + statistic_id=statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["total"] + ) + + if include_peak_off_peak: + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=f'{name} Peak', + source=DOMAIN, + statistic_id=peak_statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["peak"] + ) + + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=f'{name} Off Peak', + source=DOMAIN, + statistic_id=off_peak_statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["off_peak"] + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/cost.py b/custom_components/octopus_energy/statistics/cost.py new file mode 100644 index 00000000..61913162 --- /dev/null +++ b/custom_components/octopus_energy/statistics/cost.py @@ -0,0 +1,78 @@ +import logging +from . import (build_cost_statistics, async_get_last_sum) + +from homeassistant.core import HomeAssistant +from homeassistant.components.recorder.models import StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics +) + +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_import_external_statistics_from_cost( + hass: HomeAssistant, + unique_id: str, + name: str, + consumptions, + rates, + unit_of_measurement: str, + consumption_key: str, + include_peak_off_peak: bool = True + ): + if (consumptions is None or len(consumptions) < 1 or rates is None or len(rates) < 1): + return + + statistic_id = f"{DOMAIN}:{unique_id}".lower() + + # Our sum needs to be based from the last total, so we need to grab the last record from the previous day + total_sum = await async_get_last_sum(hass, consumptions[0]["from"], statistic_id) + + peak_statistic_id = f'{statistic_id}_peak' + peak_sum = await async_get_last_sum(hass, consumptions[0]["from"], peak_statistic_id) + + off_peak_statistic_id = f'{statistic_id}_off_peak' + off_peak_sum = await async_get_last_sum(hass, consumptions[0]["from"], off_peak_statistic_id) + + statistics = build_cost_statistics(consumptions, rates, consumption_key, total_sum, peak_sum, off_peak_sum) + + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=name, + source=DOMAIN, + statistic_id=statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["total"] + ) + + if (include_peak_off_peak): + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=f'{name} Peak', + source=DOMAIN, + statistic_id=peak_statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["peak"] + ) + + async_add_external_statistics( + hass, + StatisticMetaData( + has_mean=False, + has_sum=True, + name=f'{name} Off Peak', + source=DOMAIN, + statistic_id=off_peak_statistic_id, + unit_of_measurement=unit_of_measurement, + ), + statistics["off_peak"] + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/switch.py b/custom_components/octopus_energy/switch.py new file mode 100644 index 00000000..1f3b0bd2 --- /dev/null +++ b/custom_components/octopus_energy/switch.py @@ -0,0 +1,65 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) + +from .intelligent.smart_charge import OctopusEnergyIntelligentSmartCharge +from .intelligent.bump_charge import OctopusEnergyIntelligentBumpCharge +from .api_client import OctopusEnergyApiClient +from .intelligent import async_mock_intelligent_data, is_intelligent_tariff, mock_intelligent_device +from .utils import get_active_tariff_code + +from .const import ( + DATA_ACCOUNT_ID, + DATA_CLIENT, + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_INTELLIGENT_SETTINGS_COORDINATOR, + DATA_INTELLIGENT_DISPATCHES_COORDINATOR, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_intelligent_sensors(hass, async_add_entities) + + return True + +async def async_setup_intelligent_sensors(hass, async_add_entities): + _LOGGER.debug('Setting up intelligent sensors') + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + has_intelligent_tariff = False + if len(account_info["electricity_meter_points"]) > 0: + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if is_intelligent_tariff(tariff_code): + has_intelligent_tariff = True + break + + should_mock_intelligent_data = await async_mock_intelligent_data(hass) + if has_intelligent_tariff or should_mock_intelligent_data: + settings_coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + dispatches_coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + if should_mock_intelligent_data: + device = mock_intelligent_device() + else: + device = await client.async_get_intelligent_device(account_id) + + async_add_entities([ + OctopusEnergyIntelligentSmartCharge(hass, settings_coordinator, client, device, account_id), + OctopusEnergyIntelligentBumpCharge(hass, dispatches_coordinator, client, device, account_id) + ], True) \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/__init__.py b/custom_components/octopus_energy/target_rates/__init__.py new file mode 100644 index 00000000..99791489 --- /dev/null +++ b/custom_components/octopus_energy/target_rates/__init__.py @@ -0,0 +1,280 @@ +from datetime import datetime, timedelta +import math +import re +import logging + +from homeassistant.util.dt import (as_utc, parse_datetime) + +from ..const import REGEX_OFFSET_PARTS + +_LOGGER = logging.getLogger(__name__) + +def apply_offset(date_time: datetime, offset: str, inverse = False): + matches = re.search(REGEX_OFFSET_PARTS, offset) + if matches == None: + raise Exception(f'Unable to extract offset: {offset}') + + symbol = matches[1] + hours = float(matches[2]) + minutes = float(matches[3]) + seconds = float(matches[4]) + + if ((symbol == "-" and inverse == False) or (symbol != "-" and inverse == True)): + return date_time - timedelta(hours=hours, minutes=minutes, seconds=seconds) + + return date_time + timedelta(hours=hours, minutes=minutes, seconds=seconds) + +def __get_applicable_rates(current_date: datetime, target_start_time: str, target_end_time: str, rates, is_rolling_target: bool): + if (target_start_time is not None): + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z")) + else: + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + + if (target_end_time is not None): + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_end_time}:00%z")) + else: + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + timedelta(days=1) + + target_start = as_utc(target_start) + target_end = as_utc(target_end) + + if (target_start >= target_end): + _LOGGER.debug(f'{target_start} is after {target_end}, so setting target end to tomorrow') + if target_start > current_date: + target_start = target_start - timedelta(days=1) + else: + target_end = target_end + timedelta(days=1) + + # If our start date has passed, reset it to current_date to avoid picking a slot in the past + if (is_rolling_target == True and target_start < current_date and current_date < target_end): + _LOGGER.debug(f'Rolling target and {target_start} is in the past. Setting start to {current_date}') + target_start = current_date + + # If our start and end are both in the past, then look to the next day + if (target_start < current_date and target_end < current_date): + target_start = target_start + timedelta(days=1) + target_end = target_end + timedelta(days=1) + + _LOGGER.debug(f'Finding rates between {target_start} and {target_end}') + + # Retrieve the rates that are applicable for our target rate + applicable_rates = [] + if rates is not None: + for rate in rates: + if rate["valid_from"] >= target_start and (target_end is None or rate["valid_to"] <= target_end): + applicable_rates.append(rate) + + # Make sure that we have enough rates that meet our target period + date_diff = target_end - target_start + hours = (date_diff.days * 24) + (date_diff.seconds // 3600) + periods = hours * 2 + if len(applicable_rates) < periods: + _LOGGER.debug(f'Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_rates)}') + return None + + return applicable_rates + +def __get_valid_to(rate): + return rate["valid_to"] + +def calculate_continuous_times( + current_date: datetime, + target_start_time: str, + target_end_time: str, + target_hours: float, + rates, + is_rolling_target = True, + search_for_highest_rate = False, + find_last_rates = False + ): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + + applicable_rates.sort(key=__get_valid_to, reverse=find_last_rates) + applicable_rates_count = len(applicable_rates) + total_required_rates = math.ceil(target_hours * 2) + + best_continuous_rates = None + best_continuous_rates_total = None + + _LOGGER.debug(f'{applicable_rates_count} applicable rates found') + + # Loop through our rates and try and find the block of time that meets our desired + # hours and has the lowest combined rates + for index, rate in enumerate(applicable_rates): + continuous_rates = [rate] + continuous_rates_total = rate["value_inc_vat"] + + for offset in range(1, total_required_rates): + if (index + offset) < applicable_rates_count: + offset_rate = applicable_rates[(index + offset)] + continuous_rates.append(offset_rate) + continuous_rates_total += offset_rate["value_inc_vat"] + else: + break + + if ((best_continuous_rates is None or (search_for_highest_rate == False and continuous_rates_total < best_continuous_rates_total) or (search_for_highest_rate and continuous_rates_total > best_continuous_rates_total)) and len(continuous_rates) == total_required_rates): + best_continuous_rates = continuous_rates + best_continuous_rates_total = continuous_rates_total + else: + _LOGGER.debug(f'Total rates for current block {continuous_rates_total}. Total rates for best block {best_continuous_rates_total}') + + if best_continuous_rates is not None: + # Make sure our rates are in ascending order before returning + best_continuous_rates.sort(key=__get_valid_to) + return best_continuous_rates + + return [] + +def calculate_intermittent_times( + current_date: datetime, + target_start_time: str, + target_end_time: str, + target_hours: float, + rates, + is_rolling_target = True, + search_for_highest_rate = False, + find_last_rates = False + ): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + + total_required_rates = math.ceil(target_hours * 2) + + if find_last_rates: + if search_for_highest_rate: + applicable_rates.sort(key= lambda rate: (-rate["value_inc_vat"], -rate["valid_to"].timestamp())) + else: + applicable_rates.sort(key= lambda rate: (rate["value_inc_vat"], -rate["valid_to"].timestamp())) + else: + if search_for_highest_rate: + applicable_rates.sort(key= lambda rate: (-rate["value_inc_vat"], rate["valid_to"])) + else: + applicable_rates.sort(key= lambda rate: (rate["value_inc_vat"], rate["valid_to"])) + + applicable_rates = applicable_rates[:total_required_rates] + + _LOGGER.debug(f'{len(applicable_rates)} applicable rates found') + + if (len(applicable_rates) < total_required_rates): + return [] + + # Make sure our rates are in ascending order before returning + applicable_rates.sort(key=__get_valid_to) + return applicable_rates + +def get_target_rate_info(current_date: datetime, applicable_rates, offset: str = None): + is_active = False + next_time = None + current_duration_in_hours = 0 + next_duration_in_hours = 0 + total_applicable_rates = len(applicable_rates) + + overall_total_cost = 0 + overall_min_cost = None + overall_max_cost = None + + current_average_cost = None + current_min_cost = None + current_max_cost = None + + next_average_cost = None + next_min_cost = None + next_max_cost = None + + if (total_applicable_rates > 0): + + # Find the applicable rates that when combine become a continuous block. This is more for + # intermittent rates. + applicable_rates.sort(key=__get_valid_to) + applicable_rate_blocks = list() + block_valid_from = applicable_rates[0]["valid_from"] + + total_cost = 0 + min_cost = None + max_cost = None + + for index, rate in enumerate(applicable_rates): + if (index > 0 and applicable_rates[index - 1]["valid_to"] != rate["valid_from"]): + diff = applicable_rates[index - 1]["valid_to"] - block_valid_from + minutes = diff.total_seconds() / 60 + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[index - 1]["valid_to"], + "duration_in_hours": minutes / 60, + "average_cost": total_cost / (minutes / 30), + "min_cost": min_cost, + "max_cost": max_cost + }) + + block_valid_from = rate["valid_from"] + total_cost = 0 + min_cost = None + max_cost = None + + total_cost += rate["value_inc_vat"] + if min_cost is None or min_cost > rate["value_inc_vat"]: + min_cost = rate["value_inc_vat"] + + if max_cost is None or max_cost < rate["value_inc_vat"]: + max_cost = rate["value_inc_vat"] + + overall_total_cost += rate["value_inc_vat"] + if overall_min_cost is None or overall_min_cost > rate["value_inc_vat"]: + overall_min_cost = rate["value_inc_vat"] + + if overall_max_cost is None or overall_max_cost < rate["value_inc_vat"]: + overall_max_cost = rate["value_inc_vat"] + + # Make sure our final block is added + diff = applicable_rates[-1]["valid_to"] - block_valid_from + minutes = diff.total_seconds() / 60 + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[-1]["valid_to"], + "duration_in_hours": minutes / 60, + "average_cost": total_cost / (minutes / 30), + "min_cost": min_cost, + "max_cost": max_cost + }) + + # Find out if we're within an active block, or find the next block + for index, rate in enumerate(applicable_rate_blocks): + if (offset is not None): + valid_from = apply_offset(rate["valid_from"], offset) + valid_to = apply_offset(rate["valid_to"], offset) + else: + valid_from = rate["valid_from"] + valid_to = rate["valid_to"] + + if current_date >= valid_from and current_date < valid_to: + current_duration_in_hours = rate["duration_in_hours"] + current_average_cost = rate["average_cost"] + current_min_cost = rate["min_cost"] + current_max_cost = rate["max_cost"] + is_active = True + elif current_date < valid_from: + next_time = valid_from + next_duration_in_hours = rate["duration_in_hours"] + next_average_cost = rate["average_cost"] + next_min_cost = rate["min_cost"] + next_max_cost = rate["max_cost"] + break + + return { + "is_active": is_active, + "overall_average_cost": round(overall_total_cost / total_applicable_rates, 5) if total_applicable_rates > 0 else 0, + "overall_min_cost": overall_min_cost, + "overall_max_cost": overall_max_cost, + "current_duration_in_hours": current_duration_in_hours, + "current_average_cost": current_average_cost, + "current_min_cost": current_min_cost, + "current_max_cost": current_max_cost, + "next_time": apply_offset(next_time, offset) if next_time is not None and offset is not None else next_time, + "next_duration_in_hours": next_duration_in_hours, + "next_average_cost": next_average_cost, + "next_min_cost": next_min_cost, + "next_max_cost": next_max_cost, + } \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/config.py b/custom_components/octopus_energy/target_rates/config.py new file mode 100644 index 00000000..a91d4a83 --- /dev/null +++ b/custom_components/octopus_energy/target_rates/config.py @@ -0,0 +1,106 @@ +import re +from datetime import timedelta + +from homeassistant.util.dt import (parse_datetime) + +from ..const import ( + CONFIG_TARGET_END_TIME, + CONFIG_TARGET_HOURS, + CONFIG_TARGET_MPAN, + CONFIG_TARGET_NAME, + CONFIG_TARGET_OFFSET, + CONFIG_TARGET_START_TIME, + REGEX_ENTITY_NAME, + REGEX_HOURS, + REGEX_OFFSET_PARTS, + REGEX_TIME +) + +from ..utils import get_active_tariff_code +from ..utils.tariff_check import is_agile_tariff + +def get_meter_tariffs(account_info, now): + meters = {} + if account_info is not None and len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + active_tariff_code = get_active_tariff_code(now, point["agreements"]) + if active_tariff_code is not None: + meters[point["mpan"]] = active_tariff_code + + return meters + +def is_time_frame_long_enough(hours, start_time, end_time): + start_time = parse_datetime(f"2023-08-01T{start_time}:00Z") + end_time = parse_datetime(f"2023-08-01T{end_time}:00Z") + if end_time <= start_time: + end_time = end_time + timedelta(days=1) + + time_diff = end_time - start_time + available_minutes = time_diff.total_seconds() / 60 + target_minutes = (hours / 0.5) * 30 + + return available_minutes >= target_minutes + + +agile_start = parse_datetime(f"2023-08-01T16:00:00Z") +agile_end = parse_datetime(f"2023-08-01T23:00:00Z") + +def is_in_agile_darkzone(start_time, end_time): + start_time = parse_datetime(f"2023-08-01T{start_time}:00Z") + end_time = parse_datetime(f"2023-08-01T{end_time}:00Z") + if end_time <= start_time: + end_time = end_time + timedelta(days=1) + + return start_time < agile_start and end_time > agile_end + +def validate_target_rate_config(data, account_info, now): + errors = {} + + matches = re.search(REGEX_ENTITY_NAME, data[CONFIG_TARGET_NAME]) + if matches is None: + errors[CONFIG_TARGET_NAME] = "invalid_target_name" + + # For some reason float type isn't working properly - reporting user input malformed + if isinstance(data[CONFIG_TARGET_HOURS], float) == False: + matches = re.search(REGEX_HOURS, data[CONFIG_TARGET_HOURS]) + if matches is None: + errors[CONFIG_TARGET_HOURS] = "invalid_target_hours" + else: + data[CONFIG_TARGET_HOURS] = float(data[CONFIG_TARGET_HOURS]) + + if CONFIG_TARGET_HOURS not in errors: + if data[CONFIG_TARGET_HOURS] % 0.5 != 0: + errors[CONFIG_TARGET_HOURS] = "invalid_target_hours" + + if CONFIG_TARGET_START_TIME in data: + matches = re.search(REGEX_TIME, data[CONFIG_TARGET_START_TIME]) + if matches is None: + errors[CONFIG_TARGET_START_TIME] = "invalid_target_time" + + if CONFIG_TARGET_END_TIME in data: + matches = re.search(REGEX_TIME, data[CONFIG_TARGET_END_TIME]) + if matches is None: + errors[CONFIG_TARGET_END_TIME] = "invalid_target_time" + + if CONFIG_TARGET_OFFSET in data: + matches = re.search(REGEX_OFFSET_PARTS, data[CONFIG_TARGET_OFFSET]) + if matches is None: + errors[CONFIG_TARGET_OFFSET] = "invalid_offset" + + start_time = data[CONFIG_TARGET_START_TIME] if CONFIG_TARGET_START_TIME in data else "00:00" + end_time = data[CONFIG_TARGET_END_TIME] if CONFIG_TARGET_END_TIME in data else "00:00" + + if CONFIG_TARGET_HOURS not in errors and CONFIG_TARGET_START_TIME not in errors and CONFIG_TARGET_END_TIME not in errors: + if is_time_frame_long_enough(data[CONFIG_TARGET_HOURS], start_time, end_time) == False: + errors[CONFIG_TARGET_HOURS] = "invalid_hours_time_frame" + + meter_tariffs = get_meter_tariffs(account_info, now) + if (data[CONFIG_TARGET_MPAN] not in meter_tariffs): + errors[CONFIG_TARGET_MPAN] = "invalid_mpan" + else: + tariff = meter_tariffs[data[CONFIG_TARGET_MPAN]] + if is_agile_tariff(tariff): + if is_in_agile_darkzone(start_time, end_time): + errors[CONFIG_TARGET_END_TIME] = "invalid_end_time_agile" + + return errors \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/repairs.py b/custom_components/octopus_energy/target_rates/repairs.py new file mode 100644 index 00000000..55457981 --- /dev/null +++ b/custom_components/octopus_energy/target_rates/repairs.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from homeassistant.helpers import issue_registry as ir + +from ..const import CONFIG_TARGET_NAME, DOMAIN +from .config import validate_target_rate_config + +def check_for_errors(hass, config, account_info, now: datetime): + if account_info is not None: + errors = validate_target_rate_config(config, account_info, now) + keys = list(errors.keys()) + target_rate_name = config[CONFIG_TARGET_NAME] + repair_key = f"invalid_target_rate_{target_rate_name}" + if len(keys) > 0: + ir.async_create_issue( + hass, + DOMAIN, + repair_key, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/invalid_target_rate.md", + translation_key="invalid_target_rate", + translation_placeholders={ "name": target_rate_name }, + ) + else: + ir.async_delete_issue(hass, DOMAIN, repair_key) \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/target_rate.py b/custom_components/octopus_energy/target_rates/target_rate.py new file mode 100644 index 00000000..b5bdbadb --- /dev/null +++ b/custom_components/octopus_energy/target_rates/target_rate.py @@ -0,0 +1,254 @@ +import logging + +import re +import voluptuous as vol + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import generate_entity_id + +from homeassistant.util.dt import (utcnow, now) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity +) +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, +) + +from homeassistant.helpers import translation + +from ..const import ( + CONFIG_TARGET_NAME, + CONFIG_TARGET_HOURS, + CONFIG_TARGET_TYPE, + CONFIG_TARGET_START_TIME, + CONFIG_TARGET_END_TIME, + CONFIG_TARGET_MPAN, + CONFIG_TARGET_ROLLING_TARGET, + CONFIG_TARGET_LAST_RATES, + CONFIG_TARGET_INVERT_TARGET_RATES, + CONFIG_TARGET_OFFSET, + DATA_ACCOUNT, + DOMAIN, + + REGEX_HOURS, + REGEX_TIME, + REGEX_OFFSET_PARTS, +) + +from . import ( + calculate_continuous_times, + calculate_intermittent_times, + get_target_rate_info +) + +from .config import validate_target_rate_config +from ..target_rates.repairs import check_for_errors + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyTargetRate(CoordinatorEntity, BinarySensorEntity): + """Sensor for calculating when a target should be turned on or off.""" + + def __init__(self, hass: HomeAssistant, coordinator, config, is_export): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + + self._state = None + self._config = config + self._is_export = is_export + self._attributes = self._config.copy() + self._is_export = is_export + self._attributes["is_target_export"] = is_export + + is_rolling_target = True + if CONFIG_TARGET_ROLLING_TARGET in self._config: + is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET] + self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target + + find_last_rates = False + if CONFIG_TARGET_LAST_RATES in self._config: + find_last_rates = self._config[CONFIG_TARGET_LAST_RATES] + self._attributes[CONFIG_TARGET_LAST_RATES] = find_last_rates + + self._target_rates = [] + + self._hass = hass + self.entity_id = generate_entity_id("binary_sensor.{}", self.unique_id, hass=hass) + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_target_{self._config[CONFIG_TARGET_NAME]}" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Target {self._config[CONFIG_TARGET_NAME]}" + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:camera-timer" + + @property + def extra_state_attributes(self): + """Attributes of the sensor.""" + return self._attributes + + @property + def is_on(self): + """Determines if the target rate sensor is active.""" + if CONFIG_TARGET_OFFSET in self._config: + offset = self._config[CONFIG_TARGET_OFFSET] + else: + offset = None + + check_for_errors(self._hass, self._config, self._hass.data[DOMAIN][DATA_ACCOUNT], now()) + + # Find the current rate. Rates change a maximum of once every 30 minutes. + current_date = utcnow() + if (current_date.minute % 30) == 0 or len(self._target_rates) == 0: + _LOGGER.debug(f'Updating OctopusEnergyTargetRate {self._config[CONFIG_TARGET_NAME]}') + + # If all of our target times have passed, it's time to recalculate the next set + all_rates_in_past = True + for rate in self._target_rates: + if rate["valid_to"] > current_date: + all_rates_in_past = False + break + + if all_rates_in_past: + if self.coordinator is not None and self.coordinator.data is not None: + all_rates = self.coordinator.data + + # Retrieve our rates. For backwards compatibility, if CONFIG_TARGET_MPAN is not set, then pick the first set + if CONFIG_TARGET_MPAN not in self._config: + _LOGGER.debug(f"'CONFIG_TARGET_MPAN' not set.'{len(all_rates)}' rates available. Retrieving the first rate.") + all_rates = next(iter(all_rates.values())) + else: + _LOGGER.debug(f"Retrieving rates for '{self._config[CONFIG_TARGET_MPAN]}'") + all_rates = all_rates.get(self._config[CONFIG_TARGET_MPAN]) + else: + _LOGGER.debug(f"Rate data missing. Setting to empty array") + all_rates = [] + + _LOGGER.debug(f'{len(all_rates) if all_rates is not None else None} rate periods found') + + start_time = None + if CONFIG_TARGET_START_TIME in self._config: + start_time = self._config[CONFIG_TARGET_START_TIME] + + end_time = None + if CONFIG_TARGET_END_TIME in self._config: + end_time = self._config[CONFIG_TARGET_END_TIME] + + # True by default for backwards compatibility + is_rolling_target = True + if CONFIG_TARGET_ROLLING_TARGET in self._config: + is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET] + + find_last_rates = False + if CONFIG_TARGET_LAST_RATES in self._config: + find_last_rates = self._config[CONFIG_TARGET_LAST_RATES] + + target_hours = float(self._config[CONFIG_TARGET_HOURS]) + + invert_target_rates = False + if (CONFIG_TARGET_INVERT_TARGET_RATES in self._config): + invert_target_rates = self._config[CONFIG_TARGET_INVERT_TARGET_RATES] + + find_highest_rates = (self._is_export and invert_target_rates == False) or (self._is_export == False and invert_target_rates) + + if (self._config[CONFIG_TARGET_TYPE] == "Continuous"): + self._target_rates = calculate_continuous_times( + now(), + start_time, + end_time, + target_hours, + all_rates, + is_rolling_target, + find_highest_rates, + find_last_rates + ) + elif (self._config[CONFIG_TARGET_TYPE] == "Intermittent"): + self._target_rates = calculate_intermittent_times( + now(), + start_time, + end_time, + target_hours, + all_rates, + is_rolling_target, + find_highest_rates, + find_last_rates + ) + else: + _LOGGER.error(f"Unexpected target type: {self._config[CONFIG_TARGET_TYPE]}") + + self._attributes["target_times"] = self._target_rates + + active_result = get_target_rate_info(current_date, self._target_rates, offset) + + self._attributes["overall_average_cost"] = f'{active_result["overall_average_cost"]}p' if active_result["overall_average_cost"] is not None else None + self._attributes["overall_min_cost"] = f'{active_result["overall_min_cost"]}p' if active_result["overall_min_cost"] is not None else None + self._attributes["overall_max_cost"] = f'{active_result["overall_max_cost"]}p' if active_result["overall_max_cost"] is not None else None + + self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"] + self._attributes["current_average_cost"] = f'{active_result["current_average_cost"]}p' if active_result["current_average_cost"] is not None else None + self._attributes["current_min_cost"] = f'{active_result["current_min_cost"]}p' if active_result["current_min_cost"] is not None else None + self._attributes["current_max_cost"] = f'{active_result["current_max_cost"]}p' if active_result["current_max_cost"] is not None else None + + self._attributes["next_time"] = active_result["next_time"] + self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"] + self._attributes["next_average_cost"] = f'{active_result["next_average_cost"]}p' if active_result["next_average_cost"] is not None else None + self._attributes["next_min_cost"] = f'{active_result["next_min_cost"]}p' if active_result["next_min_cost"] is not None else None + self._attributes["next_max_cost"] = f'{active_result["next_max_cost"]}p' if active_result["next_max_cost"] is not None else None + + self._state = active_result["is_active"] + + return self._state + + @callback + async def async_update_config(self, target_start_time=None, target_end_time=None, target_hours=None, target_offset=None): + """Update sensors config""" + + config = dict(self._config) + if target_hours is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_hours = target_hours.strip('\"') + config.update({ + CONFIG_TARGET_HOURS: trimmed_target_hours + }) + + if target_start_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_start_time = target_start_time.strip('\"') + config.update({ + CONFIG_TARGET_START_TIME: trimmed_target_start_time + }) + + if target_end_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_end_time = target_end_time.strip('\"') + config.update({ + CONFIG_TARGET_END_TIME: trimmed_target_end_time + }) + + if target_offset is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_offset = target_offset.strip('\"') + config.update({ + CONFIG_TARGET_OFFSET: trimmed_target_offset + }) + + errors = validate_target_rate_config(config, self._hass.data[DOMAIN][DATA_ACCOUNT], now()) + keys = list(errors.keys()) + if (len(keys)) > 0: + translations = await translation.async_get_translations(self._hass, self._hass.config.language, "options", {DOMAIN}) + raise vol.Invalid(translations[f'component.{DOMAIN}.options.error.{errors[keys[0]]}']) + + self._config = config + self._attributes = self._config.copy() + self._attributes["is_target_export"] = self._is_export + self._target_rates = [] + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/target_sensor_utils.py b/custom_components/octopus_energy/target_sensor_utils.py new file mode 100644 index 00000000..c27e52c7 --- /dev/null +++ b/custom_components/octopus_energy/target_sensor_utils.py @@ -0,0 +1,184 @@ +from datetime import datetime, timedelta +import math +from homeassistant.util.dt import (as_utc, parse_datetime) +from .utils import (apply_offset) +import logging + +_LOGGER = logging.getLogger(__name__) + +def __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target): + if (target_start_time is not None): + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z")) + else: + target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + + if (target_end_time is not None): + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_end_time}:00%z")) + else: + target_end = parse_datetime(current_date.strftime(f"%Y-%m-%dT00:00:00%z")) + timedelta(days=1) + + target_start = as_utc(target_start) + target_end = as_utc(target_end) + + if (target_start >= target_end): + _LOGGER.debug(f'{target_start} is after {target_end}, so setting target end to tomorrow') + if target_start > current_date: + target_start = target_start - timedelta(days=1) + else: + target_end = target_end + timedelta(days=1) + + # If our start date has passed, reset it to current_date to avoid picking a slot in the past + if (is_rolling_target == True and target_start < current_date and current_date < target_end): + _LOGGER.debug(f'Rolling target and {target_start} is in the past. Setting start to {current_date}') + target_start = current_date + + # Apply our offset so we make sure our target turns on within the specified timeframe + if (target_start_offset is not None): + _LOGGER.debug(f'Offsetting time period') + target_start = apply_offset(target_start, target_start_offset, True) + target_end = apply_offset(target_end, target_start_offset, True) + + # If our start and end are both in the past, then look to the next day + if (target_start < current_date and target_end < current_date): + target_start = target_start + timedelta(days=1) + target_end = target_end + timedelta(days=1) + + _LOGGER.debug(f'Finding rates between {target_start} and {target_end}') + + # Retrieve the rates that are applicable for our target rate + applicable_rates = [] + if rates != None: + for rate in rates: + if rate["valid_from"] >= target_start and (target_end == None or rate["valid_to"] <= target_end): + applicable_rates.append(rate) + + # Make sure that we have enough rates that meet our target period + date_diff = target_end - target_start + hours = (date_diff.days * 24) + (date_diff.seconds // 3600) + periods = hours * 2 + if len(applicable_rates) < periods: + _LOGGER.debug(f'Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_rates)}') + return None + + return applicable_rates + +def __get_rate(rate): + return rate["value_inc_vat"] + +def __get_valid_to(rate): + return rate["valid_to"] + +def calculate_continuous_times(current_date, target_start_time, target_end_time, target_hours, rates, target_start_offset = None, is_rolling_target = True, search_for_highest_rate = False): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target) + if (applicable_rates is None): + return [] + + applicable_rates_count = len(applicable_rates) + total_required_rates = math.ceil(target_hours * 2) + + best_continuous_rates = None + best_continuous_rates_total = None + + _LOGGER.debug(f'{applicable_rates_count} applicable rates found') + + # Loop through our rates and try and find the block of time that meets our desired + # hours and has the lowest combined rates + for index, rate in enumerate(applicable_rates): + continuous_rates = [rate] + continuous_rates_total = rate["value_inc_vat"] + + for offset in range(1, total_required_rates): + if (index + offset) < applicable_rates_count: + offset_rate = applicable_rates[(index + offset)] + continuous_rates.append(offset_rate) + continuous_rates_total += offset_rate["value_inc_vat"] + else: + break + + if ((best_continuous_rates == None or (search_for_highest_rate == False and continuous_rates_total < best_continuous_rates_total) or (search_for_highest_rate and continuous_rates_total > best_continuous_rates_total)) and len(continuous_rates) == total_required_rates): + best_continuous_rates = continuous_rates + best_continuous_rates_total = continuous_rates_total + else: + _LOGGER.debug(f'Total rates for current block {continuous_rates_total}. Total rates for best block {best_continuous_rates_total}') + + if best_continuous_rates is not None: + # Make sure our rates are in ascending order before returning + best_continuous_rates.sort(key=__get_valid_to) + return best_continuous_rates + + return [] + +def calculate_intermittent_times(current_date, target_start_time, target_end_time, target_hours, rates, target_start_offset = None, is_rolling_target = True, search_for_highest_rate = False): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target) + if (applicable_rates is None): + return [] + + total_required_rates = math.ceil(target_hours * 2) + + applicable_rates.sort(key=__get_rate, reverse=search_for_highest_rate) + applicable_rates = applicable_rates[:total_required_rates] + + _LOGGER.debug(f'{len(applicable_rates)} applicable rates found') + + if (len(applicable_rates) < total_required_rates): + return [] + + # Make sure our rates are in ascending order before returning + applicable_rates.sort(key=__get_valid_to) + return applicable_rates + +def is_target_rate_active(current_date: datetime, applicable_rates, offset: str = None): + is_active = False + next_time = None + current_duration_in_hours = 0 + next_duration_in_hours = 0 + total_applicable_rates = len(applicable_rates) + + if (total_applicable_rates > 0): + + # Work our our rate blocks. This is more for intermittent target rates + applicable_rates.sort(key=__get_valid_to) + applicable_rate_blocks = list() + block_valid_from = applicable_rates[0]["valid_from"] + for index, rate in enumerate(applicable_rates): + if (index > 0 and applicable_rates[index - 1]["valid_to"] != rate["valid_from"]): + diff = applicable_rates[index - 1]["valid_to"] - block_valid_from + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[index - 1]["valid_to"], + "duration_in_hours": diff.total_seconds() / 60 / 60 + }) + + block_valid_from = rate["valid_from"] + + # Make sure our final block is added + diff = applicable_rates[-1]["valid_to"] - block_valid_from + applicable_rate_blocks.append({ + "valid_from": block_valid_from, + "valid_to": applicable_rates[-1]["valid_to"], + "duration_in_hours": diff.total_seconds() / 60 / 60 + }) + + # Find out if we're within an active block, or find the next block + for index, rate in enumerate(applicable_rate_blocks): + if (offset != None): + valid_from = apply_offset(rate["valid_from"], offset) + valid_to = apply_offset(rate["valid_to"], offset) + else: + valid_from = rate["valid_from"] + valid_to = rate["valid_to"] + + if current_date >= valid_from and current_date < valid_to: + current_duration_in_hours = rate["duration_in_hours"] + is_active = True + elif current_date < valid_from: + next_time = valid_from + next_duration_in_hours = rate["duration_in_hours"] + break + + return { + "is_active": is_active, + "current_duration_in_hours": current_duration_in_hours, + "next_time": next_time, + "next_duration_in_hours": next_duration_in_hours + } diff --git a/custom_components/octopus_energy/text.py b/custom_components/octopus_energy/text.py new file mode 100644 index 00000000..56aac4c9 --- /dev/null +++ b/custom_components/octopus_energy/text.py @@ -0,0 +1,77 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) +from homeassistant.core import HomeAssistant + +from .electricity.previous_accumulative_cost_override_tariff import OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride +from .gas.previous_accumulative_cost_override_tariff import OctopusEnergyPreviousAccumulativeGasCostTariffOverride + +from .utils import (get_active_tariff_code) +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_CLIENT, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_default_sensors(hass, entry, async_add_entities) + +async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_entities): + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + client = hass.data[DOMAIN][DATA_CLIENT] + + entities = [] + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + + if len(account_info["electricity_meter_points"]) > 0: + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + electricity_tariff_code = get_active_tariff_code(now, point["agreements"]) + if electricity_tariff_code is not None: + for meter in point["meters"]: + _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + + if meter["is_smart_meter"] == True: + entities.append(OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride(hass, client, electricity_tariff_code, meter, point)) + else: + for meter in point["meters"]: + _LOGGER.info(f'Skipping electricity meter due to no active agreement; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + _LOGGER.info(f'agreements: {point["agreements"]}') + else: + _LOGGER.info('No electricity meters available') + + if len(account_info["gas_meter_points"]) > 0: + for point in account_info["gas_meter_points"]: + # We only care about points that have active agreements + gas_tariff_code = get_active_tariff_code(now, point["agreements"]) + if gas_tariff_code is not None: + for meter in point["meters"]: + _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + + if meter["is_smart_meter"] == True: + entities.append(OctopusEnergyPreviousAccumulativeGasCostTariffOverride(hass, client, gas_tariff_code, meter, point)) + else: + for meter in point["meters"]: + _LOGGER.info(f'Skipping gas meter due to no active agreement; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + _LOGGER.info(f'agreements: {point["agreements"]}') + else: + _LOGGER.info('No gas meters available') + + async_add_entities(entities, True) diff --git a/custom_components/octopus_energy/time.py b/custom_components/octopus_energy/time.py new file mode 100644 index 00000000..b5605b25 --- /dev/null +++ b/custom_components/octopus_energy/time.py @@ -0,0 +1,61 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow) + +from .intelligent.ready_time import OctopusEnergyIntelligentReadyTime +from .api_client import OctopusEnergyApiClient +from .intelligent import async_mock_intelligent_data, is_intelligent_tariff, mock_intelligent_device +from .utils import get_active_tariff_code + +from .const import ( + DATA_ACCOUNT_ID, + DATA_CLIENT, + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_INTELLIGENT_SETTINGS_COORDINATOR, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_intelligent_sensors(hass, async_add_entities) + + return True + +async def async_setup_intelligent_sensors(hass, async_add_entities): + _LOGGER.debug('Setting up intelligent sensors') + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + has_intelligent_tariff = False + if len(account_info["electricity_meter_points"]) > 0: + + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if is_intelligent_tariff(tariff_code): + has_intelligent_tariff = True + break + + should_mock_intelligent_data = await async_mock_intelligent_data(hass) + if has_intelligent_tariff or should_mock_intelligent_data: + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + if should_mock_intelligent_data: + device = mock_intelligent_device() + else: + device = await client.async_get_intelligent_device(account_id) + + async_add_entities([ + OctopusEnergyIntelligentReadyTime(hass, coordinator, client, device, account_id), + ], True) \ No newline at end of file diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json new file mode 100644 index 00000000..1f761d26 --- /dev/null +++ b/custom_components/octopus_energy/translations/en.json @@ -0,0 +1,108 @@ +{ + "title": "Octopus Energy", + "config": { + "step": { + "user": { + "description": "Setup your basic account information. This can be found at https://octopus.energy/dashboard/developer/.", + "data": { + "Api key": "Api key", + "Account Id": "Your account Id (e.g. A-AAAA1111)", + "supports_live_consumption": "I have a Home Mini - https://octopus.energy/blog/octopus-home-mini/", + "live_consumption_refresh_in_minutes": "Home Mini refresh rate in minutes", + "calorific_value": "Gas calorific value. This can be found on your gas statement and can change from time to time.", + "electricity_price_cap": "Optional electricity price cap in pence", + "gas_price_cap": "Optional gas price cap in pence" + } + }, + "target_rate": { + "description": "Setup a target rate period. Continuous target will find the cheapest continuous period for your target hours. While intermittent will find the cheapest periods with potential gaps, which when combined will meet your target hours. Full documentation can be found at https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/setup_target_rate.md", + "data": { + "entity_id": "The name of your target", + "Hours": "The hours you require in decimal format.", + "Type": "The type of target you're after", + "MPAN": "The MPAN number of the meter to apply the target to", + "Start time": "The minimum time to start the device", + "End time": "The maximum time to stop the device", + "offset": "The offset to apply to the scheduled block to be considered active", + "rolling_target": "Re-evaluate multiple times a day", + "last_rates": "Find last applicable rates", + "target_invert_target_rates": "Invert targeted rates" + } + } + }, + "error": { + "account_not_found": "Account information was not found", + "invalid_target_hours": "Target hours must be in half hour increments (e.g. 0.5 = 30 minutes; 1 = 60 minutes).", + "invalid_target_name": "Name must only include lower case alpha characters and underscore (e.g. my_target)", + "invalid_target_time": "Must be in the format HH:MM", + "invalid_offset": "Offset must be in the form of HH:MM:SS with an optional negative symbol", + "invalid_hours_time_frame": "The target hours do not fit in the elected target time frame", + "invalid_mpan": "Meter not found in account with an active tariff", + "invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information." + }, + "abort": { + "not_supported": "Configuration for target rates is not supported at the moment." + } + }, + "options": { + "step": { + "user": { + "title": "Update Account Info", + "description": "Update your basic account information. This can be found at https://octopus.energy/dashboard/developer/.", + "data": { + "Api key": "Api key", + "supports_live_consumption": "I have a Home Mini - https://octopus.energy/blog/octopus-home-mini/", + "live_consumption_refresh_in_minutes": "Home Mini refresh rate in minutes", + "calorific_value": "Gas calorific value. This can be found on your gas statement and can change from time to time.", + "electricity_price_cap": "Optional electricity price cap in pence", + "clear_electricity_price_cap": "Clear electricity price cap", + "gas_price_cap": "Optional gas price cap in pence", + "clear_gas_price_cap": "Clear Gas price cap" + } + }, + "target_rate": { + "title": "Update Target Rate", + "description": "Update the settings for your target rate sensor, which can be used to help you save energy and money. Full documentation can be found at https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/setup_target_rate.md", + "data": { + "Hours": "The hours you require in decimal format.", + "MPAN": "The MPAN number of the meter to apply the target to", + "Start time": "The minimum time to start the device", + "End time": "The maximum time to stop the device", + "offset": "The offset to apply to the scheduled block to be considered active", + "rolling_target": "Re-evaluate multiple times a day", + "last_rates": "Find last applicable rates", + "target_invert_target_rates": "Invert targeted rates" + } + } + }, + "error": { + "invalid_target_hours": "Target hours must be in half hour increments (e.g. 0.5 = 30 minutes; 1 = 60 minutes).", + "invalid_target_time": "Must be in the format HH:MM", + "invalid_offset": "Offset must be in the form of HH:MM:SS with an optional negative symbol", + "invalid_hours_time_frame": "The target hours do not fit in the elected target time frame", + "invalid_mpan": "Meter not found in account with an active tariff", + "invalid_end_time_agile": "Target time not fit for agile tariffs. Please consult target rate documentation for more information." + }, + "abort": { + "not_supported": "Configuration for target rates is not supported at the moment." + } + }, + "issues": { + "account_not_found": { + "title": "Account \"{account_id}\" not found", + "description": "The integration failed to retrieve the information associated with your configured account. Please check your account exists and that your API key is valid. Click 'Learn More' to find out how to fix this." + }, + "unknown_tariff_format": { + "title": "Invalid format - {type} - {tariff_code}", + "description": "The tariff \"{tariff_code}\" associated with your {type} meter is not in an expected format. Click on 'Learn More' with instructions on what to do next." + }, + "unknown_tariff": { + "title": "Unknown tariff - {type} - {tariff_code}", + "description": "The tariff \"{tariff_code}\" associated with your {type} meter has not been found. Click on 'Learn More' with instructions on what to do next." + }, + "invalid_target_rate": { + "title": "Invalid target rate \"{name}\"", + "description": "The target rate \"{name}\" has become invalid. Click on 'Learn More' with instructions on what to do next." + } + } +} \ No newline at end of file diff --git a/custom_components/octopus_energy/utils.py b/custom_components/octopus_energy/utils.py new file mode 100644 index 00000000..47c111f4 --- /dev/null +++ b/custom_components/octopus_energy/utils.py @@ -0,0 +1,121 @@ +from datetime import date, datetime, timedelta +from homeassistant.util.dt import (as_utc, parse_datetime) + +import re + +from .const import ( + REGEX_TARIFF_PARTS, + REGEX_OFFSET_PARTS, +) + +def get_tariff_parts(tariff_code): + matches = re.search(REGEX_TARIFF_PARTS, tariff_code) + if matches == None: + raise Exception(f'Unable to extract product code from tariff code: {tariff_code}') + + # According to https://www.guylipman.com/octopus/api_guide.html#s1b, this part should indicate if we're dealing + # with standard rates or day/night rates + energy = matches[1] + rate = matches[2] + product_code = matches[3] + region = matches[4] + + return { + "energy": energy, + "rate": rate, + "product_code": product_code, + "region": region + } + +def get_active_tariff_code(utcnow: datetime, agreements): + latest_agreement = None + latest_valid_from = None + + # Find our latest agreement + for agreement in agreements: + if agreement["tariff_code"] == None: + continue + + valid_from = as_utc(parse_datetime(agreement["valid_from"])) + + if utcnow >= valid_from and (latest_valid_from == None or valid_from > latest_valid_from): + + latest_valid_to = None + if "valid_to" in agreement and agreement["valid_to"] != None: + latest_valid_to = as_utc(parse_datetime(agreement["valid_to"])) + + if latest_valid_to == None or latest_valid_to >= utcnow: + latest_agreement = agreement + latest_valid_from = valid_from + + if latest_agreement != None: + return latest_agreement["tariff_code"] + + return None + +def apply_offset(date_time: datetime, offset: str, inverse = False): + matches = re.search(REGEX_OFFSET_PARTS, offset) + if matches == None: + raise Exception(f'Unable to extract offset: {offset}') + + symbol = matches[1] + hours = float(matches[2]) + minutes = float(matches[3]) + seconds = float(matches[4]) + + if ((symbol == "-" and inverse == False) or (symbol != "-" and inverse == True)): + return date_time - timedelta(hours=hours, minutes=minutes, seconds=seconds) + + return date_time + timedelta(hours=hours, minutes=minutes, seconds=seconds) + +def get_valid_from(rate): + return rate["valid_from"] + +def rates_to_thirty_minute_increments(data, period_from: datetime, period_to: datetime, tariff_code: str): + """Process the collection of rates to ensure they're in 30 minute periods""" + starting_period_from = period_from + results = [] + if ("results" in data): + items = data["results"] + items.sort(key=get_valid_from) + + # We need to normalise our data into 30 minute increments so that all of our rates across all tariffs are the same and it's + # easier to calculate our target rate sensors + for item in items: + value_exc_vat = float(item["value_exc_vat"]) + value_inc_vat = float(item["value_inc_vat"]) + + if "valid_from" in item and item["valid_from"] != None: + valid_from = as_utc(parse_datetime(item["valid_from"])) + + # If we're on a fixed rate, then our current time could be in the past so we should go from + # our target period from date otherwise we could be adjusting times quite far in the past + if (valid_from < starting_period_from): + valid_from = starting_period_from + else: + valid_from = starting_period_from + + # Some rates don't have end dates, so we should treat this as our period to target + if "valid_to" in item and item["valid_to"] != None: + target_date = as_utc(parse_datetime(item["valid_to"])) + + # Cap our target date to our end period + if (target_date > period_to): + target_date = period_to + else: + target_date = period_to + + while valid_from < target_date: + valid_to = valid_from + timedelta(minutes=30) + results.append({ + "value_exc_vat": value_exc_vat, + "value_inc_vat": value_inc_vat, + "valid_from": valid_from, + "valid_to": valid_to, + "tariff_code": tariff_code + }) + + valid_from = valid_to + starting_period_from = valid_to + + return results \ No newline at end of file diff --git a/custom_components/octopus_energy/utils/__init__.py b/custom_components/octopus_energy/utils/__init__.py new file mode 100644 index 00000000..ebd807d7 --- /dev/null +++ b/custom_components/octopus_energy/utils/__init__.py @@ -0,0 +1,72 @@ +from datetime import datetime, timedelta +from homeassistant.util.dt import (as_utc, parse_datetime) + +import re + +from ..const import ( + REGEX_TARIFF_PARTS, +) + +class TariffParts: + energy: str + rate: str + product_code: str + region: str + + def __init__(self, energy: str, rate: str, product_code: str, region: str): + self.energy = energy + self.rate = rate + self.product_code = product_code + self.region = region + +def get_tariff_parts(tariff_code) -> TariffParts: + matches = re.search(REGEX_TARIFF_PARTS, tariff_code) + if matches is None: + return None + + # If our energy or rate isn't extracted, then assume is electricity and "single" rate as that's + # where our experimental tariffs are + energy = matches.groupdict()["energy"] or "E" + rate = matches.groupdict()["rate"] or "1R" + product_code =matches.groupdict()["product_code"] + region = matches.groupdict()["region"] + + return TariffParts(energy, rate, product_code, region) + +def get_active_tariff_code(utcnow: datetime, agreements): + latest_agreement = None + latest_valid_from = None + + # Find our latest agreement + for agreement in agreements: + if agreement["tariff_code"] is None: + continue + + valid_from = as_utc(parse_datetime(agreement["valid_from"])) + + if utcnow >= valid_from and (latest_valid_from is None or valid_from > latest_valid_from): + + latest_valid_to = None + if "valid_to" in agreement and agreement["valid_to"] is not None: + latest_valid_to = as_utc(parse_datetime(agreement["valid_to"])) + + if latest_valid_to is None or latest_valid_to >= utcnow: + latest_agreement = agreement + latest_valid_from = valid_from + + if latest_agreement is not None: + return latest_agreement["tariff_code"] + + return None + +def get_off_peak_cost(rates): + off_peak_cost = None + + rate_charges = {} + for rate in rates: + value = rate["value_inc_vat"] + rate_charges[value] = (rate_charges[value] if value in rate_charges else value) + if off_peak_cost is None or off_peak_cost > rate["value_inc_vat"]: + off_peak_cost = rate["value_inc_vat"] + + return off_peak_cost if len(rate_charges) == 2 else None \ No newline at end of file diff --git a/custom_components/octopus_energy/utils/check_tariff.py b/custom_components/octopus_energy/utils/check_tariff.py new file mode 100644 index 00000000..655a3f7e --- /dev/null +++ b/custom_components/octopus_energy/utils/check_tariff.py @@ -0,0 +1,41 @@ +from homeassistant.helpers import issue_registry as ir + +from ..const import ( + DOMAIN, + DATA_KNOWN_TARIFF, +) + +from ..api_client import (OctopusEnergyApiClient) + +from ..utils import get_tariff_parts + +async def async_check_valid_tariff(hass, client: OctopusEnergyApiClient, tariff_code: str, is_electricity: bool): + tariff_key = f'{DATA_KNOWN_TARIFF}_{tariff_code}' + if (tariff_key not in hass.data[DOMAIN]): + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + ir.async_create_issue( + hass, + DOMAIN, + f"unknown_tariff_format_{tariff_code}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/unknown_tariff_format.md", + translation_key="unknown_tariff_format", + translation_placeholders={ "type": "Electricity" if is_electricity else "Gas", "tariff_code": tariff_code }, + ) + else: + product = await client.async_get_product(tariff_parts["product_code"]) + if product is None: + ir.async_create_issue( + hass, + DOMAIN, + f"unknown_tariff_{tariff_code}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/blob/develop/_docs/repairs/unknown_tariff.md", + translation_key="unknown_tariff", + translation_placeholders={ "type": "Electricity" if is_electricity else "Gas", "tariff_code": tariff_code }, + ) + else: + hass.data[DOMAIN][tariff_key] = True \ No newline at end of file diff --git a/custom_components/octopus_energy/utils/consumption.py b/custom_components/octopus_energy/utils/consumption.py new file mode 100644 index 00000000..12674f45 --- /dev/null +++ b/custom_components/octopus_energy/utils/consumption.py @@ -0,0 +1,17 @@ +from datetime import (datetime) + +def get_total_consumption(consumption: list): + total_consumption = 0 + for item in consumption: + total_consumption += item["consumption"] + + return total_consumption + +def get_current_consumption_delta(current_datetime: datetime, current_total_consumption: float, previous_updated: datetime, previous_total_consumption: float): + if (previous_total_consumption is None or previous_updated is None): + return None + + if (current_datetime.date() == previous_updated.date()): + return (current_total_consumption - previous_total_consumption) + + return current_total_consumption \ No newline at end of file diff --git a/custom_components/octopus_energy/utils/rate_information.py b/custom_components/octopus_energy/utils/rate_information.py new file mode 100644 index 00000000..f127d144 --- /dev/null +++ b/custom_components/octopus_energy/utils/rate_information.py @@ -0,0 +1,138 @@ +from datetime import (datetime, timedelta) + +def get_current_rate_information(rates, now: datetime): + min_target = now.replace(hour=0, minute=0, second=0, microsecond=0) + max_target = min_target + timedelta(days=1) + + min_rate_value = None + max_rate_value = None + total_rate_value = 0 + total_rates = 0 + current_rate = None + + applicable_rates = [] + is_adding_applicable_rates = True + + if rates is not None: + for period in rates: + if current_rate is None and len(applicable_rates) > 0 and applicable_rates[0]["value_inc_vat"] != period["value_inc_vat"]: + applicable_rates.clear() + + if is_adding_applicable_rates and (len(applicable_rates) < 1 or current_rate is None or applicable_rates[0]["value_inc_vat"] == period["value_inc_vat"]): + applicable_rates.append(period) + elif current_rate is not None and len(applicable_rates) > 0 and applicable_rates[0]["value_inc_vat"] != period["value_inc_vat"]: + is_adding_applicable_rates = False + + if now >= period["valid_from"] and now <= period["valid_to"]: + current_rate = period + + if period["valid_from"] >= min_target and period["valid_to"] <= max_target: + if min_rate_value is None or period["value_inc_vat"] < min_rate_value: + min_rate_value = period["value_inc_vat"] + + if max_rate_value is None or period["value_inc_vat"] > max_rate_value: + max_rate_value = period["value_inc_vat"] + + total_rate_value = total_rate_value + period["value_inc_vat"] + total_rates = total_rates + 1 + + if len(applicable_rates) > 0 and current_rate is not None: + return { + "all_rates": list(map(lambda x: { + "valid_from": x["valid_from"], + "valid_to": x["valid_to"], + "value_inc_vat": x["value_inc_vat"], + "is_capped": x["is_capped"], + "is_intelligent_adjusted": x["is_intelligent_adjusted"] if "is_intelligent_adjusted" in x else False + }, rates)), + "applicable_rates": list(map(lambda x: { + "valid_from": x["valid_from"], + "valid_to": x["valid_to"], + "value_inc_vat": x["value_inc_vat"], + "is_capped": x["is_capped"], + "is_intelligent_adjusted": x["is_intelligent_adjusted"] if "is_intelligent_adjusted" in x else False + }, applicable_rates)), + "current_rate": { + "valid_from": applicable_rates[0]["valid_from"], + "valid_to": applicable_rates[-1]["valid_to"], + "value_inc_vat": applicable_rates[0]["value_inc_vat"], + "is_capped": current_rate["is_capped"], + "is_intelligent_adjusted": current_rate["is_intelligent_adjusted"] if "is_intelligent_adjusted" in current_rate else False + }, + "min_rate_today": min_rate_value, + "max_rate_today": max_rate_value, + "average_rate_today": total_rate_value / total_rates + } + + return None + +def get_valid_from(rate): + return rate["valid_from"] + +def get_previous_rate_information(rates, now: datetime): + current_rate = None + applicable_rates = [] + + if rates is not None: + for period in reversed(rates): + if now >= period["valid_from"] and now <= period["valid_to"]: + current_rate = period + + if current_rate is not None and current_rate["value_inc_vat"] != period["value_inc_vat"]: + if len(applicable_rates) == 0 or period["value_inc_vat"] == applicable_rates[0]["value_inc_vat"]: + applicable_rates.append(period) + else: + break + + applicable_rates.sort(key=get_valid_from) + + if len(applicable_rates) > 0 and current_rate is not None: + return { + "applicable_rates": list(map(lambda x: { + "valid_from": x["valid_from"], + "valid_to": x["valid_to"], + "value_inc_vat": x["value_inc_vat"], + "is_capped": x["is_capped"], + "is_intelligent_adjusted": x["is_intelligent_adjusted"] if "is_intelligent_adjusted" in x else False + }, applicable_rates)), + "previous_rate": { + "valid_from": applicable_rates[0]["valid_from"], + "valid_to": applicable_rates[-1]["valid_to"], + "value_inc_vat": applicable_rates[0]["value_inc_vat"], + } + } + + return None + +def get_next_rate_information(rates, now: datetime): + current_rate = None + applicable_rates = [] + + if rates is not None: + for period in rates: + if now >= period["valid_from"] and now <= period["valid_to"]: + current_rate = period + + if current_rate is not None and current_rate["value_inc_vat"] != period["value_inc_vat"]: + if len(applicable_rates) == 0 or period["value_inc_vat"] == applicable_rates[0]["value_inc_vat"]: + applicable_rates.append(period) + else: + break + + if len(applicable_rates) > 0 and current_rate is not None: + return { + "applicable_rates": list(map(lambda x: { + "valid_from": x["valid_from"], + "valid_to": x["valid_to"], + "value_inc_vat": x["value_inc_vat"], + "is_capped": x["is_capped"], + "is_intelligent_adjusted": x["is_intelligent_adjusted"] if "is_intelligent_adjusted" in x else False + }, applicable_rates)), + "next_rate": { + "valid_from": applicable_rates[0]["valid_from"], + "valid_to": applicable_rates[-1]["valid_to"], + "value_inc_vat": applicable_rates[0]["value_inc_vat"], + } + } + + return None \ No newline at end of file diff --git a/custom_components/octopus_energy/utils/tariff_check.py b/custom_components/octopus_energy/utils/tariff_check.py new file mode 100644 index 00000000..1fa8b4f9 --- /dev/null +++ b/custom_components/octopus_energy/utils/tariff_check.py @@ -0,0 +1,44 @@ +from . import get_tariff_parts +from ..api_client import (OctopusEnergyApiClient) + +def is_agile_tariff(tariff_code: str): + parts = get_tariff_parts(tariff_code.upper()) + + return parts is not None and "AGILE" in parts.product_code + +def is_tariff_present(root_key: str, region: str, tariff_code: str, product) -> bool: + target_region = f'_{region}' + if root_key in product and target_region in product[root_key]: + first_key = next(iter(product[root_key][target_region])) + return (first_key in product[root_key][target_region] and + 'code' in product[root_key][target_region][first_key] and + product[root_key][target_region][first_key]['code'] == tariff_code) + return False + +async def check_tariff_override_valid(client: OctopusEnergyApiClient, original_tariff_code: str, tariff_code: str): + tariff_parts = get_tariff_parts(tariff_code) + original_tariff_parts = get_tariff_parts(original_tariff_code) + if tariff_parts.energy != original_tariff_parts.energy: + return f"Energy must match '{original_tariff_parts.energy}'" + + if tariff_parts.region != original_tariff_parts.region: + return f"Region must match '{original_tariff_parts.region}'" + + product = await client.async_get_product(tariff_parts.product_code) + if product is None: + return f"Failed to find owning product '{tariff_parts.product_code}'" + + if tariff_parts.energy == 'E': + is_present = is_tariff_present('single_register_electricity_tariffs', tariff_parts.region, tariff_code, product) + if is_present == False: + is_present = is_tariff_present('dual_register_electricity_tariffs', tariff_parts.region, tariff_code, product) + if is_present == False: + return f"Failed to find tariff '{tariff_code}'" + elif tariff_parts.energy == 'G': + is_present = is_tariff_present('single_register_gas_tariffs', tariff_parts.region, tariff_code, product) + if is_present == False: + return f"Failed to find tariff '{tariff_code}'" + else: + return f"Unexpected energy '{tariff_parts.energy}'" + + return None diff --git a/custom_components/thermal_comfort/__init__.py b/custom_components/thermal_comfort/__init__.py index 2eec209e..26a95569 100644 --- a/custom_components/thermal_comfort/__init__.py +++ b/custom_components/thermal_comfort/__init__.py @@ -14,6 +14,7 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration @@ -28,6 +29,8 @@ CONF_POLL, CONF_SCAN_INTERVAL, CONF_TEMPERATURE_SENSOR, + LegacySensorType, + SensorType, ) _LOGGER = logging.getLogger(__name__) @@ -79,6 +82,29 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + + def update_unique_id(entry: RegistryEntry): + """Update unique_id of changed sensor names""" + if LegacySensorType.THERMAL_PERCEPTION in entry.unique_id: + return {"new_unique_id": entry.unique_id.replace(LegacySensorType.THERMAL_PERCEPTION, SensorType.DEW_POINT_PERCEPTION)} + if LegacySensorType.SIMMER_INDEX in entry.unique_id: + return {"new_unique_id": entry.unique_id.replace(LegacySensorType.SIMMER_INDEX, SensorType.SUMMER_SIMMER_INDEX)} + if LegacySensorType.SIMMER_ZONE in entry.unique_id: + return {"new_unique_id": entry.unique_id.replace(LegacySensorType.SIMMER_ZONE, SensorType.SUMMER_SIMMER_PERCEPTION)} + + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + config_entry.version = 2 + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the thermal_comfort integration.""" if DOMAIN in config: diff --git a/custom_components/thermal_comfort/config_flow.py b/custom_components/thermal_comfort/config_flow.py index 0dc0f0f5..d8456d1b 100644 --- a/custom_components/thermal_comfort/config_flow.py +++ b/custom_components/thermal_comfort/config_flow.py @@ -282,9 +282,7 @@ def filter_useless_units(state: State) -> bool: def filter_thermal_comfort_ids(entity_id: str) -> bool: """Filter out device_ids containing our SensorType.""" - return all( - sensor_type.to_shortform() not in entity_id for sensor_type in SensorType - ) + return all(sensor_type not in entity_id for sensor_type in SensorType) filters_for_additional_sensors: list[callable] = [ filter_useless_device_class, @@ -424,7 +422,7 @@ def build_schema( default=list(SensorType), ): cv.multi_select( { - sensor_type: sensor_type.to_title() + sensor_type: sensor_type.to_name() for sensor_type in SensorType } ), @@ -464,6 +462,8 @@ def check_input(hass: HomeAssistant, user_input: dict) -> dict: class ThermalComfortConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Configuration flow for setting up new thermal_comfort entry.""" + VERSION = 2 + @staticmethod @callback def async_get_options_flow(config_entry): @@ -485,7 +485,9 @@ async def async_step_user(self, user_input=None): if t_sensor is not None and p_sensor is not None: unique_id = f"{t_sensor.unique_id}-{p_sensor.unique_id}" - await self.async_set_unique_id(unique_id) + entry = await self.async_set_unique_id(unique_id) + if entry is not None: + _LOGGER.debug(f"An entry with the unique_id {unique_id} already exists: {entry.data}") self._abort_if_unique_id_configured() return self.async_create_entry( diff --git a/custom_components/thermal_comfort/manifest.json b/custom_components/thermal_comfort/manifest.json index 58506699..2b4c7f58 100644 --- a/custom_components/thermal_comfort/manifest.json +++ b/custom_components/thermal_comfort/manifest.json @@ -1,10 +1,10 @@ { "domain": "thermal_comfort", "name": "Thermal Comfort", - "version": "1.5.5", - "documentation": "https://github.com/dolezsa/thermal_comfort/blob/master/README.md", - "issue_tracker": "https://github.com/dolezsa/thermal_comfort/issues", "codeowners": ["@dolezsa"], + "config_flow": true, + "documentation": "https://github.com/dolezsa/thermal_comfort/blob/master/README.md", "iot_class": "calculated", - "config_flow": true + "issue_tracker": "https://github.com/dolezsa/thermal_comfort/issues", + "version": "2.1.1" } diff --git a/custom_components/thermal_comfort/sensor.py b/custom_components/thermal_comfort/sensor.py index 1db821b6..c3bc2cb0 100644 --- a/custom_components/thermal_comfort/sensor.py +++ b/custom_components/thermal_comfort/sensor.py @@ -10,7 +10,7 @@ from homeassistant import util from homeassistant.backports.enum import StrEnum from homeassistant.components.sensor import ( - ENTITY_ID_FORMAT, + DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, @@ -29,13 +29,13 @@ CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -50,8 +50,15 @@ _LOGGER = logging.getLogger(__name__) +ATTR_DEW_POINT = "dew_point" ATTR_HUMIDITY = "humidity" -ATTR_FROST_RISK_LEVEL = "frost_risk_level" +ATTR_HUMIDEX = "humidex" +ATTR_FROST_POINT = "frost_point" +ATTR_RELATIVE_STRAIN_INDEX = "relative_strain_index" +ATTR_SUMMER_SCHARLAU_INDEX = "summer_scharlau_index" +ATTR_WINTER_SCHARLAU_INDEX = "winter_scharlau_index" +ATTR_SUMMER_SIMMER_INDEX = "summer_simmer_index" +ATTR_THOMS_DISCOMFORT_INDEX = "thoms_discomfort_index" CONF_ENABLED_SENSORS = "enabled_sensors" CONF_SENSOR_TYPES = "sensor_types" CONF_CUSTOM_ICONS = "custom_icons" @@ -63,17 +70,15 @@ # Default values POLL_DEFAULT = False SCAN_INTERVAL_DEFAULT = 30 +DISPLAY_PRECISION = 2 -class ThermalComfortDeviceClass(StrEnum): - """State class for thermal comfort sensors.""" - - FROST_RISK = "thermal_comfort__frost_risk" - SIMMER_ZONE = "thermal_comfort__simmer_zone" - THERMAL_PERCEPTION = "thermal_comfort__thermal_perception" +class LegacySensorType(StrEnum): + THERMAL_PERCEPTION = "thermal_perception" + SIMMER_INDEX = "simmer_index" + SIMMER_ZONE = "simmer_zone" -# Deprecate shortform in 2.0 class SensorType(StrEnum): """Sensor type enum.""" @@ -82,20 +87,20 @@ class SensorType(StrEnum): FROST_POINT = "frost_point" FROST_RISK = "frost_risk" HEAT_INDEX = "heat_index" - SIMMER_INDEX = "simmer_index" - SIMMER_ZONE = "simmer_zone" - THERMAL_PERCEPTION = "thermal_perception" - - def to_title(self) -> str: + HUMIDEX = "humidex" + HUMIDEX_PERCEPTION = "humidex_perception" + MOIST_AIR_ENTHALPY = "moist_air_enthalpy" + RELATIVE_STRAIN_PERCEPTION = "relative_strain_perception" + SUMMER_SCHARLAU_PERCEPTION = "summer_scharlau_perception" + WINTER_SCHARLAU_PERCEPTION = "winter_scharlau_perception" + SUMMER_SIMMER_INDEX = "summer_simmer_index" + SUMMER_SIMMER_PERCEPTION = "summer_simmer_perception" + DEW_POINT_PERCEPTION = "dew_point_perception" + THOMS_DISCOMFORT_PERCEPTION = "thoms_discomfort_perception" + + def to_name(self) -> str: """Return the title of the sensor type.""" - return self.value.replace("_", " ").title() - - def to_shortform(self) -> str: - """Return the shortform of the sensor type.""" - if self.value == "thermal_perception": - return "perception" - else: - return self.value.replace("_", "") + return self.value.replace("_", " ").capitalize() @classmethod def from_string(cls, string: str) -> "SensorType": @@ -103,79 +108,228 @@ def from_string(cls, string: str) -> "SensorType": if string in list(cls): return cls(string) else: - _LOGGER.warning( - "Sensor type shortform and legacy YAML will be removed in 2.0. You should update to the new yaml format: https://github.com/dolezsa/thermal_comfort/blob/master/documentation/yaml.md" + raise ValueError( + f"Unknown sensor type: {string}. Please check https://github.com/dolezsa/thermal_comfort/blob/master/documentation/yaml.md#sensor-options for valid options." ) - if string == "absolutehumidity": - return cls.ABSOLUTE_HUMIDITY - elif string == "dewpoint": - return cls.DEW_POINT - elif string == "frostpoint": - return cls.FROST_POINT - elif string == "frostrisk": - return cls.FROST_RISK - elif string == "heatindex": - return cls.HEAT_INDEX - elif string == "simmerindex": - return cls.SIMMER_INDEX - elif string == "simmerzone": - return cls.SIMMER_ZONE - elif string == "perception": - return cls.THERMAL_PERCEPTION - else: - raise ValueError(f"Unknown sensor type: {string}") +class DewPointPerception(StrEnum): + """Thermal Perception.""" + + DRY = "dry" + VERY_COMFORTABLE = "very_comfortable" + COMFORTABLE = "comfortable" + OK_BUT_HUMID = "ok_but_humid" + SOMEWHAT_UNCOMFORTABLE = "somewhat_uncomfortable" + QUITE_UNCOMFORTABLE = "quite_uncomfortable" + EXTREMELY_UNCOMFORTABLE = "extremely_uncomfortable" + SEVERELY_HIGH = "severely_high" + + +class FrostRisk(StrEnum): + """Frost Risk.""" + + NONE = "no_risk" + LOW = "unlikely" + MEDIUM = "probable" + HIGH = "high" + + +class SummerSimmerPerception(StrEnum): + """Simmer Zone.""" + + COOL = "cool" + SLIGHTLY_COOL = "slightly_cool" + COMFORTABLE = "comfortable" + SLIGHTLY_WARM = "slightly_warm" + INCREASING_DISCOMFORT = "increasing_discomfort" + EXTREMELY_WARM = "extremely_warm" + DANGER_OF_HEATSTROKE = "danger_of_heatstroke" + EXTREME_DANGER_OF_HEATSTROKE = "extreme_danger_of_heatstroke" + CIRCULATORY_COLLAPSE_IMMINENT = "circulatory_collapse_imminent" + + +class RelativeStrainPerception(StrEnum): + """Relative Strain Perception.""" + + OUTSIDE_CALCULABLE_RANGE = "outside_calculable_range" + COMFORTABLE = "comfortable" + SLIGHT_DISCOMFORT = "slight_discomfort" + DISCOMFORT = "discomfort" + SIGNIFICANT_DISCOMFORT = "significant_discomfort" + EXTREME_DISCOMFORT = "extreme_discomfort" + + +class ScharlauPerception(StrEnum): + """Scharlau Winter and Summer Index Perception.""" + + OUTSIDE_CALCULABLE_RANGE = "outside_calculable_range" + COMFORTABLE = "comfortable" + SLIGHTLY_UNCOMFORTABLE = "slightly_uncomfortable" + MODERATLY_UNCOMFORTABLE = "moderatly_uncomfortable" + HIGHLY_UNCOMFORTABLE = "highly_uncomfortable" + + +class HumidexPerception(StrEnum): + """Humidex Perception.""" + + COMFORTABLE = "comfortable" + NOTICABLE_DISCOMFORT = "noticable_discomfort" + EVIDENT_DISCOMFORT = "evident_discomfort" + GREAT_DISCOMFORT = "great_discomfort" + DANGEROUS_DISCOMFORT = "dangerous_discomfort" + HEAT_STROKE = "heat_stroke" + + +class ThomsDiscomfortPerception(StrEnum): + """Thoms Discomfort Perception.""" + + NO_DISCOMFORT = "no_discomfort" + LESS_THEN_HALF = "less_then_half" + MORE_THEN_HALF = "more_then_half" + MOST = "most" + EVERYONE = "everyone" + DANGEROUS = "dangerous" + + +TC_ICONS = { + SensorType.DEW_POINT: "tc:dew-point", + SensorType.FROST_POINT: "tc:frost-point", + SensorType.HUMIDEX_PERCEPTION: "tc:thermal-perception", + SensorType.RELATIVE_STRAIN_PERCEPTION: "tc:thermal-perception", + SensorType.SUMMER_SCHARLAU_PERCEPTION: "tc:thermal-perception", + SensorType.WINTER_SCHARLAU_PERCEPTION: "tc:thermal-perception", + SensorType.SUMMER_SIMMER_PERCEPTION: "tc:thermal-perception", + SensorType.DEW_POINT_PERCEPTION: "tc:thermal-perception", + SensorType.THOMS_DISCOMFORT_PERCEPTION: "tc:thermal-perception", +} + SENSOR_TYPES = { SensorType.ABSOLUTE_HUMIDITY: { "key": SensorType.ABSOLUTE_HUMIDITY, - "device_class": SensorDeviceClass.HUMIDITY, + "name": SensorType.ABSOLUTE_HUMIDITY.to_name(), + "suggested_display_precision": DISPLAY_PRECISION, "native_unit_of_measurement": "g/m³", "state_class": SensorStateClass.MEASUREMENT, "icon": "mdi:water", }, SensorType.DEW_POINT: { "key": SensorType.DEW_POINT, + "name": SensorType.DEW_POINT.to_name(), "device_class": SensorDeviceClass.TEMPERATURE, - "native_unit_of_measurement": TEMP_CELSIUS, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT, - "icon": "tc:dew-point", + "icon": "mdi:thermometer-water", }, SensorType.FROST_POINT: { "key": SensorType.FROST_POINT, + "name": SensorType.FROST_POINT.to_name(), "device_class": SensorDeviceClass.TEMPERATURE, - "native_unit_of_measurement": TEMP_CELSIUS, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT, - "icon": "tc:frost-point", + "icon": "mdi:snowflake-thermometer", }, SensorType.FROST_RISK: { "key": SensorType.FROST_RISK, - "device_class": ThermalComfortDeviceClass.FROST_RISK, + "name": SensorType.FROST_RISK.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, FrostRisk)), + "translation_key": SensorType.FROST_RISK, "icon": "mdi:snowflake-alert", }, SensorType.HEAT_INDEX: { "key": SensorType.HEAT_INDEX, + "name": SensorType.HEAT_INDEX.to_name(), "device_class": SensorDeviceClass.TEMPERATURE, - "native_unit_of_measurement": TEMP_CELSIUS, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT, - "icon": "tc:heat-index", + "icon": "mdi:sun-thermometer", }, - SensorType.SIMMER_INDEX: { - "key": SensorType.SIMMER_INDEX, + SensorType.HUMIDEX: { + "key": SensorType.HUMIDEX, + "name": SensorType.HUMIDEX.to_name(), "device_class": SensorDeviceClass.TEMPERATURE, - "native_unit_of_measurement": TEMP_CELSIUS, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + "icon": "mdi:sun-thermometer", + }, + SensorType.HUMIDEX_PERCEPTION: { + "key": SensorType.HUMIDEX_PERCEPTION, + "name": SensorType.HUMIDEX_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, HumidexPerception)), + "translation_key": SensorType.HUMIDEX_PERCEPTION, + "icon": "mdi:sun-thermometer", + }, + SensorType.MOIST_AIR_ENTHALPY: { + "key": SensorType.MOIST_AIR_ENTHALPY, + "name": SensorType.MOIST_AIR_ENTHALPY.to_name(), + "translation_key": SensorType.MOIST_AIR_ENTHALPY, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": "kJ/kg", "state_class": SensorStateClass.MEASUREMENT, - "icon": "tc:simmer-index", + "icon": "mdi:water-circle", }, - SensorType.SIMMER_ZONE: { - "key": SensorType.SIMMER_ZONE, - "device_class": ThermalComfortDeviceClass.SIMMER_ZONE, - "icon": "tc:simmer-zone", + SensorType.RELATIVE_STRAIN_PERCEPTION: { + "key": SensorType.RELATIVE_STRAIN_PERCEPTION, + "name": SensorType.RELATIVE_STRAIN_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, RelativeStrainPerception)), + "translation_key": SensorType.RELATIVE_STRAIN_PERCEPTION, + "icon": "mdi:sun-thermometer", }, - SensorType.THERMAL_PERCEPTION: { - "key": SensorType.THERMAL_PERCEPTION, - "device_class": ThermalComfortDeviceClass.THERMAL_PERCEPTION, - "icon": "tc:thermal-perception", + SensorType.SUMMER_SCHARLAU_PERCEPTION: { + "key": SensorType.SUMMER_SCHARLAU_PERCEPTION, + "name": SensorType.SUMMER_SCHARLAU_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, ScharlauPerception)), + "translation_key": "scharlau_perception", + "icon": "mdi:sun-thermometer", + }, + SensorType.WINTER_SCHARLAU_PERCEPTION: { + "key": SensorType.WINTER_SCHARLAU_PERCEPTION, + "name": SensorType.WINTER_SCHARLAU_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, ScharlauPerception)), + "translation_key": "scharlau_perception", + "icon": "mdi:snowflake-thermometer", + }, + SensorType.SUMMER_SIMMER_INDEX: { + "key": SensorType.SUMMER_SIMMER_INDEX, + "name": SensorType.SUMMER_SIMMER_INDEX.to_name(), + "device_class": SensorDeviceClass.TEMPERATURE, + "suggested_display_precision": DISPLAY_PRECISION, + "native_unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT, + "icon": "mdi:sun-thermometer", + }, + SensorType.SUMMER_SIMMER_PERCEPTION: { + "key": SensorType.SUMMER_SIMMER_PERCEPTION, + "name": SensorType.SUMMER_SIMMER_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, SummerSimmerPerception)), + "translation_key": SensorType.SUMMER_SIMMER_PERCEPTION, + "icon": "mdi:sun-thermometer", + }, + SensorType.DEW_POINT_PERCEPTION: { + "key": SensorType.DEW_POINT_PERCEPTION, + "name": SensorType.DEW_POINT_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, DewPointPerception)), + "translation_key": SensorType.DEW_POINT_PERCEPTION, + "icon": "mdi:sun-thermometer", + }, + SensorType.THOMS_DISCOMFORT_PERCEPTION: { + "key": SensorType.THOMS_DISCOMFORT_PERCEPTION, + "name": SensorType.THOMS_DISCOMFORT_PERCEPTION.to_name(), + "device_class": SensorDeviceClass.ENUM, + "options": list(map(str, ThomsDiscomfortPerception)), + "translation_key": SensorType.THOMS_DISCOMFORT_PERCEPTION, + "icon": "mdi:sun-thermometer", }, } @@ -198,7 +352,7 @@ def from_string(cls, string: str) -> "SensorType": vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(CONF_UNIQUE_ID): cv.string, } ) @@ -215,42 +369,6 @@ def from_string(cls, string: str) -> "SensorType": ).extend(PLATFORM_OPTIONS_SCHEMA.schema) -class ThermalPerception(StrEnum): - """Thermal Perception.""" - - DRY = "dry" - VERY_COMFORTABLE = "very_comfortable" - COMFORTABLE = "comfortable" - OK_BUT_HUMID = "ok_but_humid" - SOMEWHAT_UNCOMFORTABLE = "somewhat_uncomfortable" - QUITE_UNCOMFORTABLE = "quite_uncomfortable" - EXTREMELY_UNCOMFORTABLE = "extremely_uncomfortable" - SEVERELY_HIGH = "severely_high" - - -class FrostRisk(StrEnum): - """Frost Risk.""" - - NONE = "no_risk" - LOW = "unlikely" - MEDIUM = "probable" - HIGH = "high" - - -class SimmerZone(StrEnum): - """Simmer Zone.""" - - COOL = "cool" - SLIGHTLY_COOL = "slightly_cool" - COMFORTABLE = "comfortable" - SLIGHTLY_WARM = "slightly_warm" - INCREASING_DISCOMFORT = "increasing_discomfort" - EXTREMELY_WARM = "extremely_warm" - DANGER_OF_HEATSTROKE = "danger_of_heatstroke" - EXTREME_DANGER_OF_HEATSTROKE = "extreme_danger_of_heatstroke" - CIRCULATORY_COLLAPSE_IMMINENT = "circulatory_collapse_imminent" - - def compute_once_lock(sensor_type): """Only compute if sensor_type needs update, return just the value otherwise.""" @@ -272,7 +390,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Thermal Comfort sensors.""" if discovery_info is None: _LOGGER.warning( - "Legacy YAML configuration support will be removed in 2.0. You should update to the new yaml format: https://github.com/dolezsa/thermal_comfort/blob/master/documentation/yaml.md" + "Legacy YAML configuration is unsupported in 2.0. You should update to the new yaml format: https://github.com/dolezsa/thermal_comfort/blob/master/documentation/yaml.md" ) devices = [ dict(device_config, **{CONF_NAME: device_name}) @@ -308,8 +426,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon_template=device_config.get(CONF_ICON_TEMPLATE), entity_picture_template=device_config.get(CONF_ENTITY_PICTURE_TEMPLATE), sensor_type=SensorType.from_string(sensor_type), - friendly_name=device_config.get(CONF_FRIENDLY_NAME), custom_icons=device_config.get(CONF_CUSTOM_ICONS, False), + is_config_entry=False, ) for sensor_type in device_config.get( CONF_SENSOR_TYPES, DEFAULT_SENSOR_TYPES @@ -387,37 +505,39 @@ def __init__( entity_description: SensorEntityDescription, icon_template: Template = None, entity_picture_template: Template = None, - friendly_name: str = None, custom_icons: bool = False, + is_config_entry: bool = True, ) -> None: """Initialize the sensor.""" self._device = device - # TODO deprecate shortform in 2.0 self._sensor_type = sensor_type self.entity_description = entity_description - if friendly_name is None: - self.entity_description.name = ( - f"{self._device.name} {self._sensor_type.to_title()}" - ) - else: + self.entity_description.has_entity_name = is_config_entry + if not is_config_entry: self.entity_description.name = ( - f"{friendly_name} {self._sensor_type.to_title()}" + f"{self._device.name} {self.entity_description.name}" ) - # TODO deprecate shortform in 2.0 - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, - f"{self._device.name}_{self._sensor_type.to_shortform()}", - hass=self._device.hass, - ) - if not custom_icons: - if "tc" in self.entity_description.icon: - self._attr_icon = None + if sensor_type in [SensorType.DEW_POINT_PERCEPTION, SensorType.SUMMER_SIMMER_INDEX, SensorType.SUMMER_SIMMER_PERCEPTION]: + registry = entity_registry.async_get(self._device.hass) + if sensor_type is SensorType.DEW_POINT_PERCEPTION: + unique_id = id_generator(self._device.unique_id, LegacySensorType.THERMAL_PERCEPTION) + entity_id = registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id) + elif sensor_type is SensorType.SUMMER_SIMMER_INDEX: + unique_id = id_generator(self._device.unique_id, LegacySensorType.SIMMER_INDEX) + entity_id = registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id) + elif sensor_type is SensorType.SUMMER_SIMMER_PERCEPTION: + unique_id = id_generator(self._device.unique_id, LegacySensorType.SIMMER_ZONE) + entity_id = registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id) + if entity_id is not None: + registry.async_update_entity(entity_id, new_unique_id=id_generator(self._device.unique_id, sensor_type)) + if custom_icons: + if self.entity_description.key in TC_ICONS: + self.entity_description.icon = TC_ICONS[self.entity_description.key] self._icon_template = icon_template self._entity_picture_template = entity_picture_template self._attr_native_value = None self._attr_extra_state_attributes = {} - if self._device.unique_id is not None: - self._attr_unique_id = id_generator(self._device.unique_id, sensor_type) + self._attr_unique_id = id_generator(self._device.unique_id, sensor_type) self._attr_should_poll = False @property @@ -448,9 +568,26 @@ async def async_update(self): if value is None: # can happen during startup return - if self._sensor_type == SensorType.FROST_RISK: - self._attr_extra_state_attributes[ATTR_FROST_RISK_LEVEL] = value - self._attr_native_value = list(FrostRisk)[value] + if type(value) == tuple and len(value) == 2: + if self._sensor_type == SensorType.HUMIDEX_PERCEPTION: + self._attr_extra_state_attributes[ATTR_HUMIDEX] = value[1] + elif self._sensor_type == SensorType.DEW_POINT_PERCEPTION: + self._attr_extra_state_attributes[ATTR_DEW_POINT] = value[1] + elif self._sensor_type == SensorType.FROST_RISK: + self._attr_extra_state_attributes[ATTR_FROST_POINT] = value[1] + elif self._sensor_type == SensorType.RELATIVE_STRAIN_PERCEPTION: + self._attr_extra_state_attributes[ATTR_RELATIVE_STRAIN_INDEX] = value[1] + elif self._sensor_type == SensorType.SUMMER_SCHARLAU_PERCEPTION: + self._attr_extra_state_attributes[ATTR_SUMMER_SCHARLAU_INDEX] = value[1] + elif self._sensor_type == SensorType.WINTER_SCHARLAU_PERCEPTION: + self._attr_extra_state_attributes[ATTR_WINTER_SCHARLAU_INDEX] = value[1] + elif self._sensor_type == SensorType.SUMMER_SIMMER_PERCEPTION: + self._attr_extra_state_attributes[ATTR_SUMMER_SIMMER_INDEX] = value[1] + elif self._sensor_type == SensorType.THOMS_DISCOMFORT_PERCEPTION: + self._attr_extra_state_attributes[ATTR_THOMS_DISCOMFORT_INDEX] = value[ + 1 + ] + self._attr_native_value = value[0] else: self._attr_native_value = value @@ -566,14 +703,17 @@ async def temperature_state_listener(self, event): async def _new_temperature_state(self, state): if _is_valid_state(state): - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + hass = self.hass + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, hass.config.units.temperature_unit) temp = util.convert(state.state, float) - self.extra_state_attributes[ATTR_TEMPERATURE] = temp # convert to celsius if necessary - if unit == TEMP_FAHRENHEIT: - temp = TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) - self._temperature = temp - await self.async_update() + temperature = TemperatureConverter.convert(temp, unit, UnitOfTemperature.CELSIUS) + if -89.2 <= temperature <= 56.7: + self.extra_state_attributes[ATTR_TEMPERATURE] = temp + self._temperature = temperature + await self.async_update() + else: + _LOGGER.info(f"Temperature has an invalid value: {state}. Can't calculate new states.") async def humidity_state_listener(self, event): """Handle humidity device state changes.""" @@ -581,9 +721,13 @@ async def humidity_state_listener(self, event): async def _new_humidity_state(self, state): if _is_valid_state(state): - self._humidity = float(state.state) - self.extra_state_attributes[ATTR_HUMIDITY] = self._humidity - await self.async_update() + humidity = float(state.state) + if 0 < humidity <= 100: + self._humidity = float(state.state) + self.extra_state_attributes[ATTR_HUMIDITY] = self._humidity + await self.async_update() + else: + _LOGGER.info(f"Relative humidity has an invalid value: {state}. Can't calculate new states.") @compute_once_lock(SensorType.DEW_POINT) async def dew_point(self) -> float: @@ -597,13 +741,13 @@ async def dew_point(self) -> float: VP = pow(10, SUM - 3) * self._humidity Td = math.log(VP / 0.61078) Td = (241.88 * Td) / (17.558 - Td) - return round(Td, 2) + return Td @compute_once_lock(SensorType.HEAT_INDEX) async def heat_index(self) -> float: """Heat Index .""" fahrenheit = TemperatureConverter.convert( - self._temperature, TEMP_CELSIUS, TEMP_FAHRENHEIT + self._temperature, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT ) hi = 0.5 * ( fahrenheit + 61.0 + ((fahrenheit - 68.0) * 1.2) + (self._humidity * 0.094) @@ -626,28 +770,57 @@ async def heat_index(self) -> float: elif self._humidity > 85 and fahrenheit >= 80 and fahrenheit <= 87: hi = hi + ((self._humidity - 85) * 0.1) * ((87 - fahrenheit) * 0.2) - return round(TemperatureConverter.convert(hi, TEMP_FAHRENHEIT, TEMP_CELSIUS), 2) + return TemperatureConverter.convert(hi, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) + + @compute_once_lock(SensorType.HUMIDEX) + async def humidex(self) -> int: + """.""" + dewpoint = await self.dew_point() + e = 6.11 * math.exp(5417.7530 * ((1 / 273.16) - (1 / (dewpoint + 273.15)))) + h = (0.5555) * (e - 10.0) + return self._temperature + h + + @compute_once_lock(SensorType.HUMIDEX_PERCEPTION) + async def humidex_perception(self) -> (HumidexPerception, float): + """.""" + humidex = await self.humidex() + if humidex > 54: + perception = HumidexPerception.HEAT_STROKE + elif humidex >= 45: + perception = HumidexPerception.DANGEROUS_DISCOMFORT + elif humidex >= 40: + perception = HumidexPerception.GREAT_DISCOMFORT + elif humidex >= 35: + perception = HumidexPerception.EVIDENT_DISCOMFORT + elif humidex >= 30: + perception = HumidexPerception.NOTICABLE_DISCOMFORT + else: + perception = HumidexPerception.COMFORTABLE - @compute_once_lock(SensorType.THERMAL_PERCEPTION) - async def thermal_perception(self) -> ThermalPerception: + return perception, humidex + + @compute_once_lock(SensorType.DEW_POINT_PERCEPTION) + async def dew_point_perception(self) -> (DewPointPerception, float): """Dew Point .""" dewpoint = await self.dew_point() if dewpoint < 10: - return ThermalPerception.DRY + perception = DewPointPerception.DRY elif dewpoint < 13: - return ThermalPerception.VERY_COMFORTABLE + perception = DewPointPerception.VERY_COMFORTABLE elif dewpoint < 16: - return ThermalPerception.COMFORTABLE + perception = DewPointPerception.COMFORTABLE elif dewpoint < 18: - return ThermalPerception.OK_BUT_HUMID + perception = DewPointPerception.OK_BUT_HUMID elif dewpoint < 21: - return ThermalPerception.SOMEWHAT_UNCOMFORTABLE + perception = DewPointPerception.SOMEWHAT_UNCOMFORTABLE elif dewpoint < 24: - return ThermalPerception.QUITE_UNCOMFORTABLE + perception = DewPointPerception.QUITE_UNCOMFORTABLE elif dewpoint < 26: - return ThermalPerception.EXTREMELY_UNCOMFORTABLE + perception = DewPointPerception.EXTREMELY_UNCOMFORTABLE else: - return ThermalPerception.SEVERELY_HIGH + perception = DewPointPerception.SEVERELY_HIGH + + return perception, dewpoint @compute_once_lock(SensorType.ABSOLUTE_HUMIDITY) async def absolute_humidity(self) -> float: @@ -660,7 +833,7 @@ async def absolute_humidity(self) -> float: abs_humidity *= self._humidity abs_humidity *= 2.1674 abs_humidity /= abs_temperature - return round(abs_humidity, 2) + return abs_humidity @compute_once_lock(SensorType.FROST_POINT) async def frost_point(self) -> float: @@ -668,36 +841,95 @@ async def frost_point(self) -> float: dewpoint = await self.dew_point() T = self._temperature + 273.15 Td = dewpoint + 273.15 - return round( - (Td + (2671.02 / ((2954.61 / T) + 2.193665 * math.log(T) - 13.3448)) - T) - - 273.15, - 2, - ) + return (Td + (2671.02 / ((2954.61 / T) + 2.193665 * math.log(T) - 13.3448)) - T) - 273.15 @compute_once_lock(SensorType.FROST_RISK) - async def frost_risk(self) -> int: + async def frost_risk(self) -> (FrostRisk, float): """Frost Risk Level.""" thresholdAbsHumidity = 2.8 absolutehumidity = await self.absolute_humidity() frostpoint = await self.frost_point() if self._temperature <= 1 and frostpoint <= 0: if absolutehumidity <= thresholdAbsHumidity: - return 1 # Frost unlikely despite the temperature + frost_risk = FrostRisk.LOW # Frost unlikely despite the temperature else: - return 3 # high probability of frost + frost_risk = FrostRisk.HIGH # high probability of frost elif ( self._temperature <= 4 and frostpoint <= 0.5 and absolutehumidity > thresholdAbsHumidity ): - return 2 # Frost probable despite the temperature - return 0 # No risk of frost + frost_risk = FrostRisk.MEDIUM # Frost probable despite the temperature + else: + frost_risk = FrostRisk.NONE # No risk of frost + + return frost_risk, frostpoint + + @compute_once_lock(SensorType.RELATIVE_STRAIN_PERCEPTION) + async def relative_strain_perception(self) -> (RelativeStrainPerception, float): + """Relative strain perception.""" + + vp = 6.112 * pow(10, 7.5 * self._temperature / (237.7 + self._temperature)) + e = self._humidity * vp / 100 + rsi = round((self._temperature - 21) / (58 - e), 2) + + if self._temperature < 26 or self._temperature > 35: + perception = RelativeStrainPerception.OUTSIDE_CALCULABLE_RANGE + elif rsi >= 0.45: + perception = RelativeStrainPerception.EXTREME_DISCOMFORT + elif rsi >= 0.35: + perception = RelativeStrainPerception.SIGNIFICANT_DISCOMFORT + elif rsi >= 0.25: + perception = RelativeStrainPerception.DISCOMFORT + elif rsi >= 0.15: + perception = RelativeStrainPerception.SLIGHT_DISCOMFORT + else: + perception = RelativeStrainPerception.COMFORTABLE + + return perception, rsi + + @compute_once_lock(SensorType.SUMMER_SCHARLAU_PERCEPTION) + async def summer_scharlau_perception(self) -> (ScharlauPerception, float): + """.""" + tc = -17.089 * math.log(self._humidity) + 94.979 + ise = tc - self._temperature + + if self._temperature < 17 or self._temperature > 39 or self._humidity < 30: + perception = ScharlauPerception.OUTSIDE_CALCULABLE_RANGE + elif ise <= -3: + perception = ScharlauPerception.HIGHLY_UNCOMFORTABLE + elif ise <= -1: + perception = ScharlauPerception.MODERATLY_UNCOMFORTABLE + elif ise < 0: + perception = ScharlauPerception.SLIGHTLY_UNCOMFORTABLE + else: + perception = ScharlauPerception.COMFORTABLE + + return perception, round(ise, 2) + + @compute_once_lock(SensorType.WINTER_SCHARLAU_PERCEPTION) + async def winter_scharlau_perception(self) -> (ScharlauPerception, float): + """.""" + tc = (0.0003 * self._humidity) + (0.1497 * self._humidity) - 7.7133 + ish = self._temperature - tc + if self._temperature < -5 or self._temperature > 6 or self._humidity < 40: + perception = ScharlauPerception.OUTSIDE_CALCULABLE_RANGE + elif ish <= -3: + perception = ScharlauPerception.HIGHLY_UNCOMFORTABLE + elif ish <= -1: + perception = ScharlauPerception.MODERATLY_UNCOMFORTABLE + elif ish < 0: + perception = ScharlauPerception.SLIGHTLY_UNCOMFORTABLE + else: + perception = ScharlauPerception.COMFORTABLE + + return perception, round(ish, 2) - @compute_once_lock(SensorType.SIMMER_INDEX) - async def simmer_index(self) -> float: + @compute_once_lock(SensorType.SUMMER_SIMMER_INDEX) + async def summer_simmer_index(self) -> float: """.""" fahrenheit = TemperatureConverter.convert( - self._temperature, TEMP_CELSIUS, TEMP_FAHRENHEIT + self._temperature, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT ) si = ( @@ -706,33 +938,116 @@ async def simmer_index(self) -> float: - 56.83 ) - if fahrenheit < 70: + if fahrenheit < 58: # Summer Simmer Index is only valid above 58°F si = fahrenheit - return round(TemperatureConverter.convert(si, TEMP_FAHRENHEIT, TEMP_CELSIUS), 2) + return TemperatureConverter.convert(si, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) - @compute_once_lock(SensorType.SIMMER_ZONE) - async def simmer_zone(self) -> SimmerZone: + @compute_once_lock(SensorType.SUMMER_SIMMER_PERCEPTION) + async def summer_simmer_perception(self) -> (SummerSimmerPerception, float): """.""" - si = await self.simmer_index() + si = await self.summer_simmer_index() if si < 21.1: - return SimmerZone.COOL + summer_simmer_perception = SummerSimmerPerception.COOL elif si < 25.0: - return SimmerZone.SLIGHTLY_COOL + summer_simmer_perception = SummerSimmerPerception.SLIGHTLY_COOL elif si < 28.3: - return SimmerZone.COMFORTABLE + summer_simmer_perception = SummerSimmerPerception.COMFORTABLE elif si < 32.8: - return SimmerZone.SLIGHTLY_WARM + summer_simmer_perception = SummerSimmerPerception.SLIGHTLY_WARM elif si < 37.8: - return SimmerZone.INCREASING_DISCOMFORT + summer_simmer_perception = SummerSimmerPerception.INCREASING_DISCOMFORT elif si < 44.4: - return SimmerZone.EXTREMELY_WARM + summer_simmer_perception = SummerSimmerPerception.EXTREMELY_WARM elif si < 51.7: - return SimmerZone.DANGER_OF_HEATSTROKE + summer_simmer_perception = SummerSimmerPerception.DANGER_OF_HEATSTROKE elif si < 65.6: - return SimmerZone.EXTREME_DANGER_OF_HEATSTROKE + summer_simmer_perception = SummerSimmerPerception.EXTREME_DANGER_OF_HEATSTROKE else: - return SimmerZone.CIRCULATORY_COLLAPSE_IMMINENT + summer_simmer_perception = SummerSimmerPerception.CIRCULATORY_COLLAPSE_IMMINENT + + return summer_simmer_perception, si + + @compute_once_lock(SensorType.MOIST_AIR_ENTHALPY) + async def moist_air_enthalpy(self) -> float: + """Calculate the enthalpy of moist air.""" + patm = 101325 + c_to_k = 273.15 + h_fg = 2501000 + cp_vapour = 1805.0 + + # calculate vapour pressure + ta_k = self._temperature + c_to_k + c1 = -5674.5359 + c2 = 6.3925247 + c3 = -0.9677843 * math.pow(10, -2) + c4 = 0.62215701 * math.pow(10, -6) + c5 = 0.20747825 * math.pow(10, -8) + c6 = -0.9484024 * math.pow(10, -12) + c7 = 4.1635019 + c8 = -5800.2206 + c9 = 1.3914993 + c10 = -0.048640239 + c11 = 0.41764768 * math.pow(10, -4) + c12 = -0.14452093 * math.pow(10, -7) + c13 = 6.5459673 + + if ta_k < c_to_k: + pascals = math.exp( + c1 / ta_k + + c2 + + ta_k * (c3 + ta_k * (c4 + ta_k * (c5 + c6 * ta_k))) + + c7 * math.log(ta_k) + ) + else: + pascals = math.exp( + c8 / ta_k + + c9 + + ta_k * (c10 + ta_k * (c11 + ta_k * c12)) + + c13 * math.log(ta_k) + ) + + # calculate humidity ratio + p_saturation = pascals + p_vap = self._humidity / 100 * p_saturation + hr = 0.62198 * p_vap / (patm - p_vap) + + # calculate enthalpy + cp_air = 1004 + h_dry_air = cp_air * self._temperature + h_sat_vap = h_fg + cp_vapour * self._temperature + h = h_dry_air + hr * h_sat_vap + + return h / 1000 + + @compute_once_lock(SensorType.THOMS_DISCOMFORT_PERCEPTION) + async def thoms_discomfort_perception(self) -> (ThomsDiscomfortPerception, float): + """Calculate Thom's discomfort index and perception.""" + tw = ( + self._temperature + * math.atan(0.151977 * pow(self._humidity + 8.313659, 1 / 2)) + + math.atan(self._temperature + self._humidity) + - math.atan(self._humidity - 1.676331) + + pow(0.00391838 * self._humidity, 3 / 2) + * math.atan(0.023101 * self._humidity) + - 4.686035 + ) + tdi = 0.5 * tw + 0.5 * self._temperature + + if tdi >= 32: + perception = ThomsDiscomfortPerception.DANGEROUS + elif tdi >= 29: + perception = ThomsDiscomfortPerception.EVERYONE + elif tdi >= 27: + perception = ThomsDiscomfortPerception.MOST + elif tdi >= 24: + perception = ThomsDiscomfortPerception.MORE_THEN_HALF + elif tdi >= 21: + perception = ThomsDiscomfortPerception.LESS_THEN_HALF + else: + perception = ThomsDiscomfortPerception.NO_DISCOMFORT + + return perception, round(tdi, 2) async def async_update(self): """Update the state.""" diff --git a/custom_components/thermal_comfort/translations/ca.json b/custom_components/thermal_comfort/translations/ca.json new file mode 100644 index 00000000..23c5b4a5 --- /dev/null +++ b/custom_components/thermal_comfort/translations/ca.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "No s'ha trobat el sensor de temperatura", + "humidity_not_found": "No s'ha trobat el sensor d'humitat" + }, + "step": { + "init": { + "title": "Configuració de Thermal Comfort", + "data": { + "temperature_sensor": "Sensor de temperatura", + "humidity_sensor": "Sensor d'humitat", + "poll": "Activar la consulta recurrent de les dades", + "scan_interval": "Interval de consulta (segons)", + "custom_icons": "Utilitzar el paquet d'icones personalitzat" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Aquesta combinació de sensors de temperatura i humitat ja està configurada.", + "no_sensors": "No s'han trobat sensors de temperatura o humitat. Torna-ho a provar en mode avançat.", + "no_sensors_advanced": "No s'han trobat sensors de temperatura o humitat." + }, + "error": { + "temperature_not_found": "No s'ha trobat el sensor de temperatura", + "humidity_not_found": "No s'ha trobat el sensor d'humitat" + }, + "step": { + "user": { + "title": "Configuració de Thermal Comfort", + "data": { + "name": "Nom", + "temperature_sensor": "Sensor de temperatura", + "humidity_sensor": "Sensor d'humitat", + "poll": "Activar la consulta recurrent de les dades", + "scan_interval": "Interval de consulta (segons)", + "custom_icons": "Utilitzar el paquet d'icones personalitzat", + "enabled_sensors": "Sensors activats" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Sense risc", + "unlikely": "Poc probable", + "probable": "Probable", + "high": "Alta probabilitat" + } + }, + "dew_point_perception": { + "state": { + "dry": "Una mica sec per a alguns", + "very_comfortable": "Molt còmode", + "comfortable": "Còmode", + "ok_but_humid": "Bé per a la majoria, però humit", + "somewhat_uncomfortable": "Una mica incòmode", + "quite_uncomfortable": "Molt humit, bastant incòmode", + "extremely_uncomfortable": "Extremadament incòmode, aclaparador", + "severely_high": "Molt alt, fins i tot mortal per a persones amb malalties relacionades amb l'asma" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Còmode", + "noticable_discomfort": "Una mica incòmode", + "evident_discomfort": "Bastant incòmode", + "great_discomfort": "Molt incòmode, evitar esforços", + "dangerous_discomfort": "Malestar perillós", + "heat_stroke": "Possible cop de calor" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Fora de l'interval calculable", + "comfortable": "Còmode", + "slight_discomfort": "Una mica incòmode", + "discomfort": "Incòmode", + "significant_discomfort": "Bastant incòmode", + "extreme_discomfort": "Molt incòmode" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Fred", + "slightly_cool": "Una mica fred", + "comfortable": "Còmode", + "slightly_warm": "Una mica càlid", + "increasing_discomfort": "Càlid i incòmode", + "extremely_warm": "Extremadament càlid", + "danger_of_heatstroke": "Perill de cop de calor", + "extreme_danger_of_heatstroke": "Perill extrem de cop de calor", + "circulatory_collapse_imminent": "Colapse circulatori imminent" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Fora de l'interval calculable", + "comfortable": "Còmode", + "slightly_uncomfortable": "Una mica incòmode", + "moderatly_uncomfortable": "Bastant incòmode", + "highly_uncomfortable": "Molt incòmode" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "No és incòmode", + "less_then_half": "Menys de la meitat de la població sent malestar", + "more_then_half": "Més de la meitat de la població sent malestar", + "most": "La majoria de les persones senten malestar i deteriorament de les condicions psicofísiques", + "everyone": "Tothom sent un malestar important", + "dangerous": "Perill, malestar molt fort que pot provocar cops de calor" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/cs.json b/custom_components/thermal_comfort/translations/cs.json new file mode 100644 index 00000000..dda960f9 --- /dev/null +++ b/custom_components/thermal_comfort/translations/cs.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Žádné riziko", + "unlikely": "Nepravděpodobné", + "probable": "Pravděpodobné", + "high": "Vysoce pravděpodobné" + } + }, + "dew_point_perception": { + "state": { + "dry": "Pro někoho sucho", + "very_comfortable": "Velmi přijemně", + "comfortable": "Příjemně", + "ok_but_humid": "OK pro většinu, ale vlhko", + "somewhat_uncomfortable": "Poněkud nepříjemně", + "quite_uncomfortable": "Velmi vlhko, docela nepříjemně", + "extremely_uncomfortable": "Extrémně nepříjemně, tísnivě", + "severely_high": "Velmi vysoká vlhkost, dokonce smrtelná pro jedince s nemocemi související s astmatem" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Chladno", + "slightly_cool": "Mírně chladno", + "comfortable": "Příjemně", + "slightly_warm": "Mírně teplo", + "increasing_discomfort": "Narůstající nepohodlí", + "extremely_warm": "Extrémně teplo", + "danger_of_heatstroke": "Nebezpečí úpalu", + "extreme_danger_of_heatstroke": "Extrémní nebezpečí úpalu", + "circulatory_collapse_imminent": "Hrozící kolaps krevního oběhu" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/da.json b/custom_components/thermal_comfort/translations/da.json new file mode 100644 index 00000000..176ac93e --- /dev/null +++ b/custom_components/thermal_comfort/translations/da.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Temperatursensor ikke fundet", + "humidity_not_found": "Fugtighedssensor ikke fundet" + }, + "step": { + "init": { + "title": "Termiske komfortindstillinger", + "data": { + "temperature_sensor": "Temperatursensor", + "humidity_sensor": "Fugtighedssensor", + "poll": "Aktiver polling", + "scan_interval": "Poll interval (sekunder)", + "custom_icons": "Brug tilpasset ikonpakke" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Denne kombination af temperatur og fugtighedssensorer er allerede konfigureret", + "no_sensors": "Ingen temperatur eller fugtighedssensorer fundet. Prøv igen i avanceret tilstand.", + "no_sensors_advanced": "Ingen temperatur eller fugtighedssensorer fundet." + }, + "error": { + "temperature_not_found": "Temperatursensor ikke fundet", + "humidity_not_found": "Fugtighedssensor ikke fundet" + }, + "step": { + "user": { + "title": "Termiske komfortindstillinger", + "data": { + "name": "Name", + "temperature_sensor": "Temperatursensor", + "humidity_sensor": "Fugtighedssensor", + "poll": "Aktiver polling", + "scan_interval": "Poll interval (sekunder)", + "custom_icons": "Brug tilpasset ikonpakke", + "enabled_sensors": "Aktiverede sensorer" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Ingen risiko", + "unlikely": "Usandsynlig", + "probable": "Sandsynlig", + "high": "Høj sandsynlighed" + } + }, + "dew_point_perception": { + "state": { + "dry": "Lidt tørt for nogle", + "very_comfortable": "Meget behagelig", + "comfortable": "Komfortabel", + "ok_but_humid": "OK for de fleste, men fugtigt", + "somewhat_uncomfortable": "Noget ubehageligt", + "quite_uncomfortable": "Meget fugtigt, ret ubehageligt", + "extremely_uncomfortable": "Ekstremt ubehageligt, undertrykkende", + "severely_high": "Alvorligt høj, endda dødelig for astmarelaterede sygdomme" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Komfortabel", + "noticable_discomfort": "Mærkbart ubehag", + "evident_discomfort": "Tydeligt ubehag", + "great_discomfort": "Stort ubehag, undgå anstrengelse", + "dangerous_discomfort": "Farligt ubehag", + "heat_stroke": "Hedeslag muligt" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Uden for det beregnelige område", + "comfortable": "Komfortabel", + "slight_discomfort": "Let ubehag", + "discomfort": "Ubehag", + "significant_discomfort": "Betydeligt ubehag", + "extreme_discomfort": "Ekstremt ubehag" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Køligt", + "slightly_cool": "Lidt køligt", + "comfortable": "Komfortabel", + "slightly_warm": "Lidt varm", + "increasing_discomfort": "Stigende ubehag", + "extremely_warm": "Ekstremt varmt", + "danger_of_heatstroke": "Fare for hedeslag", + "extreme_danger_of_heatstroke": "Ekstrem fare for hedeslag", + "circulatory_collapse_imminent": "Kredsløbskollaps nært forestående" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Uden for det beregnelige område", + "comfortable": "Komfortabel", + "slightly_uncomfortable": "Lidt ubehageligt", + "moderatly_uncomfortable": "Moderat ubehageligt", + "highly_uncomfortable": "Meget ubehageligt" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Intet ubehag", + "less_then_half": "Mindre end halvdelen af befolkningen føler ubehag", + "more_then_half": "Mere end halvdelen af befolkningen føler ubehag", + "most": "De fleste individer føler ubehag og forværring af psykofysiske tilstande", + "everyone": "Alle føler betydeligt ubehag", + "dangerous": "Farligt, meget stærkt ubehag, som kan forårsage hedeslag" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/de.json b/custom_components/thermal_comfort/translations/de.json index f740f990..654653f8 100644 --- a/custom_components/thermal_comfort/translations/de.json +++ b/custom_components/thermal_comfort/translations/de.json @@ -41,5 +41,81 @@ } } } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Kein Risiko", + "unlikely": "Unwahrscheinlich", + "probable": "Wahrscheinlich", + "high": "Sehr wahrscheinlich" + } + }, + "dew_point_perception": { + "state": { + "dry": "Etwas trocken", + "very_comfortable": "Sehr angenehm", + "comfortable": "Angenehm", + "ok_but_humid": "Angenehm aber schwül", + "somewhat_uncomfortable": "Etwas unangenehm", + "quite_uncomfortable": "Unangenehm und sehr schwül", + "extremely_uncomfortable": "Äußerst unangenehm und drückend", + "severely_high":"Extrem hoch, tödlich für asthmabedingte Erkrankungen" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Angenehm", + "noticable_discomfort": "Spürbares Unbehagen", + "evident_discomfort": "Offensichtliches Unbehagen", + "great_discomfort": "Großes Unbehagen, Anstrengung vermeiden", + "dangerous_discomfort": "Gefährliches Unbehagen unangenehm", + "heat_stroke": "Hitzschlag möglich" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Außerhalb des berechenbaren Bereichs", + "comfortable": "Angenehm", + "slight_discomfort": "Etwas unbehaglich", + "discomfort": "Unbehaglich", + "significant_discomfort": "Erhebliches Unbehagen", + "extreme_discomfort": "Extremes Unbehagen" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Kühl", + "slightly_cool": "Etwas kühl", + "comfortable": "Angenehm", + "slightly_warm": "Etwas warm", + "increasing_discomfort": "Zunehmend unbehaglich", + "extremely_warm": "Äußerst warm", + "danger_of_heatstroke": "Hitzschlaggefahr", + "extreme_danger_of_heatstroke": "Extreme Hitzschlaggefahr", + "circulatory_collapse_imminent": "Drohender Kreislaufkollaps" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Außerhalb des berechenbaren Bereichs", + "comfortable": "Angenehm", + "slightly_uncomfortable": "Leicht unangenehm", + "moderatly_uncomfortable": "Unangenehm", + "highly_uncomfortable": "Sehr unangenehm" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Kein Unbehagen", + "less_then_half": "Weniger als die Hälfte der Bevölkerung fühlt sich unwohl", + "more_then_half": "Mehr als die Hälfte der Bevölkerung fühlt sich unwohl", + "most": "Die meisten Menschen fühlen sich unwohl und einen verschlechterten psychophysischen Zustand", + "everyone": "Jeder fühlt sich unwohl", + "dangerous": "Gefährlich, sehr starke Beschwerden die zu Hitzschlägen führen können" + } + } + } } } diff --git a/custom_components/thermal_comfort/translations/el.json b/custom_components/thermal_comfort/translations/el.json new file mode 100644 index 00000000..195e8544 --- /dev/null +++ b/custom_components/thermal_comfort/translations/el.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Δεν βρέθηκε αισθητήρας θερμοκρασίας", + "humidity_not_found": "Δεν βρέθηκε αισθητήρας υγρασίας" + }, + "step": { + "init": { + "title": "Ρυθμίσεις θερμικής άνεσης", + "data": { + "temperature_sensor": "Αισθητήρας θερμοκρασίας", + "humidity_sensor": "Αισθητήρας υγρασίας", + "poll": "Ενεργοποίηση ανανέωσης", + "scan_interval": "Ρυθμός ανανέωσης (δευτερόλεπτα)", + "custom_icons": "Χρήση προσαρμοσμένου πακέτου εικονιδίων" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Αυτός ο συνδιασμός αισθητήρων θερμοκρασίας και υγρασίας είναι ήδη διαμορφωμένος", + "no_sensors": "Δεν βρέθηκε κανένας αισθητήρας θερμοκρασίας ή υγρασίας. Δοκιμάστε πάλι σε προχωρημένη λειτουργία.", + "no_sensors_advanced": "Δεν βρέθηκε κανένας αισθητήρας θερμοκρασίας ή υγρασίας." + }, + "error": { + "temperature_not_found": "Δεν βρέθηκε αισθητήρας θερμοκρασίας", + "humidity_not_found": "Δεν βρέθηκε αισθητήρας υγρασίας" + }, + "step": { + "user": { + "title": "Ρυθμίσεις θερμικής άνεσης", + "data": { + "name": "Όνομα", + "temperature_sensor": "Αισθητήρας θερμοκρασίας", + "humidity_sensor": "Αισθητήρας υγρασίας", + "poll": "Ενεργοποίηση ανανέωσης", + "scan_interval": "Ρυθμός ανανέωσης (δευτερόλεπτα)", + "custom_icons": "Χρησιμοποίηση προσαρμοσμένης ομάδας εικονιδίων", + "enabled_sensors": "Ενεργοποιημένοι αισθητήρες" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Κανένας κίνδυνος", + "unlikely": "Απίθανο", + "probable": "Πιθανό", + "high": "Μεγάλη πιθανότητα" + } + }, + "dew_point_perception": { + "state": { + "dry": "Λίγο ξηρή για κάποιους", + "very_comfortable": "Πολύ άνετη", + "comfortable": "Άνετη", + "ok_but_humid": "OK για κάποιους , αλλά με υγρασία", + "somewhat_uncomfortable": "Κάπως άβολη", + "quite_uncomfortable": "Πολύ υγρό, αρκετά άβολη", + "extremely_uncomfortable": "Εξαιρετικά άβολα, καταπιεστική", + "severely_high": "Σοβαρά υψηλή, ακόμη και θανατηφόρα για ασθένειες που σχετίζονται με το άσθμα" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Άνετη", + "noticable_discomfort": "Αισθητή δυσφορία", + "evident_discomfort": "Εμφανής δυσφορία", + "great_discomfort": "Μεγάλη δυσφορία, αποφύγετε την άσκηση", + "dangerous_discomfort": "Επικίνδυνη δυσφορία", + "heat_stroke": "Πιθανή θερμοπληξία" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Εκτός του υπολογίσιμου εύρους", + "comfortable": "Άνετη", + "slight_discomfort": "Ελαφρά δυσφορία", + "discomfort": "Δυσφορία", + "significant_discomfort": "Σημαντική δυσφορία", + "extreme_discomfort": "Ακραία δυσφορία" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Δροσερή", + "slightly_cool": "Ελαφρώς δροσερή", + "comfortable": "Άνετη", + "slightly_warm": "Ελαφρώς ζεστή", + "increasing_discomfort": "Αυξανόμενη δυσφορία", + "extremely_warm": "Εξαιρετικά ζεστή", + "danger_of_heatstroke": "Κίνδυνος θερμοπληξίας", + "extreme_danger_of_heatstroke": "Ακραίος κίνδυνος θερμοπληξίας", + "circulatory_collapse_imminent": "Επικείμενη κυκλοφορική κατάρρευση" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Εκτός του υπολογίσιμου εύρους", + "comfortable": "Άνετη", + "slightly_uncomfortable": "Ελαφρώς άβολη", + "moderatly_uncomfortable": "Μέτρια άβολη", + "highly_uncomfortable": "Ιδιαίτερα άβολη" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Καμία ενόχληση", + "less_then_half": "Λιγότερο από το ήμισυ του πληθυσμού αισθάνεται δυσφορία", + "more_then_half": "Περισσότερο από το ήμισυ του πληθυσμού αισθάνεται δυσφορία", + "most": "Τα περισσότερα άτομα αισθάνονται δυσφορία και επιδείνωση των ψυχοφυσικών συνθηκών", + "everyone": "Όλοι αισθάνονται σημαντική δυσφορία", + "dangerous": "Επικίνδυνη, πολύ έντονη δυσφορία που μπορεί να προκαλέσει θερμοπληξία" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/en.json b/custom_components/thermal_comfort/translations/en.json index 7d116b6f..36efda6a 100644 --- a/custom_components/thermal_comfort/translations/en.json +++ b/custom_components/thermal_comfort/translations/en.json @@ -41,5 +41,81 @@ } } } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "No risk", + "unlikely": "Unlikely", + "probable": "Probable", + "high": "High probability" + } + }, + "dew_point_perception": { + "state": { + "dry": "A bit dry for some", + "very_comfortable": "Very comfortable", + "comfortable": "Comfortable", + "ok_but_humid": "OK for most, but humid", + "somewhat_uncomfortable": "Somewhat uncomfortable", + "quite_uncomfortable": "Very humid, quite uncomfortable", + "extremely_uncomfortable": "Extremely uncomfortable, oppressive", + "severely_high": "Severely high, even deadly for asthma related illnesses" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Comfortable", + "noticable_discomfort": "Noticeable discomfort", + "evident_discomfort": "Evident discomfort", + "great_discomfort": "Great discomfort, avoid exertion", + "dangerous_discomfort": "Dangerous discomfort", + "heat_stroke": "Heat stroke possible" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Outside of the calculable range", + "comfortable": "Comfortable", + "slight_discomfort": "Slight discomfort", + "discomfort": "Discomfort", + "significant_discomfort": "Significant discomfort", + "extreme_discomfort": "Extreme discomfort" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Cool", + "slightly_cool": "Slightly cool", + "comfortable": "Comfortable", + "slightly_warm": "Slightly warm", + "increasing_discomfort": "Increasing discomfort", + "extremely_warm": "Extremely warm", + "danger_of_heatstroke": "Danger of heatstroke", + "extreme_danger_of_heatstroke": "Extreme danger of heatstroke", + "circulatory_collapse_imminent": "Circulatory collapse imminent" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Outside of the calculable range", + "comfortable": "Comfortable", + "slightly_uncomfortable": "Slightly uncomfortable", + "moderatly_uncomfortable": "Moderatly uncomfortable", + "highly_uncomfortable": "Highly uncomfortable" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "No discomfort", + "less_then_half": "Less than half of the population feels discomfort", + "more_then_half": "More than half of the population feels discomfort", + "most": "Most individuals feel discomfort and deterioration of psychophysical conditions", + "everyone": "Everyone feels significant discomfort", + "dangerous": "Dangerous, very strong discomfort which may cause heat strokes" + } + } + } } } diff --git a/custom_components/thermal_comfort/translations/es.json b/custom_components/thermal_comfort/translations/es.json index 0d32b622..183b7e16 100644 --- a/custom_components/thermal_comfort/translations/es.json +++ b/custom_components/thermal_comfort/translations/es.json @@ -10,7 +10,7 @@ "data": { "temperature_sensor": "Sensor de temperatura", "humidity_sensor": "Sensor de humedad", - "poll": "Activar consulta de datos", + "poll": "Activar consulta recurrente de datos", "scan_interval": "Intervalo de consulta (segundos)", "custom_icons": "Usar paquete de iconos personalizado" } @@ -34,12 +34,88 @@ "name": "Nombre", "temperature_sensor": "Sensor de temperatura", "humidity_sensor": "Sensor de humedad", - "poll": "Activar consulta de datos", + "poll": "Activar consulta recurrente de datos", "scan_interval": "Intervalo de consulta (segundos)", "custom_icons": "Usar paquete de iconos personalizado", "enabled_sensors": "Sensores activados" } } } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Sin riesgo", + "unlikely": "Poco probable", + "probable": "Probable", + "high": "Alta probabilidad" + } + }, + "dew_point_perception": { + "state": { + "dry": "Un poco seco para algunos", + "very_comfortable": "Muy cómodo", + "comfortable": "Cómodo", + "ok_but_humid": "Bien para la mayoria, pero algo húmedo", + "somewhat_uncomfortable": "Algo incómodo", + "quite_uncomfortable": "Muy húmedo, bastante incómodo", + "extremely_uncomfortable": "Extremadamente incómodo, agobiante", + "severely_high": "Muy alto, incluso mortal para enfermedades relacionadas con el asma" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Cómodo", + "noticable_discomfort": "Un poco incómodo", + "evident_discomfort": "Bastante incómodo", + "great_discomfort": "Muy incómodo, evitar esfuerzos", + "dangerous_discomfort": "Incomodidad peligrosa", + "heat_stroke": "Posible golpe de calor" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Fuera del rango calculable", + "comfortable": "Cómodo", + "slight_discomfort": "Un poco incómodo", + "discomfort": "Incómodo", + "significant_discomfort": "Bastante incómodo", + "extreme_discomfort": "Muy incómodo" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Fresco", + "slightly_cool": "Ligeramente fresco", + "comfortable": "Cómodo", + "slightly_warm": "Ligeramente caluroso", + "increasing_discomfort": "Caluroso e incómodo", + "extremely_warm": "Extremadamente caluroso", + "danger_of_heatstroke": "Riesgo de golpe de calor", + "extreme_danger_of_heatstroke": "Riesgo extremo de golpe de calor", + "circulatory_collapse_imminent": "Colapso circulatorio inminente" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Fuera del rango calculable", + "comfortable": "Cómodo", + "slightly_uncomfortable": "Un poco incómodo", + "moderatly_uncomfortable": "Bastante incómodo", + "highly_uncomfortable": "Muy incómodo" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Sin molestias", + "less_then_half": "Menos de la mitad de la población siente malestar", + "more_then_half": "Más de la mitad de la población siente malestar", + "most": "La mayoría de personas sienten malestar y deterioro de las condiciones psicofísicas", + "everyone": "Todos sienten malestar significativo", + "dangerous": "Peligro, malestar muy fuerte que puede provocar golpes de calor" + } + } + } } } diff --git a/custom_components/thermal_comfort/translations/fr.json b/custom_components/thermal_comfort/translations/fr.json new file mode 100644 index 00000000..6dd02792 --- /dev/null +++ b/custom_components/thermal_comfort/translations/fr.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Aucun risque", + "unlikely": "Peu probable", + "probable": "Probable", + "high": "Haute probabilité" + } + }, + "dew_point_perception": { + "state": { + "dry": "Un peu sec pour certains", + "very_comfortable": "Très confortable", + "comfortable": "Confortable", + "ok_but_humid": "OK pour la plupart, mais humide", + "somewhat_uncomfortable": "Un peu inconfortable", + "quite_uncomfortable": "Très humide, assez inconfortable", + "extremely_uncomfortable": "Extrêmement inconfortable, oppressant", + "severely_high": "Gravement élevé, voire mortel pour les maladies liées à l'asthme" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Froid", + "slightly_cool": "Légèrement froid", + "comfortable": "Confortable", + "slightly_warm": "Légèrement chaud", + "increasing_discomfort": "Inconfortable", + "extremely_warm": "Extrêmement chaud", + "danger_of_heatstroke": "Danger de coup de chaleur", + "extreme_danger_of_heatstroke": "Danger extrême de coup de chaleur", + "circulatory_collapse_imminent": "Arrêt cardiaque imminent" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/hu.json b/custom_components/thermal_comfort/translations/hu.json new file mode 100644 index 00000000..bb387234 --- /dev/null +++ b/custom_components/thermal_comfort/translations/hu.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Nincs kockázat", + "unlikely": "Nem valószínű", + "probable": "Valószínű", + "high": "Nagy valószínűség" + } + }, + "dew_point_perception": { + "state": { + "dry": "Egyeseknek kissé száraz", + "very_comfortable": "Nagyon kellemes", + "comfortable": "Kellemes", + "ok_but_humid": "A többségnek megfelelő, de párás", + "somewhat_uncomfortable": "Kicsit kellemetlen", + "quite_uncomfortable": "Nagyon nedves, eléggé kellemetlen", + "extremely_uncomfortable": "Rendkívül kellemetlen, nyomasztó", + "severely_high": "Különösen magas, az asztmás betegségek számára életveszélyes" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Hideg", + "slightly_cool": "Enyhén hűvös", + "comfortable": "Kellemes", + "slightly_warm": "Enyhén meleg", + "increasing_discomfort": "Fokozódó diszkomfort", + "extremely_warm": "Rendkívül meleg", + "danger_of_heatstroke": "Napszúrásveszély", + "extreme_danger_of_heatstroke": "Rendkívüli napszúrásveszély", + "circulatory_collapse_imminent": "Keringési összeomlás veszélye" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/it.json b/custom_components/thermal_comfort/translations/it.json new file mode 100644 index 00000000..aa2b1238 --- /dev/null +++ b/custom_components/thermal_comfort/translations/it.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Sensore di temperatura non trovato", + "humidity_not_found": "Sensore di umidità non trovato" + }, + "step": { + "init": { + "title": "Impostazioni di comfort termico", + "data": { + "temperature_sensor": "Sensore di temperatura", + "humidity_sensor": "Sensore di umidità", + "poll": "Abilita polling", + "scan_interval": "Intervallo di polling (secondi)", + "custom_icons": "Usa il pacchetto di icone personalizzate" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Questa combinazione di sensori di temperatura e umidità è già configurata", + "no_sensors": "Nessun sensore di temperatura o umidità trovato. Riprova in modalità avanzata.", + "no_sensors_advanced": "Nessun sensore di temperatura o umidità trovato." + }, + "error": { + "temperature_not_found": "Sensore di temperatura non trovato", + "humidity_not_found": "Sensore di umidità non trovato" + }, + "step": { + "user": { + "title": "Impostazioni di comfort termico", + "data": { + "name": "Nome", + "temperature_sensor": "Sensore di temperatura", + "humidity_sensor": "Sensore di umidità", + "poll": "Abilita polling", + "scan_interval": "Intervallo di polling (secondi)", + "custom_icons": "Usa pacchetto icone personalizzato", + "enabled_sensors": "Sensori abilitati" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Nessun rischio", + "unlikely": "Improbabile", + "probable": "Probabile", + "high": "Alta probabilità" + } + }, + "dew_point_perception": { + "state": { + "dry": "Secco per qualcuno", + "very_comfortable": "Molto confortevole", + "comfortable": "Confortevole", + "ok_but_humid": "Confortevole ma umido", + "somewhat_uncomfortable": "Leggero disagio", + "quite_uncomfortable": "Significativo disagio, molto umido", + "extremely_uncomfortable": "Forte disagio, opprimente", + "severely_high": "Estremo disagio, rischioso per malattie correlate all'asma" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Confortevole", + "noticable_discomfort": "Disagio", + "evident_discomfort": "Disagio significativo", + "great_discomfort": "Disagio enorme, evitare affaticamenti", + "dangerous_discomfort": "Disagio pericoloso", + "heat_stroke": "Probabile colpo di calore" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Fuori scala", + "comfortable": "Confortevole", + "slight_discomfort": "Disagio leggero", + "discomfort": "Disagio", + "significant_discomfort": "Disagio significativo", + "extreme_discomfort": "Disagio estremo" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Freddo", + "slightly_cool": "Leggermente freddo", + "comfortable": "Confortevole", + "slightly_warm": "Leggermente caldo", + "increasing_discomfort": "Aumento del disagio", + "extremely_warm": "Estremamente caldo", + "danger_of_heatstroke": "Pericolo colpo di calore", + "extreme_danger_of_heatstroke": "Probabile colpo di calore", + "circulatory_collapse_imminent": "Imminente colasso circolatorio" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Fuori scala", + "comfortable": "Confortevole", + "slightly_uncomfortable": "Leggero disagio", + "moderatly_uncomfortable": "Moderato disagio", + "highly_uncomfortable": "Forte disagio" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Confortevole", + "less_then_half": "Meno della metà della popolazione avverte disagio", + "more_then_half": "Più della metà della popolazione avverte disagio", + "most": "La maggior parte delle persone avverte disagio e deterioramento delle condizioni psicofisiche", + "everyone": "Forte disagio per chiunque", + "dangerous": "Stato di emergenza medica, disagio molto forte che può causare colpi di calore" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/nb.json b/custom_components/thermal_comfort/translations/nb.json new file mode 100644 index 00000000..9500ea00 --- /dev/null +++ b/custom_components/thermal_comfort/translations/nb.json @@ -0,0 +1,18 @@ +{ + "entity": { + "sensor": { + "dew_point_perception": { + "state": { + "dry": "A bit dry for some", + "very_comfortable": "Very comfortable", + "comfortable": "Comfortable", + "ok_but_humid": "OK for most, but humid", + "somewhat_uncomfortable": "Somewhat uncomfortable", + "quite_uncomfortable": "Very humid, quite uncomfortable", + "extremely_uncomfortable": "Extremely uncomfortable, oppressive", + "severely_high": "Severely high, even deadly for asthma related illnesses" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/nl.json b/custom_components/thermal_comfort/translations/nl.json new file mode 100644 index 00000000..f51f3f9b --- /dev/null +++ b/custom_components/thermal_comfort/translations/nl.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Temperatuursensor niet gevonden", + "humidity_not_found": "Vochtigheidsensor niet gevonden" + }, + "step": { + "init": { + "title": "Thermal comfort instellingen", + "data": { + "temperature_sensor": "Temperatuursensor", + "humidity_sensor": "vochtigheidsensor", + "poll": "Zet polling aan", + "scan_interval": "Poll interval (seconden)", + "custom_icons": "Gebruik custom icons pack" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Deze combinatie van temperatuur en vochtigheid sensors is reeds geconfigureerd", + "no_sensors": "Geen temperatuur of vochtigheid sensors gevonden. Probeer nog eens in geadvanceerd modus.", + "no_sensors_advanced": "Geen temperatuur of vochtigheid sensoren gevonden." + }, + "error": { + "temperature_not_found": "Temperatuursensor niet gevonden", + "humidity_not_found": "Vochtigheidsensor niet gevonden" + }, + "step": { + "user": { + "title": "Thermal comfort instellingen", + "data": { + "name": "Naam", + "temperature_sensor": "Temperatuursensor", + "humidity_sensor": "Vochtigheidsensor", + "poll": "Zet Polling aan", + "scan_interval": "Poll interval (seconden)", + "custom_icons": "Gebruik custom icons pack", + "enabled_sensors": "Ingeschakelde sensors" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Geen risico", + "unlikely": "Onwaarschijnlijk", + "probable": "Waarschijnlijk", + "high": "Hoogstwaarschijnlijk" + } + }, + "dew_point_perception": { + "state": { + "dry": "Beetje droog voor sommigen", + "very_comfortable": "Zeer gemakkelijk", + "comfortable": "Gemakkelijk", + "ok_but_humid": "OK voor meesten, wel vochtig", + "somewhat_uncomfortable": "Ietswat ongemakkelijk", + "quite_uncomfortable": "Zeer vochtig, best ongemakkelijk", + "extremely_uncomfortable": "Extreem ongemakkelijk, drukkend", + "severely_high": "Zeer hoog, zelfs gevaarlijk bij astma gerelateerde ziekten" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Gemakkelijk", + "noticable_discomfort": "Merkbaar ongemak", + "evident_discomfort": "Duidelijk ongemak", + "great_discomfort": "Groot ongemak, vermijd inspanning", + "dangerous_discomfort": "Gevaarlijk ongemak", + "heat_stroke": "Hitteberoerte mogelijk" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Buiten het berekenbare bereik", + "comfortable": "Gemakkelijk", + "slight_discomfort": "Licht ongemakkelijk", + "discomfort": "Ongemakkelijk", + "significant_discomfort": "Aanzienlijk ongemakkelijk", + "extreme_discomfort": "Extreem ongemakkelijk" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Koel", + "slightly_cool": "Enigszins koel", + "comfortable": "Gemakkelijk", + "slightly_warm": "Enigszins warm", + "increasing_discomfort": "Toenemend ongemakkelijk", + "extremely_warm": "Extreem warm", + "danger_of_heatstroke": "Gevaar voor een zonnesteek", + "extreme_danger_of_heatstroke": "Extreem gevaar voor een zonnesteek", + "circulatory_collapse_imminent": "Instorting van de bloedsomloop dreigt" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Buiten het berekenbare bereik", + "comfortable": "Gemakkelijk", + "slightly_uncomfortable": "Licht ongemakkelijk", + "moderatly_uncomfortable": "Matig ongemakkelijk", + "highly_uncomfortable": "Zeer ongemakkelijk" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Niet ongemakkelijk", + "less_then_half": "Minder, dan de helft van de populatie voelt ongemakkelijk", + "more_then_half": "Meer, dan de helft van de populatie voelt ongemakkelijk", + "most": "De meeste mensen voelen ongemak en verslechtering van psychofysische omstandigheden", + "everyone": "Iedereen ervaart behoorlijk ongemak", + "dangerous": "Gevaarlijk, zeer sterk ongemak dat hitteberoerte kan veroorzaken" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/pl.json b/custom_components/thermal_comfort/translations/pl.json new file mode 100644 index 00000000..70499910 --- /dev/null +++ b/custom_components/thermal_comfort/translations/pl.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Nie znaleziono czujnika temperatury", + "humidity_not_found": "Nie znaleziono czujnika wilgotności" + }, + "step": { + "init": { + "title": "Ustawienia komfortu cieplnego", + "data": { + "temperature_sensor": "Czujnik temperatury", + "humidity_sensor": "Czujnik wilgotności", + "poll": "Włącz odpytywanie", + "scan_interval": "Interwał odpytywania (sekundy)", + "custom_icons": "Użyj niestandardowego pakietu ikon" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Ta kombinacja czujników temperatury i wilgotności jest już skonfigurowana", + "no_sensors": "Nie znaleziono czujników temperatury ani wilgotności. Spróbuj ponownie w trybie zaawansowanym.", + "no_sensors_advanced": "Nie znaleziono czujników temperatury ani wilgotności." + }, + "error": { + "temperature_not_found": "Nie znaleziono czujnika temperatury", + "humidity_not_found": "Nie znaleziono czujnika wilgotności" + }, + "step": { + "user": { + "title": "Ustawienia komfortu cieplnego", + "data": { + "name": "Nazwa", + "temperature_sensor": "Czujnik temperatury", + "humidity_sensor": "Czujnik wilgotności", + "poll": "Włącz odpytywanie", + "scan_interval": "Interwał odpytywania (sekundy)", + "custom_icons": "Użyj niestandardowego pakietu ikon", + "enabled_sensors": "Włączone czujniki" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Brak", + "unlikely": "Małe", + "probable": "Możliwe", + "high": "Wysokie" + } + }, + "dew_point_perception": { + "state": { + "dry": "Dla niektórych może być sucho", + "very_comfortable": "Bardzo komfortowe", + "comfortable": "Komfortowe", + "ok_but_humid": "OK dla większości, ale wilgotno", + "somewhat_uncomfortable": "Trochę niekomfortowe", + "quite_uncomfortable": "Bardzo wilgotno, całkiem niekomfortowe", + "extremely_uncomfortable": "Bardzo niekomfortowe, uciążliwe", + "severely_high": "Wysoce niebezpieczne dla astmatyków, bardzo uciążliwe" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Komfortowo", + "noticable_discomfort": "Lekki dyskomfort", + "evident_discomfort": "Wyraźny dyskomfort", + "great_discomfort": "Duży dyskomfort, unikaj wysiłku", + "dangerous_discomfort": "Niebezpieczny dyskomfort", + "heat_stroke": "Możliwy udar cieplny" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Poza obliczalnym zakresem", + "comfortable": "Komfortowo", + "slight_discomfort": "Lekki dyskomfort", + "discomfort": "Dyskomfort", + "significant_discomfort": "Znaczący dyskomfort", + "extreme_discomfort": "Ekstremalny dyskomfort" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Zimno", + "slightly_cool": "Chłodnawo", + "comfortable": "Komfortowo", + "slightly_warm": "Ciepło", + "increasing_discomfort": "Trochę za ciepło", + "extremely_warm": "Bardzo ciepło", + "danger_of_heatstroke": "Możliwy udar cieplny", + "extreme_danger_of_heatstroke": "Wysokie niebezpieczeństwo udaru", + "circulatory_collapse_imminent": "Zdecydowane problemy z krążeniem, zapaść" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Poza obliczalnym zakresem", + "comfortable": "Komfortowo", + "slightly_uncomfortable": "Lekki dyskomfort", + "moderatly_uncomfortable": "Dyskomfort", + "highly_uncomfortable": "Wysoki dyskomfort" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Brak dyskomfortu", + "less_then_half": "Mniej niż połowa populacji odczuwa dyskomfort", + "more_then_half": "Ponad połowa populacji odczuwa dyskomfort", + "most": "Większość osób odczuwa dyskomfort i pogorszenie warunków psychofizycznych", + "everyone": "Wszyscy odczuwają znaczny dyskomfort", + "dangerous": "Niebezpieczny, bardzo silny dyskomfort, który może powodować udary cieplne" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/pt-BR.json b/custom_components/thermal_comfort/translations/pt-BR.json new file mode 100644 index 00000000..346835e7 --- /dev/null +++ b/custom_components/thermal_comfort/translations/pt-BR.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Sem risco", + "unlikely": "Improvável", + "probable": "Provável", + "high": "Muito provável" + } + }, + "dew_point_perception": { + "state": { + "dry": "Seco", + "very_comfortable": "Muito confortável", + "comfortable": "Confortável", + "ok_but_humid": "Confortável mas úmido", + "somewhat_uncomfortable": "Um pouco desconfortável", + "quite_uncomfortable": "Muito desconfortável", + "extremely_uncomfortable": "Extremamente desconfortável", + "severely_high": "Severamente úmido" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Frio", + "slightly_cool": "Um pouco frio", + "comfortable": "Confortável", + "slightly_warm": "Um pouco quente", + "increasing_discomfort": "Desconforto crescente", + "extremely_warm": "Muito quente", + "danger_of_heatstroke": "Perigo de insolação", + "extreme_danger_of_heatstroke": "Perigo extremo de insolação", + "circulatory_collapse_imminent": "Colapso circulatório iminente" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/pt.json b/custom_components/thermal_comfort/translations/pt.json new file mode 100644 index 00000000..2ebd5758 --- /dev/null +++ b/custom_components/thermal_comfort/translations/pt.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Sem risco", + "unlikely": "Improvável", + "probable": "Provável", + "high": "Muito provável" + } + }, + "dew_point_perception": { + "state": { + "dry": "Um pouco seco", + "very_comfortable": "Muito confortável", + "comfortable": "Confortável", + "ok_but_humid": "Um pouco húmido", + "somewhat_uncomfortable": "Algo desconfortável", + "quite_uncomfortable": "Muito húmido, muito desconfortável", + "extremely_uncomfortable": "Extremamente desconfortável, opressivo", + "severely_high": "Alto risco para a saúde, mortal em caso de asma" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Frio", + "slightly_cool": "Um pouco frio", + "comfortable": "Confortável", + "slightly_warm": "Um pouco quente", + "increasing_discomfort": "Desconforto crescente", + "extremely_warm": "Muito quente", + "danger_of_heatstroke": "Perigo de insolação", + "extreme_danger_of_heatstroke": "Perigo extremo de insolação", + "circulatory_collapse_imminent": "Colapso circulatório iminente" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/ro.json b/custom_components/thermal_comfort/translations/ro.json new file mode 100644 index 00000000..7928ceaf --- /dev/null +++ b/custom_components/thermal_comfort/translations/ro.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Senzorul de temperatură nu a fost găsit", + "humidity_not_found": "Senzorul de umiditate nu a fost găsit" + }, + "step": { + "init": { + "title": "Setări de confort termic", + "data": { + "temperature_sensor": "Senzor de temperatura", + "humidity_sensor": "Senzor de umiditate", + "poll": "Activați sondajul", + "scan_interval": "Interval de sondaj (secunde)", + "custom_icons": "Utilizați pachetul de pictograme personalizate" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Această combinație de senzori de temperatură și umiditate este deja configurată", + "no_sensors": "Nu s-au găsit senzori de temperatură sau umiditate. Încercați din nou în modul avansat.", + "no_sensors_advanced": "Nu s-au găsit senzori de temperatură sau umiditate." + }, + "error": { + "temperature_not_found": "Senzorul de temperatură nu a fost găsit", + "humidity_not_found": "Senzorul de umiditate nu a fost găsit" + }, + "step": { + "user": { + "title": "Setări de confort termic", + "data": { + "name": "Nume", + "temperature_sensor": "Senzor de temperatura", + "humidity_sensor": "Senzor de umiditate", + "poll": "Activați sondajul", + "scan_interval": "Interval de sondaj (secunde)", + "custom_icons": "Utilizați pachetul de pictograme personalizate", + "enabled_sensors": "Senzori activați" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Niciun risc", + "unlikely": "Improbabil", + "probable": "Probabil", + "high": "Probabilitate mare" + } + }, + "dew_point_perception": { + "state": { + "dry": "Cam uscat pentru unii", + "very_comfortable": "Foarte confortabil", + "comfortable": "Confortabil", + "ok_but_humid": "OK pentru majoritatea, dar umed", + "somewhat_uncomfortable": "Oarecum inconfortabil", + "quite_uncomfortable": "Foarte umed, destul de inconfortabil", + "extremely_uncomfortable": "Extrem de inconfortabil, opresiv", + "severely_high": "Sever ridicat, chiar mortal pentru bolile legate de astm" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Confortabil", + "noticable_discomfort": "Disconfort perceptibil", + "evident_discomfort": "Disconfort evident", + "great_discomfort": "Disconfort mare, evitați eforturile", + "dangerous_discomfort": "Disconfort periculos", + "heat_stroke": "Posibil accident vascular cerebral cauzat de căldură excesivă" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "În afara intervalului calculabil", + "comfortable": "Confortabil", + "slight_discomfort": "Ușor disconfort", + "discomfort": "Disconfort", + "significant_discomfort": "Disconfort semnificativ", + "extreme_discomfort": "Disconfort extrem" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Rece", + "slightly_cool": "Ușor rece", + "comfortable": "Confortabil", + "slightly_warm": "Ușor cald", + "increasing_discomfort": "Creșterea disconfortului", + "extremely_warm": "Extrem de cald", + "danger_of_heatstroke": "Pericol de insolație", + "extreme_danger_of_heatstroke": "Pericol extrem de insolație", + "circulatory_collapse_imminent": "Colapsul circulator iminent" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "În afara intervalului calculabil", + "comfortable": "Confortabil", + "slightly_uncomfortable": "Ușor inconfortabil", + "moderatly_uncomfortable": "Moderat inconfortabil", + "highly_uncomfortable": "Foarte inconfortabil" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Niciun disconfort", + "less_then_half": "Mai puțin de jumătate din populație simte disconfort", + "more_then_half": "Mai mult de jumătate din populație simte disconfort", + "most": "Majoritatea persoanelor simt disconfort și deteriorarea condițiilor psihofizice", + "everyone": "Toată lumea simte disconfort semnificativ", + "dangerous": "Disconfort periculos, foarte puternic, care poate provoca epuizare termică" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/ru.json b/custom_components/thermal_comfort/translations/ru.json new file mode 100644 index 00000000..3d43a682 --- /dev/null +++ b/custom_components/thermal_comfort/translations/ru.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Датчик температуры не найден", + "humidity_not_found": "Датчик влажности не найден" + }, + "step": { + "init": { + "title": "Настройки теплового комфорта", + "data": { + "temperature_sensor": "Датчик температуры", + "humidity_sensor": "Датчик влажности", + "poll": "Включить опрос датчиков", + "scan_interval": "Интервал опроса (секунды)", + "custom_icons": "Использовать иконки от интеграции (требуется установка Thermal Comfort Icons)" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Эта комбинация датчиков температуры и влажности уже настроена.", + "no_sensors": "Не найдено ни одного подходящего датчика температуры или влажности. Попробуйте настройку в Расширеном режиме.", + "no_sensors_advanced": "Не найдено ни одного датчика температуры или влажности." + }, + "error": { + "temperature_not_found": "Датчик температуры не найден", + "humidity_not_found": "Датчик влажности не найден" + }, + "step": { + "user": { + "title": "Настройки теплового комфорта", + "data": { + "name": "Название", + "temperature_sensor": "Датчик температуры", + "humidity_sensor": "Датчик влажности", + "poll": "Включить опрос датчиков", + "scan_interval": "Интервал опроса (секунды)", + "custom_icons": "Использовать иконки от интеграции (требуется установка Thermal Comfort Icons)", + "enabled_sensors": "Активировать следующие датчики" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Не опасно", + "unlikely": "Маловероятно", + "probable": "Вероятно", + "high": "Очень вероятно" + } + }, + "dew_point_perception": { + "state": { + "dry": "Сухо", + "very_comfortable": "Очень комфортно", + "comfortable": "Комфортно", + "ok_but_humid": "Нормально, но сыровато", + "somewhat_uncomfortable": "Слегка некомфортно", + "quite_uncomfortable": "Довольно влажно, некомфортно", + "extremely_uncomfortable": "Очень влажно, угнетающе", + "severely_high": "Очень высокая влажность, опасно для астматиков" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Комфортно", + "noticable_discomfort": "Заметный дискомфорт", + "evident_discomfort": "Дискомфотно", + "great_discomfort": "Серьезный дискомфорт, избегайте физических нагрузок", + "dangerous_discomfort": "Опасный дискомфорт", + "heat_stroke": "Возможен тепловой удар" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Значения датчиков вне допустимого диапазона", + "comfortable": "Комфортно", + "slight_discomfort": "Слегка некомфортно", + "discomfort": "Некомфортно", + "significant_discomfort": "Очень некомфортно", + "extreme_discomfort": "Черезвычайно некомфортно" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Холодно", + "slightly_cool": "Прохладно", + "comfortable": "Комфортно", + "slightly_warm": "Тепло", + "increasing_discomfort": "Жарко", + "extremely_warm": "Очень жарко", + "danger_of_heatstroke": "Риск теплового удара", + "extreme_danger_of_heatstroke": "Серьёзный риск теплового удара", + "circulatory_collapse_imminent": "Возможны сосудистые нарушения" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Значения датчиков вне допустимого диапазона", + "comfortable": "Комфортно", + "slightly_uncomfortable": "Слегка некомфортно", + "moderatly_uncomfortable": "Некомфортно", + "highly_uncomfortable": "Очень некомфортно" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Нет дискомфорта", + "less_then_half": "Менее половины населения испытывает дискомфорт", + "more_then_half": "Больше половины населения испытывает дискомфорт", + "most": "Большинство испытывает дискомфорт и ухудшение психофизического состояния", + "everyone": "Все испытывают существенный дискомфорт", + "dangerous": "Опасно, очень сильный дискомфорт, который может вызвать тепловой удар" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/sk.json b/custom_components/thermal_comfort/translations/sk.json new file mode 100644 index 00000000..7e9cc4f7 --- /dev/null +++ b/custom_components/thermal_comfort/translations/sk.json @@ -0,0 +1,121 @@ +{ + "options": { + "error": { + "temperature_not_found": "Teplotný snímač nenájdený", + "humidity_not_found": "Snímač vlhkosti nenájdený" + }, + "step": { + "init": { + "title": "Nastavenia tepelnej pohody", + "data": { + "temperature_sensor": "Teplotný snímač", + "humidity_sensor": "Snímač vlhkosti", + "poll": "Povoliť dotazovanie", + "scan_interval": "Interval dotazovania (sekundy)", + "custom_icons": "Používanie vlastného balíka ikon" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Táto kombinácia snímačov teploty a vlhkosti je už nakonfigurovaná", + "no_sensors": "Nenašli sa žiadne snímače teploty alebo vlhkosti. Skúste to znova v rozšírenom režime.", + "no_sensors_advanced": "Nenašli sa žiadne snímače teploty alebo vlhkosti." + }, + "error": { + "temperature_not_found": "Teplotný snímač nenájdený", + "humidity_not_found": "Snímač vlhkosti nenájdený" + }, + "step": { + "user": { + "title": "Nastavenia tepelnej pohody", + "data": { + "name": "Názov", + "temperature_sensor": "Teplotný snímač", + "humidity_sensor": "Snímač vlhkosti", + "poll": "Povoliť dotazovanie", + "scan_interval": "Interval dotazovania (sekundy)", + "custom_icons": "Používanie vlastného balíka ikon", + "enabled_sensors": "Povolené senzory" + } + } + } + }, + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Žiadne riziko", + "unlikely": "Nepravdepodobné", + "probable": "Pravdepodobné", + "high": "Vysoká pravdepodobnosť" + } + }, + "dew_point_perception": { + "state": { + "dry": "Pre niekoho suché", + "very_comfortable": "Veľmi komfortné", + "comfortable": "Príjemné", + "ok_but_humid": "Pre vǎčšinu OK, ale vlhké", + "somewhat_uncomfortable": "Trochu nepríjemné", + "quite_uncomfortable": "Veľmi vlhké, dosť nepríjemné", + "extremely_uncomfortable": "Extrémne nekomfortné, tiesnivé", + "severely_high": "Veľmi vysoká, pre astmatikov smrteľná vlhkosť" + } + }, + "humidex_perception": { + "state": { + "comfortable": "Príjemne", + "noticable_discomfort": "Noticeable discomfort", + "evident_discomfort": "Evident discomfort", + "great_discomfort": "Great discomfort, avoid exertion", + "dangerous_discomfort": "Dangerous discomfort", + "heat_stroke": "Heat stroke possible" + } + }, + "relative_strain_perception": { + "state": { + "outside_calculable_range": "Outside of the calculable range", + "comfortable": "Príjemne", + "slight_discomfort": "Mierne nepohodlie", + "discomfort": "Diskomfort", + "significant_discomfort": "Významný diskomfort", + "extreme_discomfort": "Extrémny discomfort" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Chladno", + "slightly_cool": "Mierne chladno", + "comfortable": "Príjemne", + "slightly_warm": "Mierne teplo", + "increasing_discomfort": "Stupňujúce sa nepohodlie", + "extremely_warm": "Extrémne teplo", + "danger_of_heatstroke": "Nebezpečenstvo úpalu", + "extreme_danger_of_heatstroke": "Extrémne nebezpečenstvo úpalu", + "circulatory_collapse_imminent": "Hroziaci kolaps krvného obehu" + } + }, + "scharlau_perception": { + "state": { + "outside_calculable_range": "Mimo vypočítateľného rozsahu", + "comfortable": "Príjemne", + "slightly_uncomfortable": "Mierny diskomfort", + "moderatly_uncomfortable": "Stredne nepríjemné", + "highly_uncomfortable": "Veľmy nepríjemné" + } + }, + "thoms_discomfort_perception": { + "state": { + "no_discomfort": "Žiadne nepohodlie", + "less_then_half": "Menej ako polovica populácie pociťuje nepríjemné pocity", + "more_then_half": "Viac ako polovica populácie pociťuje nepohodlie", + "most": "Väčšina jednotlivcov pociťuje nepohodlie a zhoršenie psychofyzických podmienok", + "everyone": "Každý pociťuje výrazné nepohodlie", + "dangerous": "Nebezpečné, veľmi silné nepohodlie, ktoré môže spôsobiť úpal" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/sv.json b/custom_components/thermal_comfort/translations/sv.json new file mode 100644 index 00000000..b0128b1f --- /dev/null +++ b/custom_components/thermal_comfort/translations/sv.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Ingen risk", + "unlikely": "Osannolikt", + "probable": "Sannolikt", + "high": "Stor risk" + } + }, + "dew_point_perception": { + "state": { + "dry": "Lite torrt", + "very_comfortable": "Väldigt bekväm", + "comfortable": "Bekvämt", + "ok_but_humid": "Ok, något fuktigt", + "somewhat_uncomfortable": "Något obekvämt", + "quite_uncomfortable": "Väldigt fuktigt, ganska obekvämt", + "extremely_uncomfortable": "Tryckande, extremt obekvämt", + "severely_high": "Allvarligt högt, kan vara dödlig för astmarelaterade sjukdomar" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Svalt", + "slightly_cool": "Ganska svalt", + "comfortable": "Bekvämt", + "slightly_warm": "Ganska varmt", + "increasing_discomfort": "Börjar bli obekvämt", + "extremely_warm": "Extremt varmt", + "danger_of_heatstroke": "Risk för värmeslag", + "extreme_danger_of_heatstroke": "Extrem risk för värmeslag", + "circulatory_collapse_imminent": "Allvarlig fara för kollaps" + } + } + } + } +} diff --git a/custom_components/thermal_comfort/translations/uk.json b/custom_components/thermal_comfort/translations/uk.json new file mode 100644 index 00000000..9b2911be --- /dev/null +++ b/custom_components/thermal_comfort/translations/uk.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "frost_risk": { + "state": { + "no_risk": "Безпечно", + "unlikely": "Малоймовірно", + "probable": "Ймовірно", + "high": "Висока ймовірність" + } + }, + "dew_point_perception": { + "state": { + "dry": "Сухо", + "very_comfortable": "Дуже комфортно", + "comfortable": "Комфортно", + "ok_but_humid": "Нормально, але волого", + "somewhat_uncomfortable": "Трохи некомфортно", + "quite_uncomfortable": "Досить волого, некомфортно", + "extremely_uncomfortable": "Дуже волого, гнітюче", + "severely_high": "Дуже висока вологість, може бути смертельною для для астматиків" + } + }, + "summer_simmer_perception": { + "state": { + "cool": "Холодно", + "slightly_cool": "Прохолодно", + "comfortable": "Комфортно", + "slightly_warm": "Тепло", + "increasing_discomfort": "Жарко", + "extremely_warm": "Дуже жарко", + "danger_of_heatstroke": "Ризик теплового удару", + "extreme_danger_of_heatstroke": "Серйозний ризик теплового удару", + "circulatory_collapse_imminent": "Можливі судинні розлади" + } + } + } + } +} diff --git a/device_trackers.yaml b/device_trackers.yaml index d5dcc324..4c22f937 100644 --- a/device_trackers.yaml +++ b/device_trackers.yaml @@ -34,3 +34,5 @@ community: !secret snmp_community baseoid: 1.3.6.1.2.1.4.22.1.2 track_new_devices: false + +- platform: bluetooth_le_tracker diff --git a/esphome/bandwidthometer.yaml b/esphome/bandwidthometer.yaml index bdedcceb..7282d689 100644 --- a/esphome/bandwidthometer.yaml +++ b/esphome/bandwidthometer.yaml @@ -1,3 +1,4 @@ +--- substitutions: device_name: bandwidthometer device_description: Bandwidthometer Display @@ -12,8 +13,6 @@ esphome: esp8266: board: d1_mini - framework: - version: 2.7.4 captive_portal: @@ -61,12 +60,12 @@ light: output: pwm_output gamma_correct: 0 name: "${friendly_name} Display" - - platform: fastled_clockless + - platform: neopixelbus id: ws2811_backlight - chipset: WS2811 + variant: WS2811 pin: D1 num_leds: 4 - rgb_order: GRB + type: GRB name: "${friendly_name} Light" effects: - strobe: diff --git a/esphome/common/alarm-panel-esp8266.yaml b/esphome/common/alarm-panel-esp8266.yaml new file mode 100644 index 00000000..6d50e024 --- /dev/null +++ b/esphome/common/alarm-panel-esp8266.yaml @@ -0,0 +1,94 @@ +--- +#### +## +## Konnected Alarm Panel (ESP8266) +## Firmware configuration for ESPHome +## +## filename: alarm-panel-esp8266.yaml +## GitHub: https://github.com/konnected-io/konnected-esphome +## Buy Konnected hardware: https://konnected.io +## Help & Support: https://help@konnected.io (support is provided for purchasers of Konnected hardware) +## +## Copyright© 2023 Konnected 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. +## +#### + +#### +# PACKAGES +# Each package includes an alarm panel feature +# Remove or comment out any packages that you do not need or want. +packages: + + remote_package: + url: http://github.com/konnected-io/konnected-esphome + ref: master + refresh: 5min + files: + + #### + # CORE + # This package is required and sets up core features. + - packages/alarm-panel-esp8266-base.yaml + - packages/alarm-panel/zone1.yaml + - packages/alarm-panel/zone2.yaml + - packages/alarm-panel/zone3.yaml + - packages/alarm-panel/zone4.yaml + - packages/alarm-panel/zone5.yaml + - packages/alarm-panel/zone6.yaml + - packages/alarm-panel/alarm.yaml + + #### + # WIFI + # Enables WiFi connectivity and diagnostics. + - packages/wifi.yaml + + #### + # WARNING BEEP + # Enables a 'Warning Beep' entity, intended to be used with a piezo buzzer. This is + # implemented using a light entity with strobe effect to create a repeated beeping sound. + - packages/warning-beep.yaml + + #### + # STATUS LED + # Enables the onboard blue status LED as an activity/error indicator + - packages/status-led.yaml + +dashboard_import: + package_import_url: github://konnected-io/konnected-esphome/alarm-panel-esp8266.yaml@master + import_full_config: false + +#### +# WEB SEVER +# Enables the built-in web server for viewing the device state, internals and controls via web browser +# on the same local network as the device. +web_server: + include_internal: true + +#### +# LOGGER +# more: https://esphome.io/components/logger.html +logger: + +#### +# NATIVE API for HOME ASSISTANT +# Enables the native API for Home Assistant +# more: https://esphome.io/components/api.html +api: + +#### +# OTA UPDATES +# Enables over-the-air updates +# more: https://esphome.io/components/ota.html +ota: diff --git a/esphome/common/bathroom_pwm_common.yaml b/esphome/common/bathroom_pwm_common.yaml index 25108048..fcfd14a5 100644 --- a/esphome/common/bathroom_pwm_common.yaml +++ b/esphome/common/bathroom_pwm_common.yaml @@ -27,10 +27,14 @@ output: inverted: true - platform: esp8266_pwm id: warm + min_power: 0.05 + zero_means_zero: true pin: D2 inverted: false - platform: esp8266_pwm id: cold + min_power: 0.05 + zero_means_zero: true pin: D3 inverted: false diff --git a/esphome/common/common.yaml b/esphome/common/common.yaml index 38ff4267..c4ee3e9b 100644 --- a/esphome/common/common.yaml +++ b/esphome/common/common.yaml @@ -19,7 +19,7 @@ sensor: name: ${friendly_name} Uptime filters: - lambda: return x / 60.0; - unit_of_measurement: minutes + unit_of_measurement: min binary_sensor: # Reports if this device is Connected or not diff --git a/esphome/common/esp32-ble-tracker.yaml b/esphome/common/esp32-ble-tracker.yaml new file mode 100644 index 00000000..194e9738 --- /dev/null +++ b/esphome/common/esp32-ble-tracker.yaml @@ -0,0 +1,83 @@ +--- +packages: + common: !include common.yaml + +esphome: + name: ${device_name} + comment: ${device_description} + platform: ESP32 + board: esp32dev + +# Enable logging +logger: + +captive_portal: + +web_server: + port: 80 + +syslog: + ip_address: 172.24.32.13 + port: 515 + + +esp32_ble_tracker: + scan_parameters: + active: true + interval: 160ms + + on_ble_advertise: + # https://esphome.io/components/esp32_ble_tracker.html#on-ble-advertise-trigger + # - mac_address: 11:22:33:44:55:66 + # then: + - lambda: |- + ESP_LOGD("ble_adv", "New BLE device"); + ESP_LOGD("ble_adv", " address: %s", x.address_str().c_str()); + ESP_LOGD("ble_adv", " name: %s", x.get_name().c_str()); + ESP_LOGD("ble_adv", " Advertised service UUIDs:"); + for (auto uuid : x.get_service_uuids()) { + ESP_LOGD("ble_adv", " - %s", uuid.to_string().c_str()); + } + ESP_LOGD("ble_adv", " Advertised service data:"); + for (auto data : x.get_service_datas()) { + ESP_LOGD("ble_adv", " - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size()); + } + ESP_LOGD("ble_adv", " Advertised manufacturer data:"); + for (auto data : x.get_manufacturer_datas()) { + ESP_LOGD("ble_adv", " - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size()); + } + +bluetooth_proxy: + active: true + +text_sensor: + # https://esphome.io/components/text_sensor/ble_scanner.html + - platform: ble_scanner + name: "BLE Devices Scanner" + +sensor: + # https://esphome.io/components/sensor/xiaomi_ble.html#lywsd03mmc + - platform: pvvx_mithermometer + mac_address: "A4:C1:38:D0:BF:A7" + temperature: + name: "Study BLE Temperature" + humidity: + name: "Study BLE Humidity" + battery_level: + name: "Study BLE Battery-Level" + battery_voltage: + name: "Study BLE Battery-Voltage" + signal_strength: + name: "Study BLE Signal" + +binary_sensor: + - platform: ble_presence + mac_address: B0:F1:D8:EA:D0:36 + name: "iPad - PC1408" + - platform: ble_presence + ibeacon_uuid: 53ACB924-F814-EA4C-9BCC-73CD97F6F5B6 + name: "Nothing Phone (1)" + +# - platform: ble_presence +# mac_address: !env_var BT_BEACON_02 +# name: "jinou_beacon_02" diff --git a/esphome/common/esp32_camera_common.yaml b/esphome/common/esp32_camera_common.yaml index ac7bbff7..2de69c86 100644 --- a/esphome/common/esp32_camera_common.yaml +++ b/esphome/common/esp32_camera_common.yaml @@ -1,3 +1,4 @@ +--- # Use ESPHome-Flasher, as esptool.py doesn't seem to do some sort of magic with the bootloader. # See https://github.com/esphome/issues/issues/598 @@ -33,7 +34,8 @@ esp32_camera: # Image settings name: ${friendly_name} Camera - resolution: 1600x1200 + # resolution: 1600x1200 # Works but slow + resolution: 1280x1024 # Works well # resolution: 800x600 XX # resolution: 640x480 XX @@ -49,6 +51,12 @@ output: channel: 3 # Don't stomp on the cameras use of timer channel 1 pin: GPIO4 +esp32_camera_web_server: + - port: 8080 + mode: stream + - port: 8081 + mode: snapshot + light: # ... and then make a light out of it. - platform: monochromatic diff --git a/esphome/common/h801_common.yaml b/esphome/common/h801_common.yaml index 265bdece..a4cf2573 100644 --- a/esphome/common/h801_common.yaml +++ b/esphome/common/h801_common.yaml @@ -1,3 +1,4 @@ +--- packages: common: !include common.yaml @@ -26,21 +27,31 @@ output: frequency: 1000 Hz id: pwm_b - platform: esp8266_pwm - pin: 13 + pin: GPIO13 frequency: 1000 Hz id: pwm_g - platform: esp8266_pwm - pin: 15 + pin: GPIO15 frequency: 1000 Hz id: pwm_r + - platform: esp8266_pwm + pin: GPIO14 + frequency: 1000 Hz + id: pwm_w2 + - platform: esp8266_pwm + pin: GPIO04 + frequency: 1000 Hz + id: pwm_w1 # RGB, RGBW, RGBWW also available light: - platform: rgb - name: "H801 Light" + name: ${friendly_name} red: pwm_r green: pwm_g blue: pwm_b + #cold_white: pwm_w1 + #warm_white: pwm_w2 id: thelight effects: - random: diff --git a/esphome/common/localbytes_plug_pm_common.yaml b/esphome/common/localbytes_plug_pm_common.yaml new file mode 100644 index 00000000..b1e4b36d --- /dev/null +++ b/esphome/common/localbytes_plug_pm_common.yaml @@ -0,0 +1,11 @@ +--- +substitutions: + project_name: localbytes.plug-pm + package_import_url: github://kylegordon/home-assistant-config/esphome/common/localbytes-plug-pm-common.yaml@main + binary_sensor_pin: GPIO3 + switch_pin: GPIO14 + sensor_sel_pin: GPIO12 + sensor_cf_pin: GPIO4 + sensor_cf1_pin: GPIO5 + output_pin: GPIO13 + status_led_pin: GPIO0 diff --git a/esphome/common/packages/alarm-panel-esp8266-base.yaml b/esphome/common/packages/alarm-panel-esp8266-base.yaml new file mode 100644 index 00000000..202e2948 --- /dev/null +++ b/esphome/common/packages/alarm-panel-esp8266-base.yaml @@ -0,0 +1,43 @@ +--- +substitutions: + + #### + # NAME + # By default, the name of the ESPHome device is "alarm-panel-pro-xxxxxx" where xxxxxx is a unique identifier. The device's + # hostname on your network is also defined by the name, defaulting to "alarm-panel-pro-xxxxxx.local". Edit this variable to + # customize the name and hostname. Note: only lowercase characters, numbers and hyphen(-) are allowed. + name: alarm-panel + friendly_name: Alarm Panel + project_name: konnected.alarm-panel-esp8266 + project_version: "0.3.1" + + #### + # SETTINGS + sensor_debounce_time: 200ms + warning_beep_pulse_time: 100ms + warning_beep_pause_time: 130ms + warning_beep_internal_only: "false" + + #### + # ZONE MAPPING + # Do not edit this section. + zone1: D1 + zone2: D2 + zone3: D5 + zone4: D6 + zone5: D7 + zone6: D9 + alarm: D8 + out: D8 + status_led: D4 + + #### + # CONNECTION MAPPINGS + warning_beep_pin: $out + +packages: + remote_package: + url: http://github.com/konnected-io/konnected-esphome + ref: master + refresh: 5min + file: packages/core-esp8266.yaml diff --git a/esphome/common/packages/core-esp8266.yaml b/esphome/common/packages/core-esp8266.yaml new file mode 100644 index 00000000..db567a70 --- /dev/null +++ b/esphome/common/packages/core-esp8266.yaml @@ -0,0 +1,13 @@ +--- +esphome: + name: $name + name_add_mac_suffix: false + platform: ESP8266 + board: nodemcuv2 + esp8266_restore_from_flash: true + project: + name: $project_name + version: $project_version + +substitutions: + status_led_inverted: "true" diff --git a/esphome/common/packages/status-led.yaml b/esphome/common/packages/status-led.yaml new file mode 100644 index 00000000..3a9bcd6a --- /dev/null +++ b/esphome/common/packages/status-led.yaml @@ -0,0 +1,7 @@ +--- +light: + - platform: status_led + id: blue_status_led + pin: + number: $status_led + inverted: $status_led_inverted diff --git a/esphome/common/packages/warning-beep.yaml b/esphome/common/packages/warning-beep.yaml new file mode 100644 index 00000000..7c76af49 --- /dev/null +++ b/esphome/common/packages/warning-beep.yaml @@ -0,0 +1,19 @@ +--- +output: + - id: buzzer_output + platform: gpio + pin: $warning_beep_pin + +light: + - id: warning_beep + name: Warning Beep + platform: binary + output: buzzer_output + effects: + - strobe: + colors: + - state: true + duration: $warning_beep_pulse_time + - state: false + duration: $warning_beep_pulse_time + internal: $warning_beep_internal_only diff --git a/esphome/common/packages/wifi.yaml b/esphome/common/packages/wifi.yaml new file mode 100644 index 00000000..d6528b2e --- /dev/null +++ b/esphome/common/packages/wifi.yaml @@ -0,0 +1,29 @@ +--- +wifi: + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + +improv_serial: + +captive_portal: + +sensor: + - platform: wifi_signal + name: $friendly_name WiFi Signal + id: wifi_signal_db + entity_category: diagnostic + + - platform: copy + source_id: wifi_signal_db + id: wifi_signal_pct + name: $friendly_name WiFi Signal % + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "%" + entity_category: diagnostic + +text_sensor: + - platform: wifi_info + ip_address: + name: $friendly_name IP Address + entity_category: diagnostic diff --git a/esphome/common/packages/zone1.yaml b/esphome/common/packages/zone1.yaml new file mode 100644 index 00000000..7431c79f --- /dev/null +++ b/esphome/common/packages/zone1.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone1 + name: Zone 1 + pin: + number: $zone1 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/packages/zone2.yaml b/esphome/common/packages/zone2.yaml new file mode 100644 index 00000000..8a551e5c --- /dev/null +++ b/esphome/common/packages/zone2.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone2 + name: Zone 2 + pin: + number: $zone2 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/packages/zone3.yaml b/esphome/common/packages/zone3.yaml new file mode 100644 index 00000000..9e5fa2b4 --- /dev/null +++ b/esphome/common/packages/zone3.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone3 + name: Zone 3 + pin: + number: $zone3 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/packages/zone4.yaml b/esphome/common/packages/zone4.yaml new file mode 100644 index 00000000..24a47386 --- /dev/null +++ b/esphome/common/packages/zone4.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone4 + name: Zone 4 + pin: + number: $zone4 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/packages/zone5.yaml b/esphome/common/packages/zone5.yaml new file mode 100644 index 00000000..bba387fd --- /dev/null +++ b/esphome/common/packages/zone5.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone5 + name: Zone 5 + pin: + number: $zone5 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/packages/zone6.yaml b/esphome/common/packages/zone6.yaml new file mode 100644 index 00000000..e06e9c04 --- /dev/null +++ b/esphome/common/packages/zone6.yaml @@ -0,0 +1,16 @@ +--- +binary_sensor: + + - id: zone6 + name: Zone 6 + pin: + number: $zone6 + mode: INPUT_PULLUP + platform: gpio + filters: + - delayed_on_off: $sensor_debounce_time + on_state: + then: + - light.turn_on: blue_status_led + - delay: 100ms + - light.turn_off: blue_status_led diff --git a/esphome/common/power_plug_common.yaml b/esphome/common/power_plug_common.yaml new file mode 100644 index 00000000..743c8e19 --- /dev/null +++ b/esphome/common/power_plug_common.yaml @@ -0,0 +1,332 @@ +--- +#### Notes here +# Values from https://gist.github.com/timmo001/7b0cf9958b80f6356a3f47d2f29aa1a6 +#### End notes + +packages: + common: !include common.yaml + +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: ${device_description} + + # Automatically add the mac address to the name + # so you can use a single firmware for all devices + # name_add_mac_suffix: true + + # This will allow for (future) project identification, + # configuration and updates. + project: + name: ${project_name} + version: "1.6.2" + +esp8266: + board: esp01_1m + restore_from_flash: true + +logger: + +syslog: + ip_address: 172.24.32.13 + port: 515 + +# This should point to the public location of this yaml file. +dashboard_import: + package_import_url: ${package_import_url} + +captive_portal: + +web_server: + port: 80 + +time: + - platform: homeassistant + timezone: Europe/London + +api: + services: + - service: calibrate_voltage + variables: + actual_value: float + then: + - lambda: |- + id(voltage_multiply) = actual_value / id(voltage).raw_state; + - number.set: + id: voltage_factor + value: !lambda "return id(voltage_multiply);" + + - service: calibrate_power + variables: + actual_value: float + then: + - lambda: |- + id(power_multiply) = actual_value / id(power).raw_state; + - number.set: + id: power_factor + value: !lambda "return id(power_multiply);" + + - service: calibrate_current + variables: + actual_value: float + then: + - lambda: |- + id(current_multiply) = actual_value / id(current).raw_state; + - number.set: + id: current_factor + value: !lambda "return id(current_multiply);" + +globals: + - id: voltage_multiply + type: float + restore_value: true + initial_value: "0.3" + + - id: power_multiply + type: float + restore_value: true + initial_value: "0.133" + + - id: current_multiply + type: float + restore_value: true + initial_value: "0.805" + +binary_sensor: + # Push Button (Toggles Relay When Pressed) + - platform: gpio + pin: + number: ${binary_sensor_pin} + mode: INPUT_PULLUP + inverted: true + name: "Button" + on_click: + + - max_length: 1s + then: + if: + condition: + switch.is_off: disable_button + then: + switch.toggle: relay + + - min_length: 1.5s + max_length: 5s + then: + switch.toggle: disable_led + + - min_length: 8s + max_length: 12s + then: + switch.toggle: disable_button + +switch: + # Relay (As Switch) + - platform: gpio + name: "" + icon: "mdi:${main_icon}" + pin: ${switch_pin} + id: relay + restore_mode: "${default_state}" + on_turn_on: + if: + condition: + switch.is_off: disable_led + then: + light.turn_on: + id: led + on_turn_off: + - light.turn_off: + id: led + + - platform: template + name: "Disable LED" + id: disable_led + icon: "mdi:led-variant-off" + restore_mode: RESTORE_DEFAULT_OFF + optimistic: true + on_turn_on: + # Flash twice + - light.turn_off: led + - delay: 0.1s + - light.turn_on: led + - delay: 0.1s + - light.turn_off: led + - delay: 0.1s + - light.turn_on: led + - delay: 0.1s + # Final state + - light.turn_off: led + on_turn_off: + # Flash twice + - light.turn_on: led + - delay: 0.1s + - light.turn_off: led + - delay: 0.1s + - light.turn_on: led + - delay: 0.1s + - light.turn_off: led + - delay: 0.7s + # Final state + - if: + condition: + switch.is_on: relay + then: + light.turn_on: led + + - platform: template + name: "Disable Button" + id: disable_button + icon: "mdi:toggle-switch-off-outline" + restore_mode: RESTORE_DEFAULT_OFF + optimistic: true + on_turn_on: + # Flash thrice + - light.turn_off: led + - delay: 0.15s + - light.turn_on: led + - delay: 0.15s + - light.turn_off: led + - delay: 0.15s + - light.turn_on: led + - delay: 0.15s + - light.turn_off: led + - delay: 0.15s + - light.turn_on: led + - delay: 0.15s + # Final state + - if: + condition: + switch.is_off: relay + then: + light.turn_off: led + on_turn_off: + # Flash thrice + - light.turn_on: led + - delay: 0.15s + - light.turn_off: led + - delay: 0.15s + - light.turn_on: led + - delay: 0.15s + - light.turn_off: led + - delay: 0.15s + - light.turn_on: led + - delay: 0.15s + - light.turn_off: led + - delay: 0.7s + # Final state + - if: + condition: + switch.is_on: relay + then: + light.turn_on: led + +sensor: + # Power Monitoring + - platform: hlw8012 + sel_pin: + number: ${sensor_sel_pin} + inverted: true + cf_pin: ${sensor_cf_pin} + cf1_pin: ${sensor_cf1_pin} + change_mode_every: 3 + update_interval: 6s + + voltage: + name: "Voltage" + id: voltage + unit_of_measurement: V + accuracy_decimals: 1 + filters: + - lambda: return x * id(voltage_multiply); + + power: + name: "Power" + id: power + unit_of_measurement: W + accuracy_decimals: 0 + filters: + - lambda: return x * id(power_multiply); + + current: + name: "Current" + id: current + unit_of_measurement: A + accuracy_decimals: 3 + filters: + - lambda: return x * id(current_multiply); + + # Total daily energy sensor + - platform: total_daily_energy + name: "Daily Energy" + power_id: power + filters: + # Multiplication factor from W to kW is 0.001 + - multiply: 0.001 + unit_of_measurement: kWh + +# Make calibration factor data readable/setable from home assistant +number: + - platform: template + name: "Voltage Calibration Factor" + id: voltage_factor + icon: "mdi:sine-wave" + min_value: 0 + max_value: 10 + step: 0.001 + entity_category: diagnostic + mode: box + lambda: |- + return id(voltage_multiply); + set_action: + lambda: |- + id(voltage_multiply) = x; + + - platform: template + name: "Power Calibration Factor" + id: power_factor + icon: "mdi:flash" + min_value: 0 + max_value: 10 + step: 0.001 + entity_category: diagnostic + mode: box + lambda: |- + return id(power_multiply); + set_action: + lambda: |- + id(power_multiply) = x; + + - platform: template + name: "Current Calibration Factor" + id: current_factor + icon: "mdi:current-ac" + min_value: 0 + max_value: 10 + step: 0.001 + entity_category: diagnostic + mode: box + lambda: |- + return id(current_multiply); + set_action: + lambda: |- + id(current_multiply) = x; + +# Relay State LED +output: + - platform: esp8266_pwm + id: state_led + pin: + number: ${output_pin} + inverted: true + +light: + - platform: binary + output: state_led + id: led + +# Uses the red LED as a status indicator +status_led: + pin: + number: ${status_led_pin} + inverted: true diff --git a/esphome/common/teckin_sp23_common.yaml b/esphome/common/teckin_sp23_common.yaml index 76a0a015..84db2c53 100644 --- a/esphome/common/teckin_sp23_common.yaml +++ b/esphome/common/teckin_sp23_common.yaml @@ -1,137 +1,11 @@ --- - -# Sourced from https://frenck.dev/calibrating-an-esphome-flashed-power-plug/ -# and https://gist.github.com/timmo001/7b0cf9958b80f6356a3f47d2f29aa1a6 -# TV manual and specs at https://www.manualslib.com/manual/650244/Panasonic-Viera-Th-D42ps81ea.html?page=51#manual - -packages: - common: !include common.yaml - -esphome: - name: ${device_name} - comment: ${device_description} - platform: ESP8266 - board: esp01_1m - on_boot: - # Turn on power to attached device. - then: - - switch.turn_on: relay - -captive_portal: - -debug: - -# Enable logging -logger: - level: debug - -# Enable Web server -web_server: - port: 80 - -syslog: - ip_address: 172.24.32.13 - port: 515 - -sensor: - - platform: hlw8012 - sel_pin: - number: GPIO12 - inverted: true - cf_pin: GPIO05 - cf1_pin: GPIO14 - # Higher value gives lower watt readout - current_resistor: 0.00221 - # Lower value gives lower voltage readout - voltage_divider: 871 - change_mode_every: 3 - update_interval: 1s - # Current sensor - current: - name: ${friendly_name} Current - unit_of_measurement: A - accuracy_decimals: 3 - # filters: - # # Map from sensor -> measured value - # - calibrate_linear: - # - 0.0 -> 0.013 - # - 0.08208 -> 0.071 - # - 1.34223 -> 1.066 - # - 5.57170 -> 4.408 - # - 6.69184 -> 5.259 - # - 6.97187 -> 5.540 - # # Make everything below 0.01A appear as just 0A. - # # Furthermore it corrects 0.013A for the power usage of the plug. - # - lambda: if (x < (0.01 - 0.013)) return 0; else return (x - 0.013); - # Voltage sensor - voltage: - name: ${friendly_name} Voltage - unit_of_measurement: V - accuracy_decimals: 1 - # filters: - # # Map from sensor -> measured value - # - calibrate_linear: - # - 0.0 -> 0.0 - # - 602.87506 -> 229.9 - # - 609.8 -> 232.8 - # Power sensor - power: - id: power - name: ${friendly_name} Power - unit_of_measurement: W - accuracy_decimals: 0 - # filters: - # # Map from sensor -> measured value - # - calibrate_linear: - # - 0.0 -> 1.14 - # - 62.06167 -> 10.93 - # - 1503.27161 -> 247.6 - # - 1599.81213 -> 263.7 - # - 3923.67700 -> 631.4 - # - 7109.50928 -> 1148.0 - # - 7237.0857 -> 1193.0 - # - 7426.71338 -> 1217.0 - # # Make everything below 2W appear as just 0W. - # # Furthermore it corrects 1.14W for the power usage of the plug. - # - lambda: if (x < (2 + 1.14)) return 0; else return (x - 1.14); - -binary_sensor: - # Binary sensor for the button press - - platform: gpio - name: button - pin: - number: GPIO13 - inverted: true - on_press: - - switch.toggle: relay - -switch: - # Switch to toggle the relay - - platform: gpio - id: relay - name: ${friendly_name} Switch - pin: GPIO15 - on_turn_on: - - light.turn_on: led - on_turn_off: - - light.turn_off: led - -output: - # Relay state led - - platform: esp8266_pwm - id: state_led - pin: - number: GPIO2 - inverted: true - -light: - # Relay state light - - platform: monochromatic - output: state_led - id: led - -# Uses the red LED as a status indicator -status_led: - pin: - number: GPIO0 - inverted: true +substitutions: + project_name: teckinsp23.plug-pm + package_import_url: github://kylegordon/home-assistant-config/esphome/common/teckin_sp23-common.yaml@main + binary_sensor_pin: GPIO13 + switch_pin: GPIO15 + sensor_sel_pin: GPIO12 + sensor_cf_pin: GPIO5 + sensor_cf1_pin: GPIO14 + output_pin: GPIO2 + status_led_pin: GPIO0 diff --git a/esphome/dishwasher.yaml b/esphome/dishwasher.yaml deleted file mode 100644 index c5b7e6f4..00000000 --- a/esphome/dishwasher.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - device_name: dishwasher - device_description: Dishwasher Power Plug - friendly_name: Dishwasher Power Plug - -<<: !include common/teckin_sp23_common.yaml diff --git a/esphome/esp32-ble-tracker-1.yaml b/esphome/esp32-ble-tracker-1.yaml new file mode 100644 index 00000000..4c54b80e --- /dev/null +++ b/esphome/esp32-ble-tracker-1.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + device_name: esp32-ble-tracker-1 + device_description: ESP32 BLE Tracker 1 + friendly_name: ESP32 BLE Tracker 1 + +<<: !include common/esp32-ble-tracker.yaml diff --git a/esphome/ili9341_1.yaml b/esphome/ili9341_1.yaml index 6a1f1f2c..d5e0eb3c 100644 --- a/esphome/ili9341_1.yaml +++ b/esphome/ili9341_1.yaml @@ -1,8 +1,8 @@ +--- packages: common: !include common/common.yaml colors: !include common/colours.yaml - substitutions: device_name: ili9341 device_description: ILI9341 Display @@ -11,7 +11,8 @@ substitutions: esphome: name: ${device_name} comment: ${device_description} - platform: ESP32 + +esp32: board: nodemcu-32s captive_portal: @@ -102,8 +103,8 @@ touchscreen: display: - id: my_display - platform: ili9341 - model: "TFT 2.4" + platform: ili9xxx + model: ili9341 cs_pin: GPIO5 dc_pin: GPIO27 reset_pin: GPIO33 diff --git a/esphome/konnected-195dec.yaml b/esphome/konnected-195dec.yaml new file mode 100644 index 00000000..be7ca096 --- /dev/null +++ b/esphome/konnected-195dec.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + name: "konnected_3" + friendly_name: "Konnected Alarm Panel 3" + +<<: !include common/alarm-panel-esp8266.yaml +<<: !include common/packages/core-esp8266.yaml diff --git a/esphome/konnected-199839.yaml b/esphome/konnected-199839.yaml new file mode 100644 index 00000000..6833f617 --- /dev/null +++ b/esphome/konnected-199839.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + name: "konnected_2" + friendly_name: "Konnected Alarm Panel 2" + +<<: !include common/alarm-panel-esp8266.yaml +<<: !include common/packages/core-esp8266.yaml diff --git a/esphome/konnected-fdd1b2.yaml b/esphome/konnected-fdd1b2.yaml new file mode 100644 index 00000000..c03ce1bb --- /dev/null +++ b/esphome/konnected-fdd1b2.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + name: "konnected_1" + friendly_name: "Konnected Alarm Panel 1" + +<<: !include common/alarm-panel-esp8266.yaml +<<: !include common/packages/core-esp8266.yaml diff --git a/esphome/monitor_lights.yaml b/esphome/monitor_lights.yaml deleted file mode 100644 index 1e63f097..00000000 --- a/esphome/monitor_lights.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - device_name: monitor_lights - device_description: Monitor Lights Power Plug - friendly_name: Monitor Lights Power Plug - -<<: !include common/teckin_sp23_common.yaml diff --git a/esphome/power_plug_monitor_lights.yaml b/esphome/power_plug_monitor_lights.yaml new file mode 100644 index 00000000..f9d3299d --- /dev/null +++ b/esphome/power_plug_monitor_lights.yaml @@ -0,0 +1,11 @@ +--- +packages: + plug: !include common/teckin_sp23_common.yaml + common: !include common/power_plug_common.yaml + +substitutions: + device_name: monitor_lights + device_description: Monitor Lights Power Plug + friendly_name: Monitor Lights Power Plug + default_state: "RESTORE_DEFAULT_ON" + main_icon: power-socket-uk diff --git a/esphome/power_plug_server.yaml b/esphome/power_plug_server.yaml new file mode 100644 index 00000000..dd2bda97 --- /dev/null +++ b/esphome/power_plug_server.yaml @@ -0,0 +1,11 @@ +--- +packages: + plug: !include common/localbytes_plug_pm_common.yaml + common: !include common/power_plug_common.yaml + +substitutions: + device_name: power_plug_server + device_description: Server Power Plug + friendly_name: Server Power Plug + default_state: "RESTORE_DEFAULT_ON" + main_icon: power-socket-uk diff --git a/esphome/power_plug_shower_pump.yaml b/esphome/power_plug_shower_pump.yaml new file mode 100644 index 00000000..c78f34da --- /dev/null +++ b/esphome/power_plug_shower_pump.yaml @@ -0,0 +1,11 @@ +--- +packages: + plug: !include common/teckin_sp23_common.yaml + common: !include common/power_plug_common.yaml + +substitutions: + device_name: shower_pump + device_description: Shower Pump Power Plug + friendly_name: Shower Pump Power Plug + default_state: "RESTORE_DEFAULT_ON" + main_icon: power-socket-uk diff --git a/esphome/power_plug_tv.yaml b/esphome/power_plug_tv.yaml new file mode 100644 index 00000000..220e0d31 --- /dev/null +++ b/esphome/power_plug_tv.yaml @@ -0,0 +1,11 @@ +--- +packages: + plug: !include common/teckin_sp23_common.yaml + common: !include common/power_plug_common.yaml + +substitutions: + device_name: tv + device_description: TV Power Plug + friendly_name: TV Power Plug + default_state: "RESTORE_DEFAULT_ON" + main_icon: power-socket-uk diff --git a/esphome/power_plug_ups.yaml b/esphome/power_plug_ups.yaml new file mode 100644 index 00000000..8347df6b --- /dev/null +++ b/esphome/power_plug_ups.yaml @@ -0,0 +1,11 @@ +--- +packages: + plug: !include common/localbytes_plug_pm_common.yaml + common: !include common/power_plug_common.yaml + +substitutions: + device_name: ups_power_plug + device_description: UPS Power Plug + friendly_name: UPS Power Plug + default_state: "RESTORE_DEFAULT_ON" + main_icon: power-socket-uk diff --git a/esphome/shower_pump.yaml b/esphome/shower_pump.yaml deleted file mode 100644 index d80bd4cd..00000000 --- a/esphome/shower_pump.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - device_name: shower_pump - device_description: Shower Pump Power Plug - friendly_name: Shower Pump Power Plug - -<<: !include common/teckin_sp23_common.yaml diff --git a/esphome/tin_hut_shelf_lights_bench.yaml b/esphome/tin_hut_shelf_lights_bench.yaml new file mode 100644 index 00000000..568324af --- /dev/null +++ b/esphome/tin_hut_shelf_lights_bench.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + device_name: tin_hut_shelf_lights_bench + device_description: Tin Hut Shelf Light Bench + friendly_name: Tin Hut Shelf Lights Bench + +<<: !include common/h801_common.yaml diff --git a/esphome/tin_hut_shelf_lights_left.yaml b/esphome/tin_hut_shelf_lights_left.yaml new file mode 100644 index 00000000..d8fbaf3a --- /dev/null +++ b/esphome/tin_hut_shelf_lights_left.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + device_name: tin_hut_shelf_lights_left + device_description: Tin Hut Shelf Light Left + friendly_name: Tin Hut Shelf Lights Left + +<<: !include common/h801_common.yaml diff --git a/esphome/tin_hut_shelf_lights_right.yaml b/esphome/tin_hut_shelf_lights_right.yaml new file mode 100644 index 00000000..054432e9 --- /dev/null +++ b/esphome/tin_hut_shelf_lights_right.yaml @@ -0,0 +1,7 @@ +--- +substitutions: + device_name: tin_hut_shelf_lights_right + device_description: Tin Hut Shelf Light Right + friendly_name: Tin Hut Shelf Lights Right + +<<: !include common/h801_common.yaml diff --git a/esphome/tv.yaml b/esphome/tv.yaml deleted file mode 100644 index 3490dda1..00000000 --- a/esphome/tv.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - device_name: tv - device_description: TV Power Plug - friendly_name: TV Power Plug - -<<: !include common/teckin_sp23_common.yaml diff --git a/groups.yaml b/groups.yaml index 45a2a6ce..3f921652 100644 --- a/groups.yaml +++ b/groups.yaml @@ -4,7 +4,6 @@ default_view: entities: - group.people_status - group.modes - - sensor.electricity_energy_usage - sensor.house_average_temperature - sensor.house_average_humidity - calendar.next_bin @@ -26,8 +25,7 @@ kitchen: entities: - sensor.kitchen_temperature - sensor.kitchen_esp8266_signal_level - - binary_sensor.kitchen_motion - - sensor.kitchen_motion + - binary_sensor.kitchen_motion_occupancy - light.kitchen_cabinets - light.kitchen - media_player.kitchen @@ -40,8 +38,8 @@ front_hall: - sensor.front_hall_sensor_humidity - sensor.front_hall_sensor_signal_level - sensor.front_hall_lightlevel - - binary_sensor.front_hall_motion - - binary_sensor.front_hall_door + - binary_sensor.front_hall_motion_occupancy + - binary_sensor.front_hall_door_contact living_room: name: Living Room @@ -50,8 +48,7 @@ living_room: - media_player.living_room_kodi - media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f - switch.tellybox - - binary_sensor.living_room_motion - - sensor.living_room_motion + - binary_sensor.living_room_motion_occupancy - sensor.living_room_temperature - sensor.living_room_pressure - sensor.living_room_humidity @@ -94,7 +91,7 @@ ensuite: name: Ensuite entities: - sensor.ensuite_lightlevel - - binary_sensor.ensuite_motion + - binary_sensor.ensuite_motion_occupancy - light.ensuite_entrance - light.ensuite_towels - light.ensuite_shower @@ -116,9 +113,9 @@ bedroom_2: name: Guest bedroom entities: - light.guest_bedroom - - sensor.guest_room_temperature - - sensor.guest_room_pressure - - sensor.guest_room_humidity + - sensor.guest_room_multi_sensor_temperature + - sensor.guest_room_multi_sensor_pressure + - sensor.guest_room_multi_sensor_humidity bedroom_3: name: Study @@ -171,22 +168,22 @@ hall_view: name: Hall entities: - light.hall - - binary_sensor.hall_rooms_motion + - binary_sensor.hall_rooms_motion_occupancy - sensor.hall_rooms_motion - - binary_sensor.hall_door_motion - - binary_sensor.boot_room_motion + - binary_sensor.hall_door_motion_occupancy + - binary_sensor.boot_room_motion_occupancy - sensor.hall_door_motion - - sensor.hall_temperature - - sensor.hall_humidity - - sensor.hall_pressure + - sensor.hallway_multi_sensor_temperature + - sensor.hallway_multi_sensor_humidity + - sensor.hallway_multi_sensor_pressure - binary_sensor.boot_room_door outside: name: Outside entities: - sun.sun - - binary_sensor.outside_front_motion - - binary_sensor.outside_driveway_motion + - binary_sensor.back_door_pir + - binary_sensor.driveway_pir - binary_sensor.front_door_motion - group.climate - group.outside_lights @@ -207,6 +204,7 @@ outside_lights: - light.mini_deck_floodlight - light.decking_lights - light.front_door_dome + - light.car_port climate: name: Climate diff --git a/input_boolean/classic_fm.yaml b/input_boolean/classic_fm.yaml deleted file mode 100644 index 26e147b6..00000000 --- a/input_boolean/classic_fm.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: Classic FM -initial: false diff --git a/lights.yaml b/lights.yaml index f82d889f..33209b71 100644 --- a/lights.yaml +++ b/lights.yaml @@ -63,3 +63,10 @@ - platform: switch name: Octoprint Lighting entity_id: switch.octoprint_relay_1 + +- platform: group + name: Tin Hut Shelving + entities: + - light.tin_hut_shelf_lights_left + - light.tin_hut_shelf_lights_right + - light.tin_hut_shelf_lights_bench diff --git a/media_players.yaml b/media_players.yaml index 3c72f537..a1752bfc 100644 --- a/media_players.yaml +++ b/media_players.yaml @@ -11,7 +11,6 @@ data: mac: 6c:3b:e5:25:8b:29 broadcast_address: 172.24.32.255 -- platform: openhome - platform: smartir name: "Living Room Television" diff --git a/notify.yaml b/notify.yaml index aa164192..472ab49a 100644 --- a/notify.yaml +++ b/notify.yaml @@ -1,25 +1,26 @@ - - platform: twitter - name: twitter_thegordonhome - consumer_key: !secret twitter_consumer_key - consumer_secret: !secret twitter_consumer_secret - access_token: !secret twitter_access_token - access_token_secret: !secret twitter_access_token_secret - - platform: twitter - name: twitter_overuplawmoor - consumer_key: !secret overuplawmoor_consumer_key - consumer_secret: !secret overuplawmoor_consumer_secret - access_token: !secret overuplawmoor_access_token - access_token_secret: !secret overuplawmoor_access_token_secret - - platform: smtp - name: email_kyle - server: smtp.gmail.com - port: 587 - starttls: true - username: !secret gmail_username - password: !secret gmail_password - sender: homeassistant@glasgownet.com - recipient: kyle@glasgownet.com - - platform: alexa_media - name: alexa_media - - platform: syslog - name: syslog +--- +- platform: mastodon + name: mastodon_overuplawmoor + base_url: https://botsin.space/ + access_token: !secret mastodon_overuplawmoor_access_token + client_id: !secret mastodon_overuplawmoor_client_id + client_secret: !secret mastodon_overuplawmoor_client_secret +- platform: mastodon + name: mastodon_viewpoint + base_url: https://botsin.space/ + access_token: !secret mastodon_viewpoint_access_token + client_id: !secret mastodon_viewpoint_client_id + client_secret: !secret mastodon_viewpoint_client_secret +- platform: smtp + name: email_kyle + server: smtp.gmail.com + port: 587 + starttls: true + username: !secret gmail_username + password: !secret gmail_password + sender: homeassistant@glasgownet.com + recipient: kyle@glasgownet.com +- platform: alexa_media + name: alexa_media +- platform: syslog + name: syslog diff --git a/packages/adaptive_lighting.yaml b/packages/adaptive_lighting.yaml index 1f1ad1f8..d5359e2a 100644 --- a/packages/adaptive_lighting.yaml +++ b/packages/adaptive_lighting.yaml @@ -109,7 +109,7 @@ adaptive_lighting: - name: "Living Room adaptive lighting" lights: - light.living_room - - light.dining_nook_group + - light.dining_nook prefer_rgb_color: false transition: 45 # seconds initial_transition: 5 # seconds diff --git a/packages/alarm.yaml b/packages/alarm.yaml index 5fe24d7a..19438ffc 100644 --- a/packages/alarm.yaml +++ b/packages/alarm.yaml @@ -22,11 +22,11 @@ automation: data: volume_level: > {% if states('sensor.time_of_day') == "Morning" %} - 5 + 0.5 {% elif states('sensor.time_of_day') == "Day" %} - 5 + 0.5 {% elif states('sensor.time_of_day') == "Night" %} - 2 + 0.2 {% endif %} - service: notify.alexa_media data_template: diff --git a/packages/bin_reminder_tts.yaml b/packages/bin_reminder_tts.yaml index 07b0167d..b57dda58 100644 --- a/packages/bin_reminder_tts.yaml +++ b/packages/bin_reminder_tts.yaml @@ -34,7 +34,7 @@ automation: state: 'on' trigger: - platform: state - entity_id: binary_sensor.front_hall_door + entity_id: binary_sensor.front_hall_door_contact to: 'on' - platform: state entity_id: binary_sensor.front_hall_motion diff --git a/packages/boot_room.yaml b/packages/boot_room.yaml index 296af72f..f4958c6a 100644 --- a/packages/boot_room.yaml +++ b/packages/boot_room.yaml @@ -1,4 +1,13 @@ --- +input_number: + boot_room_light_level_trigger: + name: Boot room light level trigger + icon: mdi:sun-angle-outline + unit_of_measurement: lx + min: 0 + max: 10000 + step: 1 + automation: - alias: Boot Room light toggle # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml @@ -25,8 +34,9 @@ automation: initial_state: true trigger: - platform: state - entity_id: binary_sensor.boot_room_motion + entity_id: binary_sensor.boot_room_motion_occupancy to: 'on' + from: 'off' # Sensor attached to wrong door at the moment... # - platform: state # entity_id: binary_sensor.boot_room_door @@ -37,7 +47,7 @@ automation: state: "off" - condition: numeric_state entity_id: sensor.average_external_light_level - below: 1000 + below: input_number.boot_room_light_level_trigger - condition: state entity_id: binary_sensor.home_occupied state: "on" @@ -56,7 +66,9 @@ automation: trigger: - platform: numeric_state entity_id: sensor.average_external_light_level - above: 1000 + above: input_number.boot_room_light_level_trigger + for: + minutes: 10 action: - service: logbook.log data_template: @@ -71,8 +83,9 @@ automation: initial_state: true trigger: - platform: state - entity_id: binary_sensor.boot_room_motion + entity_id: binary_sensor.boot_room_motion_occupancy to: 'on' + from: 'off' # Sensor attached to wrong door at the moment... # - platform: state # entity_id: binary_sensor.boot_room_door @@ -101,7 +114,7 @@ automation: - alias: Boot room motion - 2 minute timeout trigger: - platform: state - entity_id: binary_sensor.boot_room_motion + entity_id: binary_sensor.boot_room_motion_occupancy to: 'off' for: minutes: 2 @@ -123,7 +136,7 @@ automation: - alias: Boot room motion - 10 minute timeout trigger: - platform: state - entity_id: binary_sensor.boot_room_motion + entity_id: binary_sensor.boot_room_motion_occupancy to: 'off' for: minutes: 10 diff --git a/packages/climate.yaml b/packages/climate.yaml index 2e0e4fa3..eb2af126 100644 --- a/packages/climate.yaml +++ b/packages/climate.yaml @@ -5,21 +5,14 @@ # Generic thermostat currently only supports 'heat', 'cool' and 'off' binary_sensor: - - platform: workday - name: Workday - country: GB - - platform: workday - name: Workday tomorrow - country: GB - days_offset: 1 - platform: group # Openings that should prevent the heating from coming on name: Climate Openings device_class: opening entities: - - binary_sensor.back_door - - binary_sensor.front_door - - binary_sensor.patio_door + # - binary_sensor.back_door + # - binary_sensor.front_door + # - binary_sensor.patio_door - binary_sensor.ensuite_window - binary_sensor.master_bedroom_window - binary_sensor.guest_bedroom_window @@ -28,7 +21,7 @@ binary_sensor: - binary_sensor.bathroom_window - binary_sensor.dining_table_window - binary_sensor.kitchen_window - - binary_sensor.utility_room_window + # - binary_sensor.utility_room_window group: call_for_heat: @@ -98,11 +91,7 @@ automation: action: - service: climate.set_preset_mode data: - entity_id: climate.house - preset_mode: "away" - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water + entity_id: all preset_mode: "away" - service: logbook.log data_template: @@ -123,10 +112,6 @@ automation: data: entity_id: climate.house preset_mode: "none" - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water - preset_mode: "none" - service: logbook.log data_template: name: EVENT @@ -141,11 +126,7 @@ automation: action: - service: climate.set_preset_mode data: - entity_id: climate.house - preset_mode: "none" - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water + entity_id: all preset_mode: "none" - service: logbook.log data_template: @@ -170,10 +151,6 @@ automation: data: entity_id: climate.house temperature: 17 - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water - preset_mode: "away" - service: logbook.log data_template: name: EVENT @@ -196,10 +173,6 @@ automation: data: entity_id: climate.house temperature: 17 - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water - preset_mode: "Away" - service: logbook.log data_template: name: EVENT @@ -222,10 +195,6 @@ automation: data: entity_id: climate.house temperature: 20 - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water - preset_mode: "none" - service: logbook.log data_template: name: EVENT @@ -248,10 +217,6 @@ automation: data: entity_id: climate.house temperature: 20 - - service: climate.set_preset_mode - data: - entity_id: climate.hot_water - preset_mode: "none" - service: logbook.log data_template: name: EVENT diff --git a/packages/craft_room.yaml b/packages/craft_room.yaml new file mode 100644 index 00000000..29836fe2 --- /dev/null +++ b/packages/craft_room.yaml @@ -0,0 +1,34 @@ +--- +automation: + - alias: Craft Room light toggle + trigger: + - platform: event + event_type: esphome.button_pressed + event_data: + device_name: craftroom_switch + click_type: single + action: + - service: switch.turn_on + entity_id: switch.craft_room_switch_relay + - service: light.toggle + entity_id: group.craft_room_lighting + - service: logbook.log + data_template: + name: EVENT + message: "Toggling craft room light" + + - alias: Craft room fairy lights toggle + # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml + trigger: + - platform: event + event_type: esphome.button_pressed + event_data: + device_name: craftroom_switch + click_type: double + action: + - service: light.toggle + entity_id: light.craft_room_fairy_lights + - service: logbook.log + data_template: + name: EVENT + message: "Toggling fairy light" diff --git a/packages/device_alerts.yaml b/packages/device_alerts.yaml index de9378fd..f4efe05a 100644 --- a/packages/device_alerts.yaml +++ b/packages/device_alerts.yaml @@ -1,3 +1,4 @@ +--- automation: - alias: Alert when a critical device goes offline trigger: @@ -6,24 +7,25 @@ automation: - sensor.kitchen_temperature - binary_sensor.front_hall_sensor_status - sensor.garage_temperature - - binary_sensor.hall_door_motion - - binary_sensor.hall_rooms_motion - - binary_sensor.ensuite_motion - - binary_sensor.living_room_motion - - binary_sensor.kitchen_motion - - binary_sensor.front_hall_motion + - binary_sensor.hallway_bathroom_occupancy + - binary_sensor.hallway_rooms_occupancy + - binary_sensor.ensuite_motion_occupancy + - binary_sensor.living_room_motion_occupancy + - binary_sensor.kitchen_motion_occupancy + - binary_sensor.front_hall_motion_occupancy - binary_sensor.front_door_motion - binary_sensor.washing_machine_power_plug_status - binary_sensor.dishwasher_power_plug_status - binary_sensor.shower_pump_power_plug_status - binary_sensor.tv_power_plug_status - binary_sensor.craft_room_window - - binary_sensor.ensuite_window + - binary_sensor.ensuite_window_vibration + - binary_sensor.ensuite_window_opening_contact - binary_sensor.guest_bedroom_window - binary_sensor.master_bedroom_window - binary_sensor.study_window - binary_sensor.garden_door - - binary_sensor.boot_room_motion + - binary_sensor.boot_room_motion_occupancy - binary_sensor.boot_room_door - binary_sensor.bathroom_window - binary_sensor.dining_table_window @@ -56,7 +58,7 @@ automation: - sensor.living_room_multi_sensor_battery_level - sensor.master_bedroom_multi_sensor_battery_level - sensor.study_multi_sensor_battery_level - - sensor.hall_multi_sensor_battery_level + - sensor.hallway_multi_sensor_battery_level - sensor.dining_nook_multi_sensor_battery_level - sensor.bathroom_multi_sensor_battery_level to: 'unavailable' diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index 027bcee4..eb9b6ee4 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -1,4 +1,18 @@ --- +binary_sensor: + - platform: template + sensors: + ensuite_window: + friendly_name: Ensuite Window + device_class: window + icon_template: >- + {% if is_state('binary_sensor.ensuite_window','on') %} + mdi:window-open + {% else %} + mdi:window-closed + {% endif %} + value_template: "{{ is_state('binary_sensor.ensuite_window_opening_contact','on') or is_state('binary_sensor.ensuite_window_vibration','on') }}" + automation: - alias: Ensuite light toggle # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml @@ -30,8 +44,9 @@ automation: - alias: Ensuite motion trigger: - platform: state - entity_id: binary_sensor.ensuite_motion + entity_id: binary_sensor.ensuite_motion_occupancy to: 'on' + from: 'off' action: - service: light.turn_on data_template: @@ -56,11 +71,11 @@ automation: trigger: - platform: numeric_state entity_id: sensor.shower_pump_power_plug_power - above: 10 + above: 100 condition: - condition: numeric_state entity_id: sensor.average_external_light_level - below: 1000 + above: 500 action: - service: logbook.log data_template: @@ -89,13 +104,13 @@ automation: # entity_id: climate.hot_water # preset_mode: 'boost' - service: notify.alexa_media - data: + data_template: target: - media_player.bedroom data: type: announce # method: all - message: "Hello! Enjoy your shower!" + message: Hello! Enjoy your shower! The hot tank is currently {{ states('sensor.hot_tank_temperature') }} degrees. - alias: Ensuite shower off description: Return the heating boost to eco mode after the shower is finished @@ -120,7 +135,7 @@ automation: - alias: Ensuite motion - night timeout trigger: - platform: state - entity_id: binary_sensor.ensuite_motion + entity_id: binary_sensor.ensuite_motion_occupancy to: 'off' for: minutes: 2 @@ -138,7 +153,7 @@ automation: - alias: Ensuite motion - regular timeout trigger: - platform: state - entity_id: binary_sensor.ensuite_motion + entity_id: binary_sensor.ensuite_motion_occupancy to: 'off' for: minutes: 10 diff --git a/packages/front_hall.yaml b/packages/front_hall.yaml index 2d3645fa..9e61c6d9 100644 --- a/packages/front_hall.yaml +++ b/packages/front_hall.yaml @@ -1,17 +1,27 @@ --- +input_number: + front_hall_light_level_trigger: + name: Front hall light level trigger + icon: mdi:sun-angle-outline + unit_of_measurement: lx + min: 0 + max: 10000 + step: 1 + automation: - - alias: Front hall light on + - alias: Front hall motion + id: front_hall_motion trigger: - platform: state entity_id: - - binary_sensor.front_hall_door - - binary_sensor.front_hall_motion + - binary_sensor.front_hall_door_contact + - binary_sensor.front_hall_motion_occupancy to: 'on' from: 'off' condition: - condition: numeric_state entity_id: sensor.average_external_light_level - below: 1000 + below: input_number.front_hall_light_level_trigger - condition: state entity_id: binary_sensor.home_occupied state: "on" @@ -28,9 +38,10 @@ automation: - light.front_hall - alias: Front hall - 2 minute timeout + id: front_hall_motion_2_minute trigger: - platform: state - entity_id: binary_sensor.front_hall_motion + entity_id: binary_sensor.front_hall_motion_occupancy to: 'off' for: minutes: 2 @@ -49,9 +60,10 @@ automation: entity_id: light.front_hall - alias: Front hall - 10 minute timeout + id: front_hall_motion_10_minute trigger: - platform: state - entity_id: binary_sensor.front_hall_motion + entity_id: binary_sensor.front_hall_motion_occupancy to: 'off' for: minutes: 10 diff --git a/packages/garage.yaml b/packages/garage.yaml index c2190f96..002f8ab3 100644 --- a/packages/garage.yaml +++ b/packages/garage.yaml @@ -4,8 +4,9 @@ automation: initial_state: true trigger: - platform: state - entity_id: binary_sensor.garage_motion + entity_id: binary_sensor.garage_motion_occupancy to: 'on' + from: 'off' action: - service: light.turn_on entity_id: light.garage_lights @@ -13,7 +14,7 @@ automation: - alias: Garage motion - 10 minute timeout trigger: - platform: state - entity_id: binary_sensor.garage_motion + entity_id: binary_sensor.garage_motion_occupancy to: 'off' for: minutes: 10 diff --git a/packages/goodnight.yaml b/packages/goodnight.yaml index f1e602fc..89aa54b3 100644 --- a/packages/goodnight.yaml +++ b/packages/goodnight.yaml @@ -30,7 +30,7 @@ automation: - service: homeassistant.turn_off entity_id: - light.living_room - - light.dining_nook_group + - light.dining_nook - light.art - group.outside_lights - group.front_hall diff --git a/packages/guest_bedroom.yaml b/packages/guest_bedroom.yaml new file mode 100644 index 00000000..f430a2d1 --- /dev/null +++ b/packages/guest_bedroom.yaml @@ -0,0 +1,29 @@ +--- +# The input_boolean.call_for_guest_heat is provided at the upper level climate +# package, in /packages/climate.yaml + +automation: + - alias: 'Guest bedroom call for heat - on' + trigger: + - platform: template + value_template: "{{ state_attr('climate.guest_bedroom','hvac_action') == 'heating' }}" + - platform: template + value_template: "{{ (states.climate.guest_bedroom.attributes.current_temperature | float) < (states.climate.guest_bedroom.attributes.temperature | float) }}" + condition: + - condition: template + value_template: "{{ states.climate.guest_bedroom.state == 'heat' }}" + action: + - service: homeassistant.turn_on + entity_id: input_boolean.call_for_guest_heat + + - alias: 'Guest bedroom call for heat - off' + trigger: + - platform: template + value_template: "{{ state_attr('climate.guest_bedroom','hvac_action') != 'heating' }}" + - platform: template + value_template: "{{ (states.climate.guest_bedroom.attributes.current_temperature | float) >= (states.climate.guest_bedroom.attributes.temperature | float) }}" + - platform: template + value_template: "{{ states.climate.guest_bedroom.state != heat }}" + action: + - service: homeassistant.turn_off + entity_id: input_boolean.call_for_guest_heat diff --git a/packages/hallway.yaml b/packages/hallway.yaml index 855a86ec..b93480a4 100644 --- a/packages/hallway.yaml +++ b/packages/hallway.yaml @@ -1,4 +1,13 @@ --- +input_number: + hallway_light_level_trigger: + name: Hallway light level trigger + icon: mdi:sun-angle-outline + unit_of_measurement: lx + min: 0 + max: 10000 + step: 1 + automation: - alias: Hallway light toggle # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml @@ -31,13 +40,14 @@ automation: trigger: - platform: state entity_id: - - binary_sensor.hall_door_motion - - binary_sensor.hall_rooms_motion + - binary_sensor.hallway_rooms_occupancy + - binary_sensor.hallway_bathroom_occupancy to: 'on' + from: 'off' condition: - condition: numeric_state entity_id: sensor.average_external_light_level - below: 1000 + below: input_number.hallway_light_level_trigger - condition: state entity_id: binary_sensor.home_occupied state: "on" @@ -55,8 +65,8 @@ automation: trigger: - platform: state entity_id: - - binary_sensor.hall_door_motion - - binary_sensor.hall_rooms_motion + - binary_sensor.hallway_rooms_occupancy + - binary_sensor.hallway_bathroom_occupancy to: 'off' for: minutes: 2 @@ -77,8 +87,8 @@ automation: trigger: - platform: state entity_id: - - binary_sensor.hall_door_motion - - binary_sensor.hall_rooms_motion + - binary_sensor.hallway_rooms_occupancy + - binary_sensor.hallway_bathroom_occupancy to: 'off' for: minutes: 10 @@ -90,3 +100,20 @@ automation: entity_id: light.hall - service: light.turn_off entity_id: light.hall + + - alias: Hallway - sunlight timeout + trigger: + - platform: numeric_state + entity_id: sensor.average_external_light_level + above: input_number.hallway_light_level_trigger + for: + minutes: 10 + action: + - service: logbook.log + data_template: + name: EVENT + message: "Turning hallway lights off" + entity_id: light.hall + domain: light + - service: homeassistant.turn_off + entity_id: light.hall diff --git a/packages/kitchen.yaml b/packages/kitchen.yaml index 40e0755c..2b9f0f33 100644 --- a/packages/kitchen.yaml +++ b/packages/kitchen.yaml @@ -12,8 +12,9 @@ automation: - alias: Kitchen motion trigger: platform: state - entity_id: binary_sensor.kitchen_motion + entity_id: binary_sensor.kitchen_motion_occupancy to: 'on' + from: 'off' condition: - condition: numeric_state entity_id: sensor.average_external_light_level @@ -30,7 +31,7 @@ automation: - alias: Kitchen motion - 10 minute timeout trigger: platform: state - entity_id: binary_sensor.kitchen_motion + entity_id: binary_sensor.kitchen_motion_occupancy to: 'off' for: minutes: 10 diff --git a/packages/living_room_blinds.yaml b/packages/living_room_blinds.yaml index 4ad9ec7d..1d14d659 100644 --- a/packages/living_room_blinds.yaml +++ b/packages/living_room_blinds.yaml @@ -27,9 +27,9 @@ automation: - alias: Control Blinds trigger: - platform: mqtt - topic: zigbee2mqtt/zigbee2mqtt/Blinds Remote 2/action + topic: "zigbee2mqtt/Blind Remote 2/action" - platform: mqtt - topic: "zigbee2mqtt/zigbee2mqtt/Blinds Remote 2/action" + topic: "zigbee2mqtt/Blind Remote 1/action" action: - variables: diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index 58a6c18a..23fe6d1f 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -8,7 +8,7 @@ homeassistant: light.living_room: icon: mdi:lightbulb-group - light.dining_nook_group: + light.dining_nook: icon: mdi:lightbulb-group input_number: @@ -27,18 +27,9 @@ input_number: max: 10000 step: 1 -light: - - platform: group - name: Dining Nook Group - unique_id: dining_nook_group - entities: - - light.art - - light.sideboard - - light.dining_table - - light.train_cabinets - automation: - alias: Living room light toggle + id: living_room_light_toggle # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml trigger: - platform: event @@ -60,6 +51,7 @@ automation: message: "Toggling living room lights" - alias: Dining nook light toggle + id: dining_nook_light_toggle # Can be improved, examples at https://github.com/TheFes/HA-configuration/blob/main/include/automation/01_first_floor/floris/shelly_floris.yaml trigger: - platform: event @@ -74,45 +66,54 @@ automation: - service: light.toggle data_template: entity_id: - - light.dining_nook_group + - light.dining_nook - service: logbook.log data_template: name: EVENT message: "Toggling dining nook lights" - alias: Living Room motion - motion timeout + id: living_room_motion_timeout trigger: - platform: state - entity_id: binary_sensor.living_room_motion + entity_id: binary_sensor.living_room_motion_occupancy to: 'off' for: minutes: 60 action: service: homeassistant.turn_off - entity_id: light.living_room, light.dining_nook_group + entity_id: light.living_room, light.dining_nook - alias: Living Room motion - sunlight timeout trigger: - platform: numeric_state entity_id: sensor.average_external_light_level - below: input_number.living_room_light_level_trigger + above: input_number.living_room_light_level_trigger for: minutes: 10 action: service: homeassistant.turn_off - entity_id: light.living_room, light.dining_nook_group + entity_id: light.living_room, light.dining_nook - alias: Living Room motion + id: living_room_motion trigger: - platform: state - entity_id: binary_sensor.living_room_motion + entity_id: binary_sensor.living_room_motion_occupancy to: 'on' + from: 'off' condition: condition: and conditions: - condition: state entity_id: input_boolean.night_view state: "off" + - condition: state + entity_id: light.living_room + state: "off" + - condition: state + entity_id: light.dining_nook + state: "off" - condition: numeric_state entity_id: sensor.average_external_light_level below: input_number.living_room_light_level_trigger @@ -140,7 +141,7 @@ automation: data_template: entity_id: > {% if states('sensor.time_of_day') == "Morning" %} - light.living_room, light.dining_nook_group + light.living_room, light.dining_nook {% else %} - light.living_room, light.dining_nook_group + light.living_room, light.dining_nook {% endif %} diff --git a/packages/master_bathroom.yaml b/packages/master_bathroom.yaml index f6171ea5..5fcbfe22 100644 --- a/packages/master_bathroom.yaml +++ b/packages/master_bathroom.yaml @@ -11,7 +11,7 @@ binary_sensor: {% else %} mdi:window-closed {% endif %} - value_template: "{{ is_state('binary_sensor.bathroom_window_opening_sensor','on') or is_state('binary_sensor.bathroom_window_vibration','on') }}" + value_template: "{{ is_state('binary_sensor.bathroom_window_contact','on') or is_state('binary_sensor.bathroom_window_vibration','on') }}" light: - platform: group diff --git a/packages/nzbget.yaml b/packages/nzbget.yaml index f7e4ffd5..b9ea8c60 100644 --- a/packages/nzbget.yaml +++ b/packages/nzbget.yaml @@ -1,26 +1,5 @@ +--- automation: - - alias: Pause nzbget when occupied - initial_state: false - trigger: - - platform: state - entity_id: binary_sensor.home_occupied - from: "off" - to: "on" - action: - - service: nzbget.pause - - - alias: Resume nzbget when unoccupied - initial_state: false - trigger: - - platform: state - entity_id: binary_sensor.home_occupied - from: "on" - to: "off" - for: - minutes: 5 - action: - - service: nzbget.resume - - alias: Completed Download trigger: platform: event @@ -37,7 +16,6 @@ automation: message: "{{trigger.event.data.name}}" level: info - - alias: Completed Star Trek Download trigger: platform: event @@ -46,7 +24,7 @@ automation: category: tv condition: condition: template - value_template: '{% if trigger.event.data.name | regex_search("star.trek", ignorecase=True) %}True{% endif %}' + value_template: '{% if trigger.event.data.name | regex_search("star.trek", ignorecase=True) %}True{% endif %}' # yamllint disable-line rule:line-length action: service: notify.email_kyle data_template: @@ -61,7 +39,7 @@ automation: category: tv condition: condition: template - value_template: '{% if trigger.event.data.name | regex_search("westworld", ignorecase=True) %}True{% endif %}' + value_template: '{% if trigger.event.data.name | regex_search("westworld", ignorecase=True) %}True{% endif %}' # yamllint disable-line rule:line-length action: - service: notify.email_kyle data_template: @@ -74,4 +52,4 @@ automation: data: type: announce # method: all - message: "Hello! You have a new show to watch. Westworld has finished downloading!" + message: "Hello! You have a new show to watch. Westworld has finished downloading!" # yamllint disable-line rule:line-length diff --git a/packages/outside_motion.yaml b/packages/outside_motion.yaml index b499904e..a7580d45 100644 --- a/packages/outside_motion.yaml +++ b/packages/outside_motion.yaml @@ -1,16 +1,16 @@ --- automation: - alias: Outdoor lights on + id: outdoor_lights_on trigger: - platform: state entity_id: - - binary_sensor.outside_front_motion - - binary_sensor.outside_driveway_motion - - binary_sensor.front_door_motion - - binary_sensor.back_door_person_occupancy - - binary_sensor.driveway_person_occupancy + - binary_sensor.back_door_pir + - binary_sensor.driveway_pir - binary_sensor.driveway_person_occupancy + - binary_sensor.back_door_person_occupancy - binary_sensor.gates_person_occupancy + - binary_sensor.front_door_person_occupancy to: 'on' condition: - condition: state @@ -52,11 +52,12 @@ automation: entity_id: light.front_door_floodlights - alias: Outdoor lights off + id: outdoor_lights_off trigger: - platform: state entity_id: - - binary_sensor.outside_front_motion - - binary_sensor.outside_driveway_motion + - binary_sensor.back_door_pir + - binary_sensor.driveway_pir - binary_sensor.front_door_motion - binary_sensor.back_door_person_occupancy - binary_sensor.driveway_person_occupancy diff --git a/packages/overflights.yaml b/packages/overflights.yaml index aebdb74b..6df5603d 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -1,14 +1,10 @@ -sensor: - - platform: opensky - name: opensky_home - radius: 5 - - - platform: opensky - name: opensky_uplawmoor - radius: 5 - altitude: 5000 - latitude: 55.765 - longitude: -4.496 +--- +mqtt: + sensor: + - name: flightradar_home + state_topic: 'flightradar/viewpoint' + # just an example atm + value_template: "{{ value_json.id if value_json.lat == 55 else states('sensor.outdoor_temperature') }}" automation: - alias: 'Flight entry notification - Home' @@ -41,7 +37,8 @@ automation: message: 'Flight {{ trigger.event.data.callsign }} is passing near Uplawmoor at {{ trigger.event.data.altitude }} meters. https://flightaware.com/live/flight/{{ trigger.event.data.callsign }}' - - service: notify.twitter_overuplawmoor + + - service: notify.mastodon_overuplawmoor data_template: message: 'Flight {{ trigger.event.data.callsign }} is passing near Uplawmoor at {{ trigger.event.data.altitude }} meters. diff --git a/packages/pihole.yaml b/packages/pihole.yaml index 0346c8f9..45105945 100644 --- a/packages/pihole.yaml +++ b/packages/pihole.yaml @@ -53,7 +53,8 @@ automation: {{ [ "I blocked {{states.sensor.pi_hole_ads_blocked_today.state}} ads. That is {{states.sensor.pi_hole_ads_percentage_blocked_today.state}}% of my internet traffic.", "Today was a good day! Why, you ask? Because I blocked {{states.sensor.pi_hole_ads_blocked_today.state}} ads via Pi-Hole!", - ] | random + " #PiHole #Security Status:({{states.sensor.server_pihole.state}}) https://amzn.to/2CTOzFi"}} + "After a hard days work, I've blocked {{states.sensor.pi_hole_ads_blocked_today.state}} today!", + ] | random + " #PiHole #Security Status:({{states.binary_sensor.pi_hole.state}}) https://amzn.to/2CTOzFi"}} image: >- {{ [ "/config/www/custom_ui/images/pihole/Vortex-R.png", diff --git a/packages/radio_billy_country.yaml b/packages/radio_billy_country.yaml new file mode 100644 index 00000000..68b1a111 --- /dev/null +++ b/packages/radio_billy_country.yaml @@ -0,0 +1,22 @@ +--- +input_boolean: + radio_billy_country: + name: Billy Country + initial: false + +automation: + - alias: "Play Billy Country" + id: play_radio_billy_country + trigger: + platform: state + entity_id: input_boolean.billy_country + from: 'off' + to: 'on' + action: + - service: media_player.play_media + data: + entity_id: media_player.openhome_uuid_4c494e4e_0026_0f21_a10a_01260864013f + media_content_id: "airable.radios://radio?version=1&radioId=8375321727246047&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" + media_content_type: music + - service: homeassistant.turn_off + entity_id: input_boolean.billy_country diff --git a/packages/radio_caroline.yaml b/packages/radio_caroline.yaml new file mode 100644 index 00000000..ca427582 --- /dev/null +++ b/packages/radio_caroline.yaml @@ -0,0 +1,22 @@ +--- +input_boolean: + radio_caroline: + name: Radio Caroline + initial: false + +automation: + - alias: "Play Radio Caroline" + id: play_radio_caroline + trigger: + platform: state + entity_id: input_boolean.radio_caroline + from: 'off' + to: 'on' + action: + - service: media_player.play_media + data: + entity_id: media_player.openhome_uuid_4c494e4e_0026_0f21_a10a_01260864013f + media_content_id: "airable.radios://radio?version=1&radioId=2139736827462911&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" + media_content_type: music + - service: homeassistant.turn_off + entity_id: input_boolean.radio_caroline diff --git a/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml new file mode 100644 index 00000000..67d3b8ea --- /dev/null +++ b/packages/radio_classic_fm.yaml @@ -0,0 +1,26 @@ +--- +input_boolean: + radio_classic_fm: + name: Radio Classic FM + initial: false + +automation: + - alias: "Play Classic FM" + id: play_radio_classic_fm + trigger: + platform: state + entity_id: input_boolean.radio_classic_fm + from: 'off' + to: 'on' + action: + - service: homeassistant.turn_on + entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f + - delay: + seconds: 15 + - service: media_player.play_media + data: + entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f + media_content_id: "airable.radios://radio?version=1&radioId=4888751676771790&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" + media_content_type: music + - service: homeassistant.turn_off + entity_id: input_boolean.radio_classic_fm diff --git a/packages/room_aware.yaml b/packages/room_aware.yaml index 4e1571cd..decba84f 100644 --- a/packages/room_aware.yaml +++ b/packages/room_aware.yaml @@ -12,11 +12,13 @@ group: template: - sensor: - - name: last_alexa - state: > - {{ expand('group.echos') | selectattr('attributes.last_called','eq',True) | map(attribute='entity_id') | first }} - availability: > - {{ expand('group.echos') | selectattr('attributes.last_called','eq',True) | first is defined }} + - name: Last Alexa + state: |- + {{ expand(integration_entities('alexa_media') | select('search', 'media_player')) + | selectattr('attributes.last_called', 'eq', True) | map(attribute='entity_id') | first }} + availability: |- + {{ expand(integration_entities('alexa_media') | select('search', 'media_player')) + | selectattr('attributes.last_called','eq',True) | first is defined }} # "Alexa, turn on the lights" script: diff --git a/persons.yaml b/persons.yaml index 09ccf4fb..ad189218 100644 --- a/persons.yaml +++ b/persons.yaml @@ -1,13 +1,13 @@ - name: Kyle id: Kyle001 device_trackers: - - device_tracker.kyle_phone + # - device_tracker.kyle_phone - device_tracker.nothing_phone - name: Charlotte id: Charlotte001 device_trackers: - - device_tracker.charlotte_phone + - device_tracker.mine - name: Ronnie id: Ronnie0001 diff --git a/scripts/hue_reset.yaml b/scripts/hue_reset.yaml new file mode 100644 index 00000000..cf74edbc --- /dev/null +++ b/scripts/hue_reset.yaml @@ -0,0 +1,35 @@ +--- +reset_hue: + alias: reset_hue + sequence: + - service: switch.turn_off + target: + entity_id: switch.candle_arch + - delay: + hours: 0 + minutes: 0 + seconds: 5 + milliseconds: 0 + - repeat: + count: '2' + sequence: + - service: switch.turn_on + target: + entity_id: switch.candle_arch + - delay: + hours: 0 + minutes: 0 + seconds: 8 + milliseconds: 0 + - service: switch.turn_off + target: + entity_id: switch.candle_arch + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - service: switch.turn_on + target: + entity_id: switch.candle_arch + mode: single diff --git a/scripts/ikea_reset.yaml b/scripts/ikea_reset.yaml new file mode 100644 index 00000000..87c876b8 --- /dev/null +++ b/scripts/ikea_reset.yaml @@ -0,0 +1,35 @@ +--- +reset_ikea: + alias: reset_ikea + sequence: + - service: switch.turn_off + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 5 + milliseconds: 0 + - repeat: + count: '5' + sequence: + - service: switch.turn_on + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 0 + milliseconds: 400 + - service: switch.turn_off + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 1 + milliseconds: 500 + - service: switch.turn_on + target: + entity_id: switch.boot_room_switch_relay + mode: single diff --git a/scripts/silvercrest_reset.yaml b/scripts/silvercrest_reset.yaml new file mode 100644 index 00000000..ae3d3bfb --- /dev/null +++ b/scripts/silvercrest_reset.yaml @@ -0,0 +1,35 @@ +--- +reset_silvercrest: + alias: reset_silvercrest + sequence: + - service: switch.turn_off + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 10 + milliseconds: 0 + - repeat: + count: '2' + sequence: + - service: switch.turn_on + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - service: switch.turn_off + target: + entity_id: switch.boot_room_switch_relay + - delay: + hours: 0 + minutes: 0 + seconds: 2 + milliseconds: 0 + - service: switch.turn_on + target: + entity_id: switch.boot_room_switch_relay + mode: single diff --git a/scripts/tweet_engine.yaml b/scripts/tweet_engine.yaml index 398857a8..b37f0fba 100644 --- a/scripts/tweet_engine.yaml +++ b/scripts/tweet_engine.yaml @@ -1,9 +1,10 @@ +--- ###################################################################################################### -### Script to send notifications to Twitter as @thegordonhome. +### Script to send notifications to Mastodon as @viewpoint@botsin.space ###################################################################################################### # sequence: -# - service: notify.twitter_thegordonhome +# - service: notify.mastodon_viewpoint # data_template: # message: >- # {{ tweet }} #IOT #SmartHome @@ -11,7 +12,7 @@ tweet_engine_image: sequence: - - service: notify.twitter_thegordonhome + - service: notify.mastodon_viewpoint data_template: message: >- {{ tweet }} #IOT #SmartHome @@ -21,7 +22,7 @@ tweet_engine_image: tweet_engine: sequence: - - service: notify.twitter_thegordonhome + - service: notify.mastodon_viewpoint data_template: message: >- {{ tweet }} #IOT #SmartHome diff --git a/sensors.yaml b/sensors.yaml index 2d035cb1..7f64b49e 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -56,7 +56,7 @@ unique_id: input_select.charlotte_status_dropdown - platform: integration - source: sensor.electricity_energy_usage + source: sensor.elec2_cm119_160_db_92_instantaneous_power name: energy_spent unit_prefix: k round: 2 @@ -65,6 +65,11 @@ name: energy_spent_tv unit_prefix: k round: 2 +- platform: integration + source: sensor.shower_power_plug_power + name: energy_spent_shower + unit_prefix: k + round: 2 - platform: min_max name: Average External Light Level @@ -79,28 +84,27 @@ type: mean entity_ids: - sensor.kitchen_temperature - - sensor.craft_room_temperature - - sensor.guest_room_temperature - - sensor.living_room_temperature - - sensor.master_bedroom_temperature - - sensor.nook_temperature - - sensor.hall_temperature - - sensor.study_temperature - - sensor.bathroom_temperature + - sensor.craft_room_multi_sensor_temperature + - sensor.living_room_multi_sensor_temperature + - sensor.master_bedroom_multi_sensor_temperature + - sensor.nook_multi_sensor_temperature + - sensor.hallway_multi_sensor_temperature + - sensor.study_multi_sensor_temperature + - sensor.bathroom_multi_sensor_temperature - platform: min_max name: House Average Humidity type: median entity_ids: - - sensor.front_hall_sensor_humidity - - sensor.craft_room_humidity - - sensor.guest_room_humidity - - sensor.living_room_humidity - - sensor.master_bedroom_humidity - - sensor.nook_humidity - - sensor.hall_humidity - - sensor.study_humidity - - sensor.bathroom_humidity + - sensor.front_hall_multi_sensor_humidity + - sensor.craft_room_multi_sensor_humidity + - sensor.guest_room_multi_sensor_humidity + - sensor.living_room_multi_sensor_humidity + - sensor.master_bedroom_multi_sensor_humidity + - sensor.nook_multi_sensor_humidity + - sensor.hallway_multi_sensor_humidity + - sensor.study_multi_sensor_humidity + - sensor.bathroom_multi_sensor_humidity - platform: time_date display_options: @@ -140,37 +144,46 @@ sensors: living_room: friendly_name: Living Room - temperature_sensor: sensor.living_room_temperature - humidity_sensor: sensor.living_room_humidity + unique_id: living_room_thermal_comfort + temperature_sensor: sensor.living_room_multi_sensor_temperature + humidity_sensor: sensor.living_room_multi_sensor_humidity dining_nook: friendly_name: Dining Nook - temperature_sensor: sensor.nook_temperature - humidity_sensor: sensor.nook_humidity + unique_id: dining_nook_thermal_comfort + temperature_sensor: sensor.nook_multi_sensor_temperature + humidity_sensor: sensor.nook_multi_sensor_humidity hall: friendly_name: Hall - temperature_sensor: sensor.hall_temperature - humidity_sensor: sensor.hall_humidity + unique_id: hall_thermal_comfort + temperature_sensor: sensor.hall_multi_sensor_temperature + humidity_sensor: sensor.hall_multi_sensor_humidity bathroom: friendly_name: Bathroom - temperature_sensor: sensor.bathroom_temperature - humidity_sensor: sensor.bathroom_humidity + unique_id: bathroom_thermal_comfort + temperature_sensor: sensor.bathroom_multi_sensor_temperature + humidity_sensor: sensor.bathroom_multi_sensor_humidity front_hall: friendly_name: Front Hall - temperature_sensor: sensor.front_hall_sensor_temperature - humidity_sensor: sensor.front_hall_sensor_humidity + unique_id: front_hall_thermal_comfort + temperature_sensor: sensor.front_hall_multi_sensor_temperature + humidity_sensor: sensor.front_hall_multi_sensor_humidity craft_room: friendly_name: Craft Room - temperature_sensor: sensor.craft_room_temperature - humidity_sensor: sensor.craft_room_humidity + unique_id: craft_room_thermal_comfort + temperature_sensor: sensor.craft_room_multi_sensor_temperature + humidity_sensor: sensor.craft_room_multi_sensor_humidity study: friendly_name: Study - temperature_sensor: sensor.study_temperature - humidity_sensor: sensor.study_humidity + unique_id: study_thermal_comfort + temperature_sensor: sensor.study_multi_sensor_temperature + humidity_sensor: sensor.study_multi_sensor_humidity guest_room: friendly_name: Guest Room - temperature_sensor: sensor.guest_room_temperature - humidity_sensor: sensor.guest_room_humidity + unique_id: guest_room_thermal_comfort + temperature_sensor: sensor.guest_room_multi_sensor_temperature + humidity_sensor: sensor.guest_room_multi_sensor_humidity master_bedroom: friendly_name: Master Bedroom + unique_id: master_bedroom_thermal_comfort temperature_sensor: sensor.master_bedroom_temperature humidity_sensor: sensor.master_bedroom_humidity diff --git a/travis_secrets.yaml b/travis_secrets.yaml index 3d661779..901621a2 100644 --- a/travis_secrets.yaml +++ b/travis_secrets.yaml @@ -10,10 +10,6 @@ kyle_work_latitude: 0.0000 kyle_work_longitude: 0.0000 openweathermap_api_key: secret_openweathermap_key snmp_community: public -twitter_consumer_key: consumer_key -twitter_consumer_secret: consumer_secret -twitter_access_token: access_token -twitter_access_token_secret: access_token_secret alpha_vantage_key: alpha_vantage_key btc_wallet_1: 3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r btc_wallet_2: 16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe @@ -41,3 +37,9 @@ influxdb_organization: organization_id influxdb_bucket: bucket_name influxdb_token: influxdb_token alarm_code: 1234 +mastodon_overuplawmoor_access_token: access_token +mastodon_overuplawmoor_client_id: client_id +mastodon_overuplawmoor_client_secret: client_secret +mastodon_viewpoint_access_token: access_token +mastodon_viewpoint_client_id: client_id +mastodon_viewpoint_client_secret: client_secret diff --git a/www/battery-state-card.js b/www/battery-state-card.js index dc3dbfac..58baf17d 100644 --- a/www/battery-state-card.js +++ b/www/battery-state-card.js @@ -1,2 +1,44 @@ -!function(){"use strict";var t=t||Object.getPrototypeOf(customElements.get("home-assistant-main"));const{html:e,css:i}=t.prototype;console.info("%c BATTERY-STATE-CARD %c 1.6.4","color: white; background: forestgreen; font-weight: 700;","color: forestgreen; background: white; font-weight: 700;");const n=(t,e="warn")=>{console[e]("[battery-state-card] "+t)},r=t=>(t=t.replace("#",""),{r:parseInt(t.substr(0,2),16),g:parseInt(t.substr(2,2),16),b:parseInt(t.substr(4,2),16)}),s=t=>!isNaN(Number(t)),o=t=>Array.isArray(t)?t:t?[t]:[],a=(t,e)=>{switch(typeof t){case"string":const i={};return i[e]=t,i;case"object":return Object.assign({},t)}return t},c=t=>t&&e`
${t}
`,l=(t,i)=>t&&e`
`,h=t=>e`
${l(t.icon,t.levelColor)}
${t.name} ${c(t.secondary_info)}
${t.level}${s(t.level)?e` %`:""}
`,d=(t,i)=>{return e`${t?(n=t,e`
${n}
`):""}
${i}
`;var n},u=i`.clickable{cursor:pointer}.truncate{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.entity-spacing{margin:8px 0}.entity-spacing:first-child{margin-top:0}.entity-spacing:last-child{margin-bottom:0}.entity-row{display:flex;align-items:center}.entity-row .name{flex:1;margin:0 6px}.entity-row .secondary{color:var(--secondary-text-color)}.entity-row .icon{flex:0 0 40px;border-radius:50%;text-align:center;line-height:40px;margin-right:10px}.expandWrapper>.toggler{cursor:pointer}.expandWrapper>.toggler>.name{flex:1}.expandWrapper>.toggler div.chevron{transform:rotate(-90deg);font-size:26px;height:40px;width:40px;display:flex;justify-content:center;align-items:center}.expandWrapper>.toggler .chevron,.expandWrapper>.toggler+div{transition:all .5s ease}.expandWrapper>.toggler.expanded .chevron{transform:rotate(-90deg) scaleX(-1)}.expandWrapper>.toggler+div{overflow:hidden}.expandWrapper>.toggler:not(.expanded)+div{max-height:0!important}`,g={"more-info":t=>{const e=new Event("hass-more-info",{composed:!0});e.detail={entityId:t.entity.entity},t.card.dispatchEvent(e)},navigate:t=>{if(!t.config.navigation_path)return void n("Missing 'navigation_path' for 'navigate' tap action");window.history.pushState(null,"",t.config.navigation_path);const e=new Event("location-changed",{composed:!0});e.detail={replace:!1},window.dispatchEvent(e)},"call-service":t=>{if(!t.config.service)return void n("Missing 'service' for 'call-service' tap action");const[e,i]=t.config.service.split(".",2),r=Object.assign({},t.config.service_data);f.hass.callService(e,i,r)},url:t=>{t.config.url_path?window.location.href=t.config.url_path:n("Missing 'url_path' for 'url' tap action")}};class f{static getAction(t){return t.config&&"none"!=t.config.action?e=>{e.stopPropagation(),t.config.action in g?g[t.config.action](t):n("Unknown tap action type: "+t.config.action)}:null}}class p{constructor(t,e){this.config=t,this.action=e,this._level="Unknown",this._charging=!1,this._secondary_info=null,this._is_hidden=!1,this.updated=!1,this.colorPattern=/^#[A-Fa-f0-9]{6}$/,this.stringValuePattern=/\b([0-9]{1,3})\s?%/,this._name=t.name||t.entity}get entity_id(){return this.config.entity}get data_required_for(){var t;return(null===(t=this.config.charging_state)||void 0===t?void 0:t.entity_id)?[this.config.entity,this.config.charging_state.entity_id]:[this.config.entity]}set name(t){this.updated=this.updated||this._name!=t,this._name=t}get name(){let t=this._name;return o(this.config.bulk_rename).forEach(e=>{t="/"==e.from[0]&&"/"==e.from[e.from.length-1]?t.replace(new RegExp(e.from.substr(1,e.from.length-2)),e.to||""):t.replace(e.from,e.to||"")}),t}set level(t){this.updated=this.updated||this._level!=t,this._level=t}get level(){return this._level}set charging(t){this.updated=this.updated||this.charging!=t,this._charging=t}get charging(){return this._charging}get is_hidden(){return this._is_hidden}set is_hidden(t){this.updated=this.updated||this._is_hidden!=t,this._is_hidden=t}get secondary_info(){return this._secondary_info}set secondary_info(t){this.updated=this.updated||this._secondary_info!=t,this._secondary_info=t}get levelColor(){var t,e;const i="inherit",n=Number(this._level);if(this.charging&&(null===(t=this.config.charging_state)||void 0===t?void 0:t.color))return this.config.charging_state.color;if(isNaN(n)||n>100||n<0)return i;if(this.config.color_gradient&&this.isColorGradientValid(this.config.color_gradient))return function(t,e){e/=100;const i=t.map((e,i)=>({pct:1/(t.length-1)*i,color:r(e)}));let n=1;for(n=1;nn<=t.value))||void 0===e?void 0:e.color)||i}get icon(){var t;const e=Number(this._level);if(this.charging&&(null===(t=this.config.charging_state)||void 0===t?void 0:t.icon))return this.config.charging_state.icon;if(this.config.icon)return this.config.icon;if(isNaN(e)||e>100||e<0)return"mdi:battery-unknown";const i=10*Math.round(e/10);switch(i){case 100:return this.charging?"mdi:battery-charging-100":"mdi:battery";case 0:return this.charging?"mdi:battery-charging-outline":"mdi:battery-outline";default:return(this.charging?"mdi:battery-charging-":"mdi:battery-")+i}}get classNames(){const t=[];return this.action&&t.push("clickable"),!s(this.level)&&t.push("non-numeric-state"),t.join(" ")}update(t){const e=t.states[this.config.entity];e?(this.updated=!1,this.name=this.config.name||e.attributes.friendly_name,this.level=this.getLevel(e,t),this.charging=this.getChargingState(t),this.secondary_info=this.setSecondaryInfo(t,e)):n("Entity not found: "+this.config.entity,"error")}getLevel(t,e){var i;const r=e.localize("state.default.unknown");let o;if(this.config.attribute)o=t.attributes[this.config.attribute],null==o&&(n(`Attribute "${this.config.attribute}" doesn't exist on "${this.config.entity}" entity`),o=r);else{const e=[t.attributes.battery_level,t.attributes.battery,t.state];o=(null===(i=e.find(t=>null!=t))||void 0===i?void 0:i.toString())||r}if(this.config.state_map){const t=this.config.state_map.find(t=>t.from===o);void 0===t?s(o)||n(`Missing option for '${o}' in 'state_map'`):o=t.to.toString()}if(!s(o)){const t=this.stringValuePattern.exec(o);null!=t&&(o=t[1])}return this.config.multiplier&&s(o)&&(o=(this.config.multiplier*Number(o)).toString()),o=void 0===this.config.value_override?o:this.config.value_override,s(o)||(o=o.charAt(0).toUpperCase()+o.slice(1)),o}getChargingState(t){const e=this.config.charging_state;if(!e)return!1;let i=this.level,r=t.states[this.config.entity];if(e.entity_id){if(r=t.states[e.entity_id],!r)return n(`'charging_state' entity id (${e.entity_id}) not found`),!1;i=r.state}const s=o(e.attribute);if(0!=s.length){const t=s.find(t=>null!=r.attributes[t.name]);return!!t&&(null==t.value||r.attributes[t.name]==t.value)}const a=o(e.state);return 0==a.length?!!i:a.some(t=>t==i)}setSecondaryInfo(t,e){var i;if(this.config.secondary_info){if("charging"==this.config.secondary_info)return this.charging?(null===(i=this.config.charging_state)||void 0===i?void 0:i.secondary_info_text)||"Charging":null;{const i=e[this.config.secondary_info]||e.attributes[this.config.secondary_info]||this.config.secondary_info;return isNaN(Date.parse(i))?i:((t,e)=>{let i=Date.parse(e);if(isNaN(i))return t.localize("ui.components.relative_time.never");i=Math.round((Date.now()-i)/1e3);let n="";return n=i<60?t.localize("ui.components.relative_time.past_duration.second","count",i):i<3600?t.localize("ui.components.relative_time.past_duration.minute","count",Math.round(i/60)):i<86400?t.localize("ui.components.relative_time.past_duration.hour","count",Math.round(i/3600)):i<604800?t.localize("ui.components.relative_time.past_duration.day","count",Math.round(i/86400)):t.localize("ui.components.relative_time.past_duration.week","count",Math.round(i/604800)),n})(t,i)}}return null}isColorGradientValid(t){if(!(t.length<2)){for(const e of t)if(!this.colorPattern.test(e))return n("Color '${color}' is not valid. Please provide valid HTML hex color in #XXXXXX format."),!1;return!0}n("Value for 'color_gradient' should be an array with at least 2 colors.")}}const v=(t,e,i)=>t.findIndex(t=>{var n,r;if(t.group_id&&!(null===(r=null===(n=i[t.group_id])||void 0===n?void 0:n.entity_id)||void 0===r?void 0:r.some(t=>e.entity_id==t)))return!1;if(t.entities&&!t.entities.some(t=>e.entity_id==t))return!1;const s=isNaN(Number(e.level))?0:Number(e.level);return s>=t.min&&s<=t.max});var m=t=>t.forEach(t=>{null==t.min&&(t.min=0),null!=t.max&&t.max{if((null==i?void 0:i.group_id)&&!t[i.group_id])throw new Error("Group not found: "+i.group_id);let n=null==i?void 0:i.name;!n&&(null==i?void 0:i.group_id)&&(n=t[i.group_id].friendly_name);let r=null==i?void 0:i.icon;return void 0===r&&(null==i?void 0:i.group_id)&&(r=t[i.group_id].icon),{name:n,icon:r,batteries:e,secondary_info:null==i?void 0:i.secondary_info}},_=(t,e)=>t=t.replace(/\{[a-z]+\}/g,t=>{switch(t){case"{min}":return e.batteries.reduce((t,e)=>t>Number(e.level)?Number(e.level):t,100).toString();case"{max}":return e.batteries.reduce((t,e)=>tt>Number(e.level)?Number(e.level):t,100).toString(),n=e.batteries.reduce((t,e)=>tvoid 0!==t,contains:(t,e)=>void 0!==t&&-1!=t.toString().indexOf(e.toString()),"=":(t,e)=>t==e,">":(t,e)=>Number(t)>e,"<":(t,e)=>Number(t)=":(t,e)=>Number(t)>=e,"<=":(t,e)=>Number(t)<=e,matches:(t,e)=>{if(void 0===t)return!1;let i;const n=(e=e.toString()).match(x);return n?i=new RegExp(n[1],n[2]):-1!=e.indexOf("*")&&(i=new RegExp("^"+e.replace(/\*/g,".*")+"$")),i?i.test(t.toString()):t===e}};class N{constructor(t){this.config=t}get is_permanent(){return"state"!=this.config.name}isValid(t,e){const i=this.getValue(t,e);return this.meetsExpectations(i)}getValue(t,e){if(this.config.name)return 0==this.config.name.indexOf("attributes.")?t.attributes[this.config.name.substr(11)]:"state"==this.config.name&&void 0!==e?e:t[this.config.name];n("Missing filter 'name' property")}meetsExpectations(t){let e=this.config.operator;if(!e)if(void 0===this.config.value)e="exists";else{const t=this.config.value.toString();e=-1!=t.indexOf("*")||"/"==t[0]&&"/"==t[t.length-1]?"matches":"="}const i=w[e];return i?i(t,this.config.value):(n(`Operator '${this.config.operator}' not supported. Supported operators: ${Object.keys(w).join(", ")}`),!1)}}class E{constructor(t,e){var i,n,r,s;this.config=t,this.cardNode=e,this.batteries=[],this.groupsToResolve=[],this.groupsData={},this.initialized=!1,this.include=null===(n=null===(i=t.filter)||void 0===i?void 0:i.include)||void 0===n?void 0:n.map(t=>new N(t)),this.exclude=null===(s=null===(r=t.filter)||void 0===r?void 0:r.exclude)||void 0===s?void 0:s.map(t=>new N(t)),this.include||(this.initialized=!1),this.processExplicitEntities()}update(t){let e=!1;return this.initialized||(this.initialized=!0,e=this.processGroups(t)||e,e=this.processIncludes(t)||e),e=this.updateBatteries(t)||e,e&&this.processExcludes(t),e}getBatteries(){return((t,e,i)=>{const n={batteries:[],groups:[]};if(!t)return n.batteries=e,n;if("number"==typeof t){let r=e.filter(t=>!t.is_hidden);n.batteries=r.slice(0,t),n.groups.push(y(i,r.slice(t)))}else m(t),e.forEach(e=>{const r=v(t,e,i);-1==r?n.batteries.push(e):(n.groups[r]=n.groups[r]||y(i,[],t[r]),n.groups[r].batteries.push(e))});return n.groups.forEach(t=>{t.name&&(t.name=_(t.name,t)),t.secondary_info&&(t.secondary_info=_(t.secondary_info,t))}),n})(this.config.collapse,this.batteries,this.groupsData)}createBattery(t){return b.filter(e=>null==t[e]).forEach(e=>t[e]=this.config[e]),new p(t,f.getAction({card:this.cardNode,config:a(t.tap_action||this.config.tap_action||null,"action"),entity:t}))}processExplicitEntities(){let t=this.config.entity?[this.config]:(this.config.entities||[]).map(t=>("string"==typeof t&&(t={entity:t}),t));t=t.filter(t=>{if(!t.entity)throw new Error("Invalid configuration - missing property 'entity' on:\n"+JSON.stringify(t));return!t.entity.startsWith("group.")||(this.groupsToResolve.push(t.entity),!1)}),this.config.collapse&&Array.isArray(this.config.collapse)&&this.config.collapse.forEach(e=>{e.group_id?-1==this.groupsToResolve.indexOf(e.group_id)&&this.groupsToResolve.push(e.group_id):e.entities&&e.entities.forEach(e=>{t.some(t=>t.entity==e)||t.push({entity:e})})}),this.batteries=t.map(t=>this.createBattery(t))}processIncludes(t){let e=!1;return this.include?(Object.keys(t.states).forEach(i=>{var n;(null===(n=this.include)||void 0===n?void 0:n.some(e=>e.isValid(t.states[i])))&&!this.batteries.some(t=>t.entity_id==i)&&(e=!0,this.batteries.push(this.createBattery({entity:i})))}),e):e}processGroups(t){let e=!1;return this.groupsToResolve.forEach(i=>{const r=t.states[i];if(!r)return void n(`Group "${i}" not found`);const s=r.attributes;Array.isArray(s.entity_id)?(s.entity_id.forEach(t=>{this.batteries.some(e=>e.entity_id==t)||(e=!0,this.batteries.push(this.createBattery({entity:t})))}),this.groupsData[i]=s):n(`Entities not found in "${i}"`)}),this.groupsToResolve=[],e}processExcludes(t){if(null==this.exclude)return;const e=this.exclude,i=[];this.batteries.forEach((n,r)=>{let s=!1;for(let o of e)o.isValid(t.states[n.entity_id],n.level)&&(o.is_permanent?i.push(r):s=!0);n.is_hidden=s}),i.reverse().forEach(t=>this.batteries.splice(t,1))}updateBatteries(t){let e=!1;if(this.batteries.forEach((i,n)=>{i.update(t),e=e||i.updated}),e){switch(this.config.sort_by_level){case"asc":this.batteries.sort((t,e)=>this.sort(t.level,e.level));break;case"desc":this.batteries.sort((t,e)=>this.sort(e.level,t.level));break;default:this.config.sort_by_level&&n("Unknown sort option. Allowed values: 'asc', 'desc'")}this.batteries=[...this.batteries]}return e}sort(t,e){let i=Number(t),n=Number(e);return i=isNaN(i)?-1:i,n=isNaN(n)?-1:n,i-n}}customElements.define("battery-state-card",class extends t{constructor(){super(...arguments),this.rawConfig="",this.config={},this.simpleView=!1,this.batteryProvider=null,this.cssStyles="",this.triggerRender=function(t,e){let i;return(...e)=>{i&&(clearTimeout(i),i=null),i=setTimeout(()=>t.apply(null,e),100)}}(()=>this.requestUpdate())}static get styles(){return u}setConfig(t){var e;if(!(t.entities||t.entity||(null===(e=t.filter)||void 0===e?void 0:e.include)||Array.isArray(t.collapse)))throw new Error("You need to define entities, filter.include or collapse.group");const i=JSON.stringify(t);this.rawConfig!==i&&(this.rawConfig=i,this.config=JSON.parse(i),this.simpleView=!!this.config.entity,this.batteryProvider=new E(this.config,this),this.triggerRender())}set hass(t){f.hass=t;this.batteryProvider.update(t)&&this.triggerRender()}render(){const t=this.batteryProvider.getBatteries();if(this.simpleView)return h(t.batteries[0]);let i=[];return t.batteries.forEach(t=>!t.is_hidden&&i.push(h(t))),t.groups.forEach(t=>{const n=[];var r,s;t.batteries.forEach(t=>!t.is_hidden&&n.push(h(t))),n.length&&i.push((r=n,s=t,Math.random().toString().substr(2),e`
${l(s.icon,s.iconColor)}
${s.name} ${c(s.secondary_info)}
${r}
`))}),0==i.length?e``:d(this.config.name||this.config.title,i)}updated(){var t;if(!(null===(t=this.config)||void 0===t?void 0:t.style)||this.cssStyles==this.config.style)return;this.cssStyles=this.config.style;let e=this.shadowRoot.querySelector("style");e||(e=document.createElement("style"),e.type="text/css",this.shadowRoot.appendChild(e)),e.innerHTML=((t,e)=>e.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g,e=>`${t} ${e}`))("ha-card",this.cssStyles)}getCardSize(){var t;let e=(null===(t=this.config.entities)||void 0===t?void 0:t.length)||1;return this.config.collapse?"number"==typeof this.config.collapse?this.config.collapse+1:this.config.collapse.length+1:e+1}})}(); +!function(){"use strict"; +/*! ***************************************************************************** + 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,s){var n,r=arguments.length,o=r<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,i,s);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(o=(r<3?n(o):r>3?n(e,i,o):n(e,i))||o);return r>3&&o&&Object.defineProperty(e,i,o),o}function e(t,e,i,s){return new(i||(i=Promise))((function(n,r){function o(t){try{l(s.next(t))}catch(t){r(t)}}function a(t){try{l(s.throw(t))}catch(t){r(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(o,a)}l((s=s.apply(t,e||[])).next())}))} +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const i=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),n=new Map;class r{constructor(t,e){if(this._$cssResult$=!0,e!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){let t=n.get(this.cssText);return i&&void 0===t&&(n.set(this.cssText,t=new CSSStyleSheet),t.replaceSync(this.cssText)),t}toString(){return this.cssText}}const o=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,s)=>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[s+1]),t[0]);return new r(i,s)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new r("string"==typeof t?t:t+"",s))(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var l;const c=window.trustedTypes,h=c?c.emptyScript:"",d=window.reactiveElementPolyfillSupport,u={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}},p=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:u,reflect:!1,hasChanged:p};class f 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=g){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 n=this[t];this[e]=s,this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}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(a(t))}else void 0!==t&&e.push(a(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 e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,e)=>{i?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),s=window.litNonce;void 0!==s&&i.setAttribute("nonce",s),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._$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=g){var s,n;const r=this.constructor._$Eh(t,i);if(void 0!==r&&!0===i.reflect){const o=(null!==(n=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==n?n:u.toAttribute)(e,i.type);this._$Ei=t,null==o?this.removeAttribute(r):this.setAttribute(r,o),this._$Ei=null}}_$AK(t,e){var i,s,n;const r=this.constructor,o=r._$Eu.get(t);if(void 0!==o&&this._$Ei!==o){const t=r.getPropertyOptions(o),a=t.converter,l=null!==(n=null!==(s=null===(i=a)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof a?a:null)&&void 0!==n?n:u.fromAttribute;this._$Ei=o,this[o]=l(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||p)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$E_&&(this._$E_=new Map),this._$E_.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this._$Ep=this._$EC())}async _$EC(){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._$E_&&(this._$E_.forEach(((t,e)=>this._$ES(e,this[e],t))),this._$E_=void 0),this._$EU()}updated(t){}firstUpdated(t){}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var v;f.finalized=!0,f.elementProperties=new Map,f.elementStyles=[],f.shadowRootOptions={mode:"open"},null==d||d({ReactiveElement:f}),(null!==(l=globalThis.reactiveElementVersions)&&void 0!==l?l:globalThis.reactiveElementVersions=[]).push("1.1.1");const y=globalThis.trustedTypes,m=y?y.createPolicy("lit-html",{createHTML:t=>t}):void 0,_=`lit$${(Math.random()+"").slice(9)}$`,b="?"+_,$=`<${b}>`,A=document,E=(t="")=>A.createComment(t),w=t=>null===t||"object"!=typeof t&&"function"!=typeof t,x=Array.isArray,S=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,C=/-->/g,N=/>/g,U=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,P=/'/g,O=/"/g,k=/^(?:script|style|textarea)$/i,T=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),I=Symbol.for("lit-noChange"),R=Symbol.for("lit-nothing"),M=new WeakMap,H=A.createTreeWalker(A,129,null,!1),z=(t,e)=>{const i=t.length-1,s=[];let n,r=2===e?"":"",o=S;for(let e=0;e"===l[0]?(o=null!=n?n:S,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,a=l[1],o=void 0===l[3]?U:'"'===l[3]?O:P):o===O||o===P?o=U:o===C||o===N?o=S:(o=U,n=void 0);const d=o===U&&t[e+1].startsWith("/>")?" ":"";r+=o===S?i+$:c>=0?(s.push(a),i.slice(0,c)+"$lit$"+i.slice(c)+_+d):i+_+(-2===c?(s.push(void 0),e):d)}const a=r+(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 j{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let n=0,r=0;const o=t.length-1,a=this.parts,[l,c]=z(t,e);if(this.el=j.createElement(l,i),H.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=H.nextNode())&&a.length0){s.textContent=y?y.emptyScript:"";for(let i=0;i{var e;return x(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])})(t)?this.A(t):this.$(t)}M(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}S(t){this._$AH!==t&&(this._$AR(),this._$AH=this.M(t))}$(t){this._$AH!==R&&w(this._$AH)?this._$AA.nextSibling.data=t:this.S(A.createTextNode(t)),this._$AH=t}T(t){var e;const{values:i,_$litType$:s}=t,n="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=j.createElement(s.h,this.options)),s);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===n)this._$AH.m(i);else{const t=new B(n,this),e=t.p(this.options);t.m(i),this.S(e),this._$AH=t}}_$AC(t){let e=M.get(t.strings);return void 0===e&&M.set(t.strings,e=new j(t)),e}A(t){x(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const n of t)s===e.length?e.push(i=new D(this.M(E()),this.M(E()),this,this.options)):i=e[s],i._$AI(n),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=R}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,s){const n=this.strings;let r=!1;if(void 0===n)t=L(this,t,e,0),r=!w(t)||t!==this._$AH&&t!==I,r&&(this._$AH=t);else{const s=t;let o,a;for(t=n[0],o=0;o{var s,n;const r=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let o=r._$litPart$;if(void 0===o){const t=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:null;r._$litPart$=o=new D(e.insertBefore(E(),t),t,void 0,null!=i?i:{})}return o._$AI(t),o})(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 I}}Z.finalized=!0,Z._$litElement$=!0,null===(F=globalThis.litElementHydrateSupport)||void 0===F||F.call(globalThis,{LitElement:Z});const Y=globalThis.litElementPolyfillSupport;null==Y||Y({LitElement:Z}),(null!==(K=globalThis.litElementVersions)&&void 0!==K?K:globalThis.litElementVersions=[]).push("3.1.1"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const tt=(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 et(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):tt(t,e)}const it=(t,e="warn")=>{console[e]("[battery-state-card] "+t)},st=t=>(t=t.replace("#",""),{r:parseInt(t.substr(0,2),16),g:parseInt(t.substr(2,2),16),b:parseInt(t.substr(4,2),16)}),nt=t=>null!==t&&""!==t&&!isNaN(Number(t)),rt=t=>Array.isArray(t)?t:t?[t]:[],ot=(t,e)=>{switch(typeof t){case"string":const i={};return i[e]=t,i;case"object":return Object.assign({},t)}return t},at=t=>t&&T`
${t}
`,lt=(t,e)=>t&&T`
`,ct=t=>{return T`${lt(t.icon,t.iconColor)}
${t.name} ${"string"==typeof t.secondaryInfo?at(t.secondaryInfo):(e=t.hass,i=t.secondaryInfo,i&&T`
`)}
${t.state}${t.unit}
`;var e,i};class ht extends Z{constructor(){super(...arguments),this.updateNotifyQueue=[],this.configUpdated=!1,this.hassUpdated=!1,this.triggerUpdate=function(t,e){let i;return(...e)=>{i&&(clearTimeout(i),i=null),i=setTimeout((()=>t.apply(null,e)),100)}}((()=>e(this,void 0,void 0,(function*(){yield this.internalUpdate(this.configUpdated,this.hassUpdated),this.configUpdated=!1,this.hassUpdated=!1,this.updateNotifyQueue.forEach((t=>t())),this.updateNotifyQueue=[]}))))}set hass(t){this._hass=t,this.hassUpdated=!0,this.triggerUpdate()}get hass(){return this._hass}get cardUpdated(){return new Promise((t=>this.updateNotifyQueue.push(t)))}setConfig(t){this.config=JSON.parse(JSON.stringify(t)),this.configUpdated=!0,this.triggerUpdate()}}var dt="\n.clickable {\n cursor: pointer;\n}\n\n.truncate {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n.name {\n flex: 1;\n margin: 0 6px;\n}\n\n.secondary {\n color: var(--secondary-text-color);\n}\n\n.icon {\n flex: 0 0 40px;\n border-radius: 50%;\n text-align: center;\n line-height: 40px;\n margin-right: 10px;\n color: var(--paper-item-icon-color)\n}";const ut={"more-info":t=>{const e=new Event("hass-more-info",{composed:!0});e.detail={entityId:t.entityId},t.card.dispatchEvent(e)},navigate:t=>{if(!t.config.navigation_path)return void it("Missing 'navigation_path' for 'navigate' tap action");window.history.pushState(null,"",t.config.navigation_path);const e=new Event("location-changed",{composed:!0});e.detail={replace:!1},window.dispatchEvent(e)},"call-service":(t,e)=>{if(!t.config.service)return void it("Missing 'service' for 'call-service' tap action");const[i,s]=t.config.service.split(".",2),n=Object.assign({},t.config.service_data);e.callService(i,s,n)},url:t=>{t.config.url_path?window.location.href=t.config.url_path:it("Missing 'url_path' for 'url' tap action")}},pt=/\b([0-9]{1,3})\s?%/,gt=/^#[A-Fa-f0-9]{6}$/;class ft extends ht{static get styles(){return o([dt+":host {\n display: flex;\n align-items: center;\n}\n"])}internalUpdate(){return e(this,void 0,void 0,(function*(){this.name=vt(this.config,this.hass),this.state=mt(this.config,this.hass),nt(this.state)?this.unit=String.fromCharCode(160)+(this.config.unit||"%"):this.unit=void 0;const t=$t(this.config,this.state,this.hass);this.secondaryInfo=yt(this.config,this.hass,t),this.icon=_t(this.config,Number(this.state),t,this.hass),this.iconColor=bt(this.config,this.state,t)}))}connectedCallback(){super.connectedCallback(),this.setupAction(!0)}disconnectedCallback(){super.disconnectedCallback(),this.setupAction(!1)}render(){return ct(this)}setupAction(t=!0){t?this.config.tap_action&&!this.action&&(this.action=t=>{var e,i;t.stopPropagation(),e={card:this,config:ot(this.config.tap_action,"action"),entityId:this.config.entity},i=this.hass,e.config&&"none"!=e.config.action&&(e.config.action in ut?ut[e.config.action](e,i):it("Unknown tap action type: "+e.config.action))},this.addEventListener("click",this.action),this.classList.add("clickable")):this.action&&(this.classList.remove("clickable"),this.removeEventListener("click",this.action),this.action=void 0)}}t([et({attribute:!1})],ft.prototype,"name",void 0),t([et({attribute:!1})],ft.prototype,"secondaryInfo",void 0),t([et({attribute:!1})],ft.prototype,"state",void 0),t([et({attribute:!1})],ft.prototype,"unit",void 0),t([et({attribute:!1})],ft.prototype,"icon",void 0),t([et({attribute:!1})],ft.prototype,"iconColor",void 0),t([et({attribute:!1})],ft.prototype,"action",void 0);const vt=(t,e)=>{var i;if(t.name)return t.name;if(!e)return t.entity;let s=(null===(i=e.states[t.entity])||void 0===i?void 0:i.attributes.friendly_name)||t.entity;return rt(t.bulk_rename).forEach((t=>{s="/"==t.from[0]&&"/"==t.from[t.from.length-1]?s.replace(new RegExp(t.from.substr(1,t.from.length-2)),t.to||""):s.replace(t.from,t.to||"")})),s},yt=(t,e,i)=>{var s;if(t.secondary_info){if("charging"==t.secondary_info)return i?(null===(s=t.charging_state)||void 0===s?void 0:s.secondary_info_text)||"Charging":null;{let i=t.secondary_info;const s=null==e?void 0:e.states[t.entity];s&&(i=s[t.secondary_info]||(null==s?void 0:s.attributes[t.secondary_info])||t.secondary_info);const n=Date.parse(i);return isNaN(n)?i:new Date(n)}}return null},mt=(t,e)=>{var i;const s=(null==e?void 0:e.localize("state.default.unknown"))||"Unknown";let n;if(void 0!==t.value_override)return t.value_override;const r=null==e?void 0:e.states[t.entity];if(!r)return s;if(t.attribute)n=r.attributes[t.attribute],null==n&&(it(`Attribute "${t.attribute}" doesn't exist on "${t.entity}" entity`),n=s);else{const t=[r.attributes.battery_level,r.attributes.battery,r.state];n=t.find((t=>nt(t)))||(null===(i=t.find((t=>null!=t)))||void 0===i?void 0:i.toString())||s}if(t.state_map){const e=t.state_map.find((t=>t.from===n));void 0===e?nt(n)||it(`Missing option for '${n}' in 'state_map'`):n=e.to.toString()}if(!nt(n)){const t=pt.exec(n);null!=t&&(n=t[1])}return nt(n)?(t.multiplier&&(n=(t.multiplier*Number(n)).toString()),"number"==typeof t.round&&(n=parseFloat(n).toFixed(t.round).toString())):n=n.charAt(0).toUpperCase()+n.slice(1),n},_t=(t,e,i,s)=>{var n;if(i&&(null===(n=t.charging_state)||void 0===n?void 0:n.icon))return t.charging_state.icon;if(t.icon){const e="attribute.";if(s&&t.icon.startsWith(e)){const i=t.icon.substr(e.length),n=s.states[t.entity].attributes[i];return n||(it(`Icon attribute missing in '${t.entity}' entity`,"error"),t.icon)}return t.icon}if(isNaN(e)||e>100||e<0)return"mdi:battery-unknown";const r=10*Math.round(e/10);switch(r){case 100:return i?"mdi:battery-charging-100":"mdi:battery";case 0:return i?"mdi:battery-charging-outline":"mdi:battery-outline";default:return(i?"mdi:battery-charging-":"mdi:battery-")+r}},bt=(t,e,i)=>{var s,n;const r="inherit",o=Number(e);if(i&&(null===(s=t.charging_state)||void 0===s?void 0:s.color))return t.charging_state.color;if(isNaN(o)||o>100||o<0)return r;if(t.color_gradient&&At(t.color_gradient))return function(t,e){e/=100;const i=t.map(((e,i)=>({pct:1/(t.length-1)*i,color:st(e)})));let s=1;for(s=1;so<=t.value)))||void 0===n?void 0:n.color)||r},$t=(t,e,i)=>{const s=t.charging_state;if(!s||!i)return!1;let n=i.states[t.entity];if(s.entity_id){if(n=i.states[s.entity_id],!n)return it(`'charging_state' entity id (${s.entity_id}) not found`),!1;e=n.state}const r=rt(s.attribute);if(0!=r.length){const t=r.find((t=>void 0!==Et(n.attributes,t.name)));return!!t&&(void 0===t.value||Et(n.attributes,t.name)==t.value)}const o=rt(s.state);return 0==o.length?!!e:o.some((t=>t==e))},At=t=>{if(!(t.length<2)){for(const e of t)if(!gt.test(e))return it("Color '${color}' is not valid. Please provide valid HTML hex color in #XXXXXX format."),!1;return!0}it("Value for 'color_gradient' should be an array with at least 2 colors.")},Et=(t,e)=>(void 0===t||e.split(".").forEach((e=>{t=t?t[e]:void 0})),t),wt=t=>{return T`${e=t.header,e&&T`
${e}
`}
${t.list.map((e=>xt(t.batteries[e])))} ${t.groups.map((e=>((t,e)=>(Math.random().toString().substr(2),T`
${lt(t.icon,t.iconColor)}
${t.title} ${at(t.secondaryInfo)}
${t.batteryIds.map((t=>xt(e[t])))}
`))(e,t.batteries)))}
`;var e},xt=t=>T`
${t}
`,St=["tap_action","state_map","charging_state","secondary_info","color_thresholds","color_gradient","bulk_rename","icon","round","unit"],Ct=/\/([^/]+)\/([igmsuy]*)/,Nt={exists:t=>void 0!==t,contains:(t,e)=>void 0!==t&&-1!=t.toString().indexOf(e.toString()),"=":(t,e)=>t==e,">":(t,e)=>Number(t)>e,"<":(t,e)=>Number(t)=":(t,e)=>Number(t)>=e,"<=":(t,e)=>Number(t)<=e,matches:(t,e)=>{if(void 0===t)return!1;let i;const s=(e=e.toString()).match(Ct);return s?i=new RegExp(s[1],s[2]):-1!=e.indexOf("*")&&(i=new RegExp("^"+e.replace(/\*/g,".*")+"$")),i?i.test(t.toString()):t===e}};class Ut{constructor(t){this.config=t}get is_permanent(){return"state"!=this.config.name}isValid(t,e){const i=this.getValue(t,e);return this.meetsExpectations(i)}getValue(t,e){if(this.config.name)return 0==this.config.name.indexOf("attributes.")?t.attributes[this.config.name.substr(11)]:"state"==this.config.name&&void 0!==e?e:t[this.config.name];it("Missing filter 'name' property")}meetsExpectations(t){let e=this.config.operator;if(!e)if(void 0===this.config.value)e="exists";else{const t=this.config.value.toString();e=-1!=t.indexOf("*")||"/"==t[0]&&"/"==t[t.length-1]?"matches":"="}const i=Nt[e];return i?i(t,this.config.value):(it(`Operator '${this.config.operator}' not supported. Supported operators: ${Object.keys(Nt).join(", ")}`),!1)}}class Pt{constructor(t){var e,i,s,n;this.config=t,this.batteries={},this.groupsToResolve=[],this.groupsData={},this.initialized=!1,this.include=null===(i=null===(e=t.filter)||void 0===e?void 0:e.include)||void 0===i?void 0:i.map((t=>new Ut(t))),this.exclude=null===(n=null===(s=t.filter)||void 0===s?void 0:s.exclude)||void 0===n?void 0:n.map((t=>new Ut(t))),this.include||(this.initialized=!1),this.processExplicitEntities()}update(t){return e(this,void 0,void 0,(function*(){this.initialized||(this.initialized=!0,this.processGroupEntities(t),this.processIncludes(t)),this.processExcludes(t);const e=Object.keys(this.batteries).map((e=>{const i=this.batteries[e];return i.hass=t,i.cardUpdated}));yield Promise.all(e)}))}getBatteries(){return this.batteries}createBattery(t){St.filter((e=>null==t[e])).forEach((e=>t[e]=this.config[e]));const e=new ft;return e.entityId=t.entity,e.setConfig(t),e}processExplicitEntities(){let t=(this.config.entities||[]).map((t=>("string"==typeof t&&(t={entity:t}),t)));t=t.filter((t=>{if(!t.entity)throw new Error("Invalid configuration - missing property 'entity' on:\n"+JSON.stringify(t));return!t.entity.startsWith("group.")||(this.groupsToResolve.push(t.entity),!1)})),this.config.collapse&&Array.isArray(this.config.collapse)&&this.config.collapse.forEach((e=>{e.group_id?-1==this.groupsToResolve.indexOf(e.group_id)&&this.groupsToResolve.push(e.group_id):e.entities&&e.entities.forEach((e=>{t.some((t=>t.entity==e))||t.push({entity:e})}))})),t.forEach((t=>{this.batteries[t.entity]=this.createBattery(t)}))}processIncludes(t){this.include&&Object.keys(t.states).forEach((e=>{var i;(null===(i=this.include)||void 0===i?void 0:i.some((i=>i.isValid(t.states[e]))))&&!this.batteries[e]&&(this.batteries[e]=this.createBattery({entity:e}))}))}processGroupEntities(t){this.groupsToResolve.forEach((e=>{const i=t.states[e];if(!i)return void it(`Group "${e}" not found`);const s=i.attributes;Array.isArray(s.entity_id)?(s.entity_id.forEach((t=>{this.batteries[t]||(this.batteries[t]=this.createBattery({entity:t}))})),this.groupsData[e]=s):it(`Entities not found in "${e}"`)})),this.groupsToResolve=[]}processExcludes(t){if(null==this.exclude)return;const e=this.exclude,i=[];Object.keys(this.batteries).forEach((s=>{const n=this.batteries[s];let r=!1;for(let o of e)if(o.isValid(t.states[s],n.state)){if(o.is_permanent){i.push(s);break}r=!0}n.isHidden=r})),i.forEach((t=>delete this.batteries[t]))}}const Ot=(t,e,i)=>t.findIndex((t=>{var s,n;if(t.group_id&&!(null===(n=null===(s=i[t.group_id])||void 0===s?void 0:s.entity_id)||void 0===n?void 0:n.some((t=>e.entityId==t))))return!1;if(t.entities&&!t.entities.some((t=>e.entityId==t)))return!1;const r=isNaN(Number(e.state))?0:Number(e.state);return r>=t.min&&r<=t.max}));var kt=t=>t.forEach((t=>{null==t.min&&(t.min=0),null!=t.max&&t.max{if((null==i?void 0:i.group_id)&&!t[i.group_id])throw new Error("Group not found: "+i.group_id);let s=null==i?void 0:i.name;!s&&(null==i?void 0:i.group_id)&&(s=t[i.group_id].friendly_name);let n=null==i?void 0:i.icon;return void 0===n&&(null==i?void 0:i.group_id)&&(n=t[i.group_id].icon),{title:s,icon:n,iconColor:null==i?void 0:i.icon_color,batteryIds:e,secondaryInfo:null==i?void 0:i.secondary_info}},It=(t,e,i)=>t=t.replace(/\{[a-z]+\}/g,(t=>{switch(t){case"{min}":return e.batteryIds.reduce(((t,e)=>t>Number(i[e].state)?Number(i[e].state):t),100).toString();case"{max}":return e.batteryIds.reduce(((t,e)=>tt>Number(i[e].state)?Number(i[e].state):t),100).toString(),n=e.batteryIds.reduce(((t,e)=>t{switch(t){case"first":t=e.length>0?i[e[0]].icon:void 0;break;case"last":if(e.length>0){t=i[e[e.length-1]].icon}else t=void 0}return t},Mt=(t,e,i)=>{switch(t){case"first":t=e.length>0?i[e[0]].iconColor:void 0;break;case"last":if(e.length>0){t=i[e[e.length-1]].iconColor}else t=void 0}return t};class Ht extends ht{constructor(){super(...arguments),this.list=[],this.groups=[],this.batteries={}}static get styles(){return o([dt+".entity-spacing {\n margin: 8px 0;\n}\n\n.entity-spacing:first-child {\n margin-top: 0;\n}\n\n.entity-spacing:last-child {\n margin-bottom: 0;\n}\n\n.expandWrapper > .toggler {\n display: flex;\n align-items: center;\n cursor: pointer;\n}\n.expandWrapper > .toggler > .name {\n flex: 1;\n}\n.expandWrapper > .toggler div.chevron {\n transform: rotate(-90deg);\n font-size: 26px;\n height: 40px;\n width: 40px;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.expandWrapper > .toggler .chevron,\n.expandWrapper > .toggler + div {\n transition: all 0.5s ease;\n}\n.expandWrapper > .toggler.expanded .chevron {\n transform: rotate(-90deg) scaleX(-1);\n}\n.expandWrapper > .toggler + div {\n overflow: hidden;\n}\n.expandWrapper > .toggler:not(.expanded) + div {\n max-height: 0 !important;\n}"])}internalUpdate(t,i){return e(this,void 0,void 0,(function*(){(null==this.batteryProvider||t)&&(this.batteryProvider=new Pt(this.config)),i&&(yield this.batteryProvider.update(this.hass)),this.header=this.config.title,this.batteries=this.batteryProvider.getBatteries();const e=zt(this.config,this.batteries).filter((t=>!this.batteries[t].isHidden)),s=((t,e,i,s)=>{const n={list:[],groups:[]};if(!i)return n.list=e,n;if("number"==typeof i){n.list=e.slice(0,i);const t=e.slice(i);t.length>0&&n.groups.push(Tt(s,t))}else kt(i),e.forEach((e=>{const r=Ot(i,t[e],s);-1==r?n.list.push(e):(n.groups[r]=n.groups[r]||Tt(s,[],i[r]),n.groups[r].batteryIds.push(e))}));return n.groups.forEach((e=>{e.title&&(e.title=It(e.title,e,t)),e.secondaryInfo&&(e.secondaryInfo=It(e.secondaryInfo,e,t)),e.icon=Rt(e.icon,e.batteryIds,t),e.iconColor=Mt(e.iconColor,e.batteryIds,t)})),n})(this.batteries,e,this.config.collapse,this.batteryProvider.groupsData);JSON.stringify(s.list)!=JSON.stringify(this.list)&&(this.list=s.list),JSON.stringify(s.groups)!=JSON.stringify(this.groups)&&(this.groups=s.groups)}))}render(){return 0==this.list.length&&0==this.groups.length?T``:wt(this)}getCardSize(){var t;let e=(null===(t=this.config.entities)||void 0===t?void 0:t.length)||1;return this.config.collapse?"number"==typeof this.config.collapse?this.config.collapse+1:this.config.collapse.length+1:e+1}}t([et({attribute:!1})],Ht.prototype,"header",void 0),t([et({attribute:!1})],Ht.prototype,"list",void 0),t([et({attribute:!1})],Ht.prototype,"groups",void 0);const zt=(t,e)=>{let i=Object.keys(e).map((t=>e[t]));switch(t.sort_by_level){case"asc":i=i.sort(((t,e)=>jt(t.state,e.state)));break;case"desc":i=i.sort(((t,e)=>jt(e.state,t.state)))}return i.map((t=>t.entityId))},jt=(t,e)=>{let i=Number(t),s=Number(e);return i=isNaN(i)?-1:i,s=isNaN(s)?-1:s,i-s};void 0===customElements.get("battery-state-entity")?(console.info("%c BATTERY-STATE-CARD %c 2.1.1","color: white; background: forestgreen; font-weight: 700;","color: forestgreen; background: white; font-weight: 700;"),customElements.define("battery-state-entity",ft),customElements.define("battery-state-card",Ht)):it("Element seems to be defined already","warn")}(); //# sourceMappingURL=battery-state-card.js.map diff --git a/www/battery-state-card.js.map b/www/battery-state-card.js.map new file mode 100644 index 00000000..55513cfb --- /dev/null +++ b/www/battery-state-card.js.map @@ -0,0 +1 @@ +{"version":3,"file":"battery-state-card.js","sources":["https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/tslib/tslib.es6.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/@lit/reactive-element/css-tag.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/@lit/reactive-element/reactive-element.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/lit-html/lit-html.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/lit-element/lit-element.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/node_modules/@lit/reactive-element/decorators/property.js","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/utils.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/custom-elements/battery-state-entity.views.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/custom-elements/lovelace-card.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/action.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/custom-elements/battery-state-entity.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/custom-elements/battery-state-card.views.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/battery-provider.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/grouping.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/custom-elements/battery-state-card.ts","https://raw.githubusercontent.com/maxwroc/battery-state-card/v2.1.1/src/index.ts"],"sourcesContent":null,"names":["__decorate","decorators","target","key","desc","d","c","arguments","length","r","Object","getOwnPropertyDescriptor","Reflect","decorate","i","defineProperty","__awaiter","thisArg","_arguments","P","generator","Promise","resolve","reject","fulfilled","value","step","next","e","rejected","result","done","then","apply","t","window","ShadowRoot","ShadyCSS","nativeShadow","Document","prototype","CSSStyleSheet","Symbol","n","Map","s","constructor","this","_$cssResult$","Error","cssText","styleSheet","get","set","replaceSync","toString","o","reduce","S","cssRules","trustedTypes","emptyScript","h","reactiveElementPolyfillSupport","toAttribute","Boolean","Array","JSON","stringify","fromAttribute","Number","parse","l","attribute","type","String","converter","reflect","hasChanged","a","HTMLElement","super","_$Et","isUpdatePending","hasUpdated","_$Ei","static","push","observedAttributes","finalize","elementProperties","forEach","_$Eh","_$Eu","state","noAccessor","hasOwnProperty","getPropertyDescriptor","requestUpdate","configurable","enumerable","finalized","getPrototypeOf","properties","getOwnPropertyNames","getOwnPropertySymbols","createProperty","elementStyles","finalizeStyles","styles","isArray","Set","flat","reverse","unshift","toLowerCase","_$Ep","enableUpdating","_$AL","_$Em","addController","_$Eg","renderRoot","isConnected","hostConnected","call","removeController","splice","indexOf","createRenderRoot","shadowRoot","attachShadow","shadowRootOptions","adoptedStyleSheets","map","document","createElement","litNonce","setAttribute","textContent","appendChild","connectedCallback","disconnectedCallback","hostDisconnected","attributeChangedCallback","_$AK","_$ES","removeAttribute","getPropertyOptions","has","_$E_","_$EC","async","scheduleUpdate","performUpdate","shouldUpdate","willUpdate","hostUpdate","update","_$EU","_$AE","hostUpdated","firstUpdated","updated","updateComplete","getUpdateComplete","mode","ReactiveElement","globalThis","reactiveElementVersions","createPolicy","createHTML","Math","random","slice","createComment","v","f","_","m","g","$","_$litType$","strings","values","p","b","for","w","T","WeakMap","A","createTreeWalker","C","u","lastIndex","exec","test","RegExp","y","startsWith","E","parts","el","currentNode","content","firstChild","remove","append","childNodes","nextNode","nodeType","hasAttributes","getAttributeNames","endsWith","getAttribute","split","index","name","ctor","M","H","I","tagName","data","innerHTML","_$Cl","_$Cu","_$litDirective$","_$AO","_$AT","_$AS","V","_$AN","_$AD","_$AM","parentNode","_$AU","creationScope","importNode","N","nextSibling","L","_$AI","_$AH","_$AA","_$AB","options","_$Cg","startNode","endNode","_$AR","iterator","insertBefore","createTextNode","_$AC","_$AP","setConnected","element","fill","k","capture","once","passive","removeEventListener","addEventListener","handleEvent","host","z","litHtmlPolyfillSupport","litHtmlVersions","renderOptions","_$Dt","renderBefore","render","_$litPart$","_$litElement$","litElementHydrateSupport","LitElement","litElementPolyfillSupport","litElementVersions","kind","descriptor","finisher","placement","originalKey","initializer","log","message","level","console","convertHexColorToRGB","color","replace","parseInt","substr","isNumber","isNaN","safeGetArray","val","safeGetConfigObject","propertyName","secondaryInfo","text","html","icon","batteryHtml","model","iconColor","hass","time","unit","LovelaceCard","func","throttleMs","timeoutHook","args","clearTimeout","setTimeout","throttledCall","internalUpdate","configUpdated","hassUpdated","updateNotifyQueue","_hass","triggerUpdate","cardUpdated","setConfig","config","nameToFuncMap","evt","Event","composed","detail","entityId","card","dispatchEvent","navigate","navigation_path","history","pushState","service","domain","serviceData","service_data","callService","url","url_path","location","href","stringValuePattern","htmlColorPattern","BatteryStateEntity","css","sharedStyles","getName","getBatteryLevel","fromCharCode","undefined","isCharging","getChargingState","getSecondaryInfo","getIcon","getIconColor","setupAction","enable","tap_action","action","stopPropagation","entity","classList","add","property","states","attributes","friendly_name","bulk_rename","from","to","secondary_info","charging_state","secondary_info_text","entityData","dateVal","Date","UnknownLevel","localize","value_override","candidates","battery_level","battery","find","state_map","convertedVal","match","multiplier","round","parseFloat","toFixed","charAt","toUpperCase","attribPrefix","attribName","roundedLevel","batteryLevel","defaultColor","color_gradient","isColorGradientValid","colors","pct","percentColors","colorBucket","lower","upper","range","rangePct","pctLower","pctUpper","floor","join","getColorInterpolationForPercentage","color_thresholds","th","chargingConfig","entityWithChargingState","entity_id","attributesLookup","exisitngAttrib","attr","getValueFromJsonPath","statesIndicatingCharging","some","gradientColors","path","chunk","cardHtml","header","list","id","batteryWrapper","batteries","groups","currentTarget","toggle","title","keys","batteryIds","collapsableWrapper","entititesGlobalProps","regExpPattern","operatorHandlers","exists","contains","searchString","expectedVal","matches","pattern","exp","regexpMatch","Filter","is_permanent","isValid","getValue","meetsExpectations","operator","BatteryProvider","include","filter","exclude","initialized","processExplicitEntities","processGroupEntities","processIncludes","processExcludes","all","getBatteries","createBattery","entityConfig","entities","groupsToResolve","collapse","group","group_id","entityConf","groupEntity","groupData","groupsData","filters","toBeRemoved","isHidden","getGroupIndex","haGroupData","findIndex","min","max","populateMinMaxFields","groupConfig","createGroup","icon_color","getEnrichedText","keyword","agg","batteryIdsInGroup","BatteryStateCard","batteryProvider","indexes","getIdsOfSortedBatteries","groupingResult","sortedIds","remainingBatteries","foundIndex","getBatteryGroups","getCardSize","size","batteriesToSort","sort_by_level","sort","compareBatteries","aNum","bNum","customElements","info","define"],"mappings":";;;;;;;;;;;;;;oFAsDO,SAASA,EAAWC,EAAYC,EAAQC,EAAKC,GAChD,IAA2HC,EAAvHC,EAAIC,UAAUC,OAAQC,EAAIH,EAAI,EAAIJ,EAAkB,OAATE,EAAgBA,EAAOM,OAAOC,yBAAyBT,EAAQC,GAAOC,EACrH,GAAuB,iBAAZQ,SAAoD,mBAArBA,QAAQC,SAAyBJ,EAAIG,QAAQC,SAASZ,EAAYC,EAAQC,EAAKC,QACpH,IAAK,IAAIU,EAAIb,EAAWO,OAAS,EAAGM,GAAK,EAAGA,KAAST,EAAIJ,EAAWa,MAAIL,GAAKH,EAAI,EAAID,EAAEI,GAAKH,EAAI,EAAID,EAAEH,EAAQC,EAAKM,GAAKJ,EAAEH,EAAQC,KAASM,GAChJ,OAAOH,EAAI,GAAKG,GAAKC,OAAOK,eAAeb,EAAQC,EAAKM,GAAIA,EAWzD,SAASO,EAAUC,EAASC,EAAYC,EAAGC,GAE9C,OAAO,IAAKD,IAAMA,EAAIE,WAAU,SAAUC,EAASC,GAC/C,SAASC,EAAUC,GAAS,IAAMC,EAAKN,EAAUO,KAAKF,IAAW,MAAOG,GAAKL,EAAOK,IACpF,SAASC,EAASJ,GAAS,IAAMC,EAAKN,EAAiB,MAAEK,IAAW,MAAOG,GAAKL,EAAOK,IACvF,SAASF,EAAKI,GAJlB,IAAeL,EAIaK,EAAOC,KAAOT,EAAQQ,EAAOL,QAJ1CA,EAIyDK,EAAOL,MAJhDA,aAAiBN,EAAIM,EAAQ,IAAIN,GAAE,SAAUG,GAAWA,EAAQG,OAITO,KAAKR,EAAWK,GAClGH,GAAMN,EAAYA,EAAUa,MAAMhB,EAASC,GAAc,KAAKS;;;;;OCtEtE,MAAMO,EAAEC,OAAOC,kBAAa,IAASD,OAAOE,UAAUF,OAAOE,SAASC,eAAe,uBAAuBC,SAASC,WAAW,YAAYC,cAAcD,UAAUZ,EAAEc,SAASC,EAAE,IAAIC,IAAI,MAAMC,EAAEC,YAAYZ,EAAES,GAAG,GAAGI,KAAKC,cAAa,EAAGL,IAAIf,EAAE,MAAMqB,MAAM,qEAAqEF,KAAKG,QAAQhB,EAAMiB,iBAAa,IAAIvB,EAAEe,EAAES,IAAIL,KAAKG,SAAS,OAAOhB,QAAG,IAASN,IAAIe,EAAEU,IAAIN,KAAKG,QAAQtB,EAAE,IAAIa,eAAeb,EAAE0B,YAAYP,KAAKG,UAAUtB,EAAE2B,WAAW,OAAOR,KAAKG,SAAS,MAA8CzC,EAAE,CAACyB,KAAKS,KAAK,MAAMa,EAAE,IAAItB,EAAE1B,OAAO0B,EAAE,GAAGS,EAAEc,SAAS7B,EAAEe,EAAEE,IAAIjB,EAAE,CAACM,IAAI,IAAG,IAAKA,EAAEc,aAAa,OAAOd,EAAEgB,QAAQ,GAAG,iBAAiBhB,EAAE,OAAOA,EAAE,MAAMe,MAAM,mEAAmEf,EAAE,yFAA7J,CAAuPS,GAAGT,EAAEW,EAAE,IAAIX,EAAE,IAAI,OAAO,IAAIW,EAAEW,EAAE5B,IAAuP8B,EAAExB,EAAEA,GAAGA,EAAEA,GAAGA,aAAaO,cAAc,CAACP,IAAI,IAAIN,EAAE,GAAG,IAAI,MAAMe,KAAKT,EAAEyB,SAAS/B,GAAGe,EAAEO,QAAQ,MAA5sBhB,CAAAA,GAAG,IAAIW,EAAE,iBAAiBX,EAAEA,EAAEA,EAAE,GAAGN,GAAgrB4B,CAAE5B,IAA9D,CAAmEM,GAAGA;;;;;QCA3tC,IAAIW,EAAE,MAAMjB,EAAEO,OAAOyB,aAAanD,EAAEmB,EAAEA,EAAEiC,YAAY,GAAGC,EAAE3B,OAAO4B,+BAA+BP,EAAE,CAACQ,YAAY9B,EAAEpB,GAAG,OAAOA,GAAG,KAAKmD,QAAQ/B,EAAEA,EAAEzB,EAAE,KAAK,MAAM,KAAKC,OAAO,KAAKwD,MAAMhC,EAAE,MAAMA,EAAEA,EAAEiC,KAAKC,UAAUlC,GAAG,OAAOA,GAAGmC,cAAcnC,EAAEpB,GAAG,IAAI+B,EAAEX,EAAE,OAAOpB,GAAG,KAAKmD,QAAQpB,EAAE,OAAOX,EAAE,MAAM,KAAKoC,OAAOzB,EAAE,OAAOX,EAAE,KAAKoC,OAAOpC,GAAG,MAAM,KAAKxB,OAAO,KAAKwD,MAAM,IAAIrB,EAAEsB,KAAKI,MAAMrC,GAAG,MAAMA,GAAGW,EAAE,MAAM,OAAOA,IAAIF,EAAE,CAACT,EAAEpB,IAAIA,IAAIoB,IAAIpB,GAAGA,GAAGoB,GAAGA,GAAGsC,EAAE,CAACC,WAAU,EAAGC,KAAKC,OAAOC,UAAUpB,EAAEqB,SAAQ,EAAGC,WAAWnC,GAAG,MAAMoC,UAAUC,YAAYlC,cAAcmC,QAAQlC,KAAKmC,KAAK,IAAItC,IAAIG,KAAKoC,iBAAgB,EAAGpC,KAAKqC,YAAW,EAAGrC,KAAKsC,KAAK,KAAKtC,KAAKS,IAAI8B,sBAAsBpD,GAAG,IAAIpB,EAAE,QAAQA,EAAEiC,KAAKyB,SAAI,IAAS1D,IAAIiC,KAAKyB,EAAE,IAAIzB,KAAKyB,EAAEe,KAAKrD,GAAcsD,gCAAqBzC,KAAK0C,WAAW,MAAMvD,EAAE,GAAG,OAAOa,KAAK2C,kBAAkBC,UAAU7E,EAAE+B,KAAK,MAAMjB,EAAEmB,KAAK6C,KAAK/C,EAAE/B,QAAG,IAASc,IAAImB,KAAK8C,KAAKxC,IAAIzB,EAAEiB,GAAGX,EAAEqD,KAAK3D,OAAOM,EAAEoD,sBAAsBpD,EAAEpB,EAAE0D,GAAG,GAAG1D,EAAEgF,QAAQhF,EAAE2D,WAAU,GAAI1B,KAAK0C,WAAW1C,KAAK2C,kBAAkBrC,IAAInB,EAAEpB,IAAIA,EAAEiF,aAAahD,KAAKP,UAAUwD,eAAe9D,GAAG,CAAC,MAAMW,EAAE,iBAAiBX,EAAEQ,SAAS,KAAKR,EAAEN,EAAEmB,KAAKkD,sBAAsB/D,EAAEW,EAAE/B,QAAG,IAASc,GAAGlB,OAAOK,eAAegC,KAAKP,UAAUN,EAAEN,IAAI0D,6BAA6BpD,EAAEpB,EAAE+B,GAAG,MAAM,CAACO,MAAM,OAAOL,KAAKjC,IAAIuC,IAAIzB,GAAG,MAAMnB,EAAEsC,KAAKb,GAAGa,KAAKjC,GAAGc,EAAEmB,KAAKmD,cAAchE,EAAEzB,EAAEoC,IAAIsD,cAAa,EAAGC,YAAW,GAAId,0BAA0BpD,GAAG,OAAOa,KAAK2C,kBAAkBtC,IAAIlB,IAAIsC,EAAEc,kBAAkB,GAAGvC,KAAKiD,eAAe,aAAa,OAAM,EAAGjD,KAAKsD,WAAU,EAAG,MAAMnE,EAAExB,OAAO4F,eAAevD,MAAM,GAAGb,EAAEuD,WAAW1C,KAAK2C,kBAAkB,IAAI9C,IAAIV,EAAEwD,mBAAmB3C,KAAK8C,KAAK,IAAIjD,IAAIG,KAAKiD,eAAe,cAAc,CAAC,MAAM9D,EAAEa,KAAKwD,WAAWzF,EAAE,IAAIJ,OAAO8F,oBAAoBtE,MAAMxB,OAAO+F,sBAAsBvE,IAAI,IAAI,MAAMW,KAAK/B,EAAEiC,KAAK2D,eAAe7D,EAAEX,EAAEW,IAAI,OAAOE,KAAK4D,cAAc5D,KAAK6D,eAAe7D,KAAK8D,SAAQ,EAAGvB,sBAAsBxE,GAAG,MAAM+B,EAAE,GAAG,GAAGqB,MAAM4C,QAAQhG,GAAG,CAAC,MAAMc,EAAE,IAAImF,IAAIjG,EAAEkG,KAAK,EAAA,GAAKC,WAAW,IAAI,MAAMnG,KAAKc,EAAEiB,EAAEqE,QAAQhF,EAAEpB,cAAS,IAASA,GAAG+B,EAAE0C,KAAKrD,EAAEpB,IAAI,OAAO+B,EAAEyC,YAAYpD,EAAEpB,GAAG,MAAM+B,EAAE/B,EAAE2D,UAAU,OAAM,IAAK5B,OAAE,EAAO,iBAAiBA,EAAEA,EAAE,iBAAiBX,EAAEA,EAAEiF,mBAAc,EAAO3D,IAAI,IAAItB,EAAEa,KAAKqE,KAAK,IAAI/F,SAASa,GAAGa,KAAKsE,eAAenF,IAAIa,KAAKuE,KAAK,IAAI1E,IAAIG,KAAKwE,OAAOxE,KAAKmD,gBAAgB,QAAQhE,EAAEa,KAAKD,YAAY0B,SAAI,IAAStC,GAAGA,EAAEyD,SAASzD,GAAGA,EAAEa,QAAQyE,cAActF,GAAG,IAAIpB,EAAE+B,GAAG,QAAQ/B,EAAEiC,KAAK0E,YAAO,IAAS3G,EAAEA,EAAEiC,KAAK0E,KAAK,IAAIlC,KAAKrD,QAAG,IAASa,KAAK2E,YAAY3E,KAAK4E,cAAc,QAAQ9E,EAAEX,EAAE0F,qBAAgB,IAAS/E,GAAGA,EAAEgF,KAAK3F,IAAI4F,iBAAiB5F,GAAG,IAAIpB,EAAE,QAAQA,EAAEiC,KAAK0E,YAAO,IAAS3G,GAAGA,EAAEiH,OAAOhF,KAAK0E,KAAKO,QAAQ9F,KAAK,EAAE,GAAGqF,OAAOxE,KAAKD,YAAY4C,kBAAkBC,UAAUzD,EAAEpB,KAAKiC,KAAKiD,eAAelF,KAAKiC,KAAKmC,KAAK7B,IAAIvC,EAAEiC,KAAKjC,WAAWiC,KAAKjC,OAAOmH,mBAAmB,IAAI/F,EAAE,MAAMW,EAAE,QAAQX,EAAEa,KAAKmF,kBAAa,IAAShG,EAAEA,EAAEa,KAAKoF,aAAapF,KAAKD,YAAYsF,mBAAmB,MDAp6D,EAACxG,EAAEe,KAAKT,EAAEN,EAAEyG,mBAAmB1F,EAAE2F,KAAKpG,GAAGA,aAAaO,cAAcP,EAAEA,EAAEiB,aAAaR,EAAEgD,SAASzD,IAAI,MAAMS,EAAE4F,SAASC,cAAc,SAAS3F,EAAEV,OAAOsG,cAAS,IAAS5F,GAAGF,EAAE+F,aAAa,QAAQ7F,GAAGF,EAAEgG,YAAYzG,EAAEgB,QAAQtB,EAAEgH,YAAYjG,OCAisD7B,CAAE+B,EAAEE,KAAKD,YAAY6D,eAAe9D,EAAEgG,oBAAoB,IAAI3G,OAAE,IAASa,KAAK2E,aAAa3E,KAAK2E,WAAW3E,KAAKkF,oBAAoBlF,KAAKsE,gBAAe,GAAI,QAAQnF,EAAEa,KAAK0E,YAAO,IAASvF,GAAGA,EAAEyD,SAASzD,IAAI,IAAIpB,EAAE,OAAO,QAAQA,EAAEoB,EAAE0F,qBAAgB,IAAS9G,OAAE,EAAOA,EAAE+G,KAAK3F,MAAMmF,eAAenF,IAAI4G,uBAAuB,IAAI5G,EAAE,QAAQA,EAAEa,KAAK0E,YAAO,IAASvF,GAAGA,EAAEyD,SAASzD,IAAI,IAAIpB,EAAE,OAAO,QAAQA,EAAEoB,EAAE6G,wBAAmB,IAASjI,OAAE,EAAOA,EAAE+G,KAAK3F,MAAM8G,yBAAyB9G,EAAEpB,EAAE+B,GAAGE,KAAKkG,KAAK/G,EAAEW,GAAGqG,KAAKhH,EAAEpB,EAAE+B,EAAE2B,GAAG,IAAI5C,EAAEnB,EAAE,MAAMqD,EAAEf,KAAKD,YAAY8C,KAAK1D,EAAEW,GAAG,QAAG,IAASiB,IAAG,IAAKjB,EAAEgC,QAAQ,CAAC,MAAMlC,GAAG,QAAQlC,EAAE,QAAQmB,EAAEiB,EAAE+B,iBAAY,IAAShD,OAAE,EAAOA,EAAEoC,mBAAc,IAASvD,EAAEA,EAAE+C,EAAEQ,aAAalD,EAAE+B,EAAE6B,MAAM3B,KAAKsC,KAAKnD,EAAE,MAAMS,EAAEI,KAAKoG,gBAAgBrF,GAAGf,KAAK2F,aAAa5E,EAAEnB,GAAGI,KAAKsC,KAAK,MAAM4D,KAAK/G,EAAEpB,GAAG,IAAI+B,EAAEjB,EAAEnB,EAAE,MAAMqD,EAAEf,KAAKD,YAAYH,EAAEmB,EAAE+B,KAAKzC,IAAIlB,GAAG,QAAG,IAASS,GAAGI,KAAKsC,OAAO1C,EAAE,CAAC,MAAMT,EAAE4B,EAAEsF,mBAAmBzG,GAAG6B,EAAEtC,EAAE0C,UAAUG,EAAE,QAAQtE,EAAE,QAAQmB,EAAE,QAAQiB,EAAE2B,SAAI,IAAS3B,OAAE,EAAOA,EAAEwB,qBAAgB,IAASzC,EAAEA,EAAE,mBAAmB4C,EAAEA,EAAE,YAAO,IAAS/D,EAAEA,EAAE+C,EAAEa,cAActB,KAAKsC,KAAK1C,EAAEI,KAAKJ,GAAGoC,EAAEjE,EAAEoB,EAAEwC,MAAM3B,KAAKsC,KAAK,MAAMa,cAAchE,EAAEpB,EAAE+B,GAAG,IAAIjB,GAAE,OAAG,IAASM,MAAMW,EAAEA,GAAGE,KAAKD,YAAYsG,mBAAmBlH,IAAI4C,YAAYnC,GAAGI,KAAKb,GAAGpB,IAAIiC,KAAKuE,KAAK+B,IAAInH,IAAIa,KAAKuE,KAAKjE,IAAInB,EAAEpB,IAAG,IAAK+B,EAAEgC,SAAS9B,KAAKsC,OAAOnD,SAAI,IAASa,KAAKuG,OAAOvG,KAAKuG,KAAK,IAAI1G,KAAKG,KAAKuG,KAAKjG,IAAInB,EAAEW,KAAKjB,GAAE,IAAKmB,KAAKoC,iBAAiBvD,IAAImB,KAAKqE,KAAKrE,KAAKwG,QAAQC,aAAazG,KAAKoC,iBAAgB,EAAG,UAAUpC,KAAKqE,KAAK,MAAMlF,GAAGb,QAAQE,OAAOW,GAAG,MAAMA,EAAEa,KAAK0G,iBAAiB,OAAO,MAAMvH,SAASA,GAAGa,KAAKoC,gBAAgBsE,iBAAiB,OAAO1G,KAAK2G,gBAAgBA,gBAAgB,IAAIxH,EAAE,IAAIa,KAAKoC,gBAAgB,OAAOpC,KAAKqC,WAAWrC,KAAKmC,OAAOnC,KAAKmC,KAAKS,UAAUzD,EAAEpB,IAAIiC,KAAKjC,GAAGoB,IAAIa,KAAKmC,UAAK,GAAQ,IAAIpE,GAAE,EAAG,MAAM+B,EAAEE,KAAKuE,KAAK,IAAIxG,EAAEiC,KAAK4G,aAAa9G,GAAG/B,GAAGiC,KAAK6G,WAAW/G,GAAG,QAAQX,EAAEa,KAAK0E,YAAO,IAASvF,GAAGA,EAAEyD,SAASzD,IAAI,IAAIpB,EAAE,OAAO,QAAQA,EAAEoB,EAAE2H,kBAAa,IAAS/I,OAAE,EAAOA,EAAE+G,KAAK3F,MAAMa,KAAK+G,OAAOjH,IAAIE,KAAKgH,OAAO,MAAM7H,GAAG,MAAMpB,GAAE,EAAGiC,KAAKgH,OAAO7H,EAAEpB,GAAGiC,KAAKiH,KAAKnH,GAAG+G,WAAW1H,IAAI8H,KAAK9H,GAAG,IAAIpB,EAAE,QAAQA,EAAEiC,KAAK0E,YAAO,IAAS3G,GAAGA,EAAE6E,SAASzD,IAAI,IAAIpB,EAAE,OAAO,QAAQA,EAAEoB,EAAE+H,mBAAc,IAASnJ,OAAE,EAAOA,EAAE+G,KAAK3F,MAAMa,KAAKqC,aAAarC,KAAKqC,YAAW,EAAGrC,KAAKmH,aAAahI,IAAIa,KAAKoH,QAAQjI,GAAG6H,OAAOhH,KAAKuE,KAAK,IAAI1E,IAAIG,KAAKoC,iBAAgB,EAAOiF,qBAAiB,OAAOrH,KAAKsH,oBAAoBA,oBAAoB,OAAOtH,KAAKqE,KAAKuC,aAAazH,GAAG,OAAM,EAAG4H,OAAO5H,QAAG,IAASa,KAAKuG,OAAOvG,KAAKuG,KAAK3D,UAAUzD,EAAEpB,IAAIiC,KAAKmG,KAAKpI,EAAEiC,KAAKjC,GAAGoB,KAAKa,KAAKuG,UAAK,GAAQvG,KAAKgH,OAAOI,QAAQjI,IAAIgI,aAAahI;;;;;;ACApyK,IAAIA,EDAqyK6C,EAAEsB,WAAU,EAAGtB,EAAEW,kBAAkB,IAAI9C,IAAImC,EAAE4B,cAAc,GAAG5B,EAAEqD,kBAAkB,CAACkC,KAAK,QAAQ,MAAMxG,GAAGA,EAAE,CAACyG,gBAAgBxF,KAAK,QAAQlC,EAAE2H,WAAWC,+BAA0B,IAAS5H,EAAEA,EAAE2H,WAAWC,wBAAwB,IAAIlF,KAAK,SCAvgL,MAACzE,EAAE0J,WAAW5G,aAAaf,EAAE/B,EAAEA,EAAE4J,aAAa,WAAW,CAACC,WAAWzI,GAAGA,SAAI,EAAON,EAAE,QAAQgJ,KAAKC,SAAS,IAAIC,MAAM,MAAMtH,EAAE,IAAI5B,EAAEe,EAAE,IAAIa,KAAKgB,EAAE+D,SAASzE,EAAE,CAAC5B,EAAE,KAAKsC,EAAEuG,cAAc7I,GAAGzB,EAAEyB,GAAG,OAAOA,GAAG,iBAAiBA,GAAG,mBAAmBA,EAAE7B,EAAE6D,MAAM4C,QAAyGxG,EAAE,sDAAsD0K,EAAE,OAAOjG,EAAE,KAAKkG,EAAE,oFAAoFC,EAAE,KAAKC,EAAE,KAAKC,EAAE,+BAAkFC,EAAjDnJ,CAAAA,GAAG,CAACpB,KAAK+B,MAAMyI,WAAWpJ,EAAEqJ,QAAQzK,EAAE0K,OAAO3I,IAAM4I,CAAE,GAAUC,EAAEhJ,OAAOiJ,IAAI,gBAAgBC,EAAElJ,OAAOiJ,IAAI,eAAeE,EAAE,IAAIC,QAAyRC,EAAEvH,EAAEwH,iBAAiBxH,EAAE,IAAI,MAAK,GAAIyH,EAAE,CAAC/J,EAAEpB,KAAK,MAAM0C,EAAEtB,EAAE1B,OAAO,EAAEgE,EAAE,GAAG,IAAIV,EAAErD,EAAE,IAAIK,EAAE,QAAQ,GAAGT,EAAEC,EAAE,IAAI,IAAIQ,EAAE,EAAEA,EAAE0C,EAAE1C,IAAI,CAAC,MAAM+B,EAAEX,EAAEpB,GAAG,IAAI0C,EAAE0I,EAAET,GAAG,EAAEJ,EAAE,EAAE,KAAKA,EAAExI,EAAErC,SAASH,EAAE8L,UAAUd,EAAEa,EAAE7L,EAAE+L,KAAKvJ,GAAG,OAAOqJ,IAAIb,EAAEhL,EAAE8L,UAAU9L,IAAIC,EAAE,QAAQ4L,EAAE,GAAG7L,EAAE2K,OAAE,IAASkB,EAAE,GAAG7L,EAAE0E,OAAE,IAASmH,EAAE,IAAId,EAAEiB,KAAKH,EAAE,MAAMpI,EAAEwI,OAAO,KAAKJ,EAAE,GAAG,MAAM7L,EAAE4K,QAAG,IAASiB,EAAE,KAAK7L,EAAE4K,GAAG5K,IAAI4K,EAAE,MAAMiB,EAAE,IAAI7L,EAAE,MAAMyD,EAAEA,EAAExD,EAAEmL,GAAG,QAAG,IAASS,EAAE,GAAGT,GAAG,GAAGA,EAAEpL,EAAE8L,UAAUD,EAAE,GAAG1L,OAAOgD,EAAE0I,EAAE,GAAG7L,OAAE,IAAS6L,EAAE,GAAGjB,EAAE,MAAMiB,EAAE,GAAGf,EAAED,GAAG7K,IAAI8K,GAAG9K,IAAI6K,EAAE7K,EAAE4K,EAAE5K,IAAI2K,GAAG3K,IAAI0E,EAAE1E,EAAEC,GAAGD,EAAE4K,EAAEnH,OAAE,GAAQ,MAAMyI,EAAElM,IAAI4K,GAAG/I,EAAEpB,EAAE,GAAG0L,WAAW,MAAM,IAAI,GAAG/L,GAAGJ,IAAIC,EAAEuC,EAAEF,EAAE8I,GAAG,GAAGjH,EAAEe,KAAK/B,GAAGX,EAAEiI,MAAM,EAAEW,GAAG,QAAQ5I,EAAEiI,MAAMW,GAAG7J,EAAE2K,GAAG1J,EAAEjB,IAAI,IAAI6J,GAAGjH,EAAEe,UAAK,GAAQzE,GAAGyL,GAAG,MAAML,EAAEzL,GAAGyB,EAAEsB,IAAI,QAAQ,IAAI1C,EAAE,SAAS,IAAI,IAAIoD,MAAM4C,QAAQ5E,KAAKA,EAAE8D,eAAe,OAAO,MAAM/C,MAAM,kCAAkC,MAAM,MAAC,IAASJ,EAAEA,EAAE8H,WAAWuB,GAAGA,EAAE1H,IAAI,MAAMiI,EAAE3J,aAAayI,QAAQrJ,EAAEoJ,WAAWzI,GAAGF,GAAG,IAAI6B,EAAEzB,KAAK2J,MAAM,GAAG,IAAIjM,EAAE,EAAEJ,EAAE,EAAE,MAAM6L,EAAEhK,EAAE1B,OAAO,EAAEF,EAAEyC,KAAK2J,OAAO1B,EAAEjG,GAAGkH,EAAE/J,EAAEW,GAAG,GAAGE,KAAK4J,GAAGF,EAAEjE,cAAcwC,EAAErI,GAAGoJ,EAAEa,YAAY7J,KAAK4J,GAAGE,QAAQ,IAAIhK,EAAE,CAAC,MAAMX,EAAEa,KAAK4J,GAAGE,QAAQ/L,EAAEoB,EAAE4K,WAAWhM,EAAEiM,SAAS7K,EAAE8K,UAAUlM,EAAEmM,YAAY,KAAK,QAAQzI,EAAEuH,EAAEmB,aAAa5M,EAAEE,OAAO0L,GAAG,CAAC,GAAG,IAAI1H,EAAE2I,SAAS,CAAC,GAAG3I,EAAE4I,gBAAgB,CAAC,MAAMlL,EAAE,GAAG,IAAI,MAAMpB,KAAK0D,EAAE6I,oBAAoB,GAAGvM,EAAEwM,SAAS,UAAUxM,EAAE0L,WAAW5K,GAAG,CAAC,MAAMiB,EAAEkC,EAAE1E,KAAK,GAAG6B,EAAEqD,KAAKzE,QAAG,IAAS+B,EAAE,CAAC,MAAMX,EAAEsC,EAAE+I,aAAa1K,EAAEsE,cAAc,SAASqG,MAAM5L,GAAGd,EAAE,eAAesL,KAAKvJ,GAAGvC,EAAEiF,KAAK,CAACb,KAAK,EAAE+I,MAAMhN,EAAEiN,KAAK5M,EAAE,GAAGyK,QAAQrJ,EAAEyL,KAAK,MAAM7M,EAAE,GAAG8M,EAAE,MAAM9M,EAAE,GAAG+M,EAAE,MAAM/M,EAAE,GAAGgN,EAAEpK,SAASpD,EAAEiF,KAAK,CAACb,KAAK,EAAE+I,MAAMhN,IAAI,IAAI,MAAMK,KAAKoB,EAAEsC,EAAE2E,gBAAgBrI,GAAG,GAAGsK,EAAEiB,KAAK7H,EAAEuJ,SAAS,CAAC,MAAM7L,EAAEsC,EAAEmE,YAAY6E,MAAM5L,GAAGiB,EAAEX,EAAE1B,OAAO,EAAE,GAAGqC,EAAE,EAAE,CAAC2B,EAAEmE,YAAY7H,EAAEA,EAAE+C,YAAY,GAAG,IAAI,IAAI/C,EAAE,EAAEA,EAAE+B,EAAE/B,IAAI0D,EAAEwI,OAAO9K,EAAEpB,GAAGgD,KAAKiI,EAAEmB,WAAW5M,EAAEiF,KAAK,CAACb,KAAK,EAAE+I,QAAQhN,IAAI+D,EAAEwI,OAAO9K,EAAEW,GAAGiB,YAAY,GAAG,IAAIU,EAAE2I,SAAS,GAAG3I,EAAEwJ,OAAOxK,EAAElD,EAAEiF,KAAK,CAACb,KAAK,EAAE+I,MAAMhN,QAAQ,CAAC,IAAIyB,GAAG,EAAE,MAAM,KAAKA,EAAEsC,EAAEwJ,KAAKhG,QAAQpG,EAAEM,EAAE,KAAK5B,EAAEiF,KAAK,CAACb,KAAK,EAAE+I,MAAMhN,IAAIyB,GAAGN,EAAEpB,OAAO,EAAEC,KAAK6E,qBAAqBpD,EAAEpB,GAAG,MAAM+B,EAAE2B,EAAEgE,cAAc,YAAY,OAAO3F,EAAEoL,UAAU/L,EAAEW,GAAG,SAAS1B,EAAEe,EAAEpB,EAAE+B,EAAEX,EAAEN,GAAG,IAAI4B,EAAEb,EAAE6B,EAAEV,EAAE,GAAGhD,IAAI4K,EAAE,OAAO5K,EAAE,IAAIT,OAAE,IAASuB,EAAE,QAAQ4B,EAAEX,EAAEqL,YAAO,IAAS1K,OAAE,EAAOA,EAAE5B,GAAGiB,EAAEsL,KAAK,MAAMjC,EAAEzL,EAAEK,QAAG,EAAOA,EAAEsN,gBAAgB,OAAO,MAAM/N,OAAE,EAAOA,EAAEyC,eAAeoJ,IAAI,QAAQvJ,EAAE,MAAMtC,OAAE,EAAOA,EAAEgO,YAAO,IAAS1L,GAAGA,EAAEkF,KAAKxH,GAAE,QAAI,IAAS6L,EAAE7L,OAAE,GAAQA,EAAE,IAAI6L,EAAEhK,GAAG7B,EAAEiO,KAAKpM,EAAEW,EAAEjB,SAAI,IAASA,GAAG,QAAQ4C,GAAGV,EAAEjB,GAAGqL,YAAO,IAAS1J,EAAEA,EAAEV,EAAEoK,KAAK,IAAItM,GAAGvB,EAAEwC,EAAEsL,KAAK9N,QAAG,IAASA,IAAIS,EAAEK,EAAEe,EAAE7B,EAAEkO,KAAKrM,EAAEpB,EAAE0K,QAAQnL,EAAEuB,IAAId,EAAE,MAAM0N,EAAE1L,YAAYZ,EAAEpB,GAAGiC,KAAKiI,EAAE,GAAGjI,KAAK0L,UAAK,EAAO1L,KAAK2L,KAAKxM,EAAEa,KAAK4L,KAAK7N,EAAM8N,iBAAa,OAAO7L,KAAK4L,KAAKC,WAAeC,WAAO,OAAO9L,KAAK4L,KAAKE,KAAKpD,EAAEvJ,GAAG,IAAIpB,EAAE,MAAM6L,IAAIE,QAAQhK,GAAG6J,MAAM9K,GAAGmB,KAAK2L,KAAKlL,GAAG,QAAQ1C,EAAE,MAAMoB,OAAE,EAAOA,EAAE4M,qBAAgB,IAAShO,EAAEA,EAAE0D,GAAGuK,WAAWlM,GAAE,GAAIkJ,EAAEa,YAAYpJ,EAAE,IAAIb,EAAEoJ,EAAEmB,WAAWpJ,EAAE,EAAErD,EAAE,EAAEJ,EAAEuB,EAAE,GAAG,UAAK,IAASvB,GAAG,CAAC,GAAGyD,IAAIzD,EAAEoN,MAAM,CAAC,IAAI3M,EAAE,IAAIT,EAAEqE,KAAK5D,EAAE,IAAIkO,EAAErM,EAAEA,EAAEsM,YAAYlM,KAAKb,GAAG,IAAI7B,EAAEqE,KAAK5D,EAAE,IAAIT,EAAEsN,KAAKhL,EAAEtC,EAAEqN,KAAKrN,EAAEkL,QAAQxI,KAAKb,GAAG,IAAI7B,EAAEqE,OAAO5D,EAAE,IAAIoO,EAAEvM,EAAEI,KAAKb,IAAIa,KAAKiI,EAAEzF,KAAKzE,GAAGT,EAAEuB,IAAInB,GAAGqD,KAAK,MAAMzD,OAAE,EAAOA,EAAEoN,SAAS9K,EAAEoJ,EAAEmB,WAAWpJ,KAAK,OAAON,EAAE2H,EAAEjJ,GAAG,IAAIpB,EAAE,EAAE,IAAI,MAAM+B,KAAKE,KAAKiI,OAAE,IAASnI,SAAI,IAASA,EAAE0I,SAAS1I,EAAEsM,KAAKjN,EAAEW,EAAE/B,GAAGA,GAAG+B,EAAE0I,QAAQ/K,OAAO,GAAGqC,EAAEsM,KAAKjN,EAAEpB,KAAKA,KAAK,MAAMkO,EAAElM,YAAYZ,EAAEpB,EAAE+B,EAAEjB,GAAG,IAAI4B,EAAET,KAAK2B,KAAK,EAAE3B,KAAKqM,KAAKxD,EAAE7I,KAAK0L,UAAK,EAAO1L,KAAKsM,KAAKnN,EAAEa,KAAKuM,KAAKxO,EAAEiC,KAAK4L,KAAK9L,EAAEE,KAAKwM,QAAQ3N,EAAEmB,KAAKyM,KAAK,QAAQhM,EAAE,MAAM5B,OAAE,EAAOA,EAAE+F,mBAAc,IAASnE,GAAGA,EAAMqL,WAAO,IAAI3M,EAAEpB,EAAE,OAAO,QAAQA,EAAE,QAAQoB,EAAEa,KAAK4L,YAAO,IAASzM,OAAE,EAAOA,EAAE2M,YAAO,IAAS/N,EAAEA,EAAEiC,KAAKyM,KAASZ,iBAAa,IAAI1M,EAAEa,KAAKsM,KAAKT,WAAW,MAAM9N,EAAEiC,KAAK4L,KAAK,YAAO,IAAS7N,GAAG,KAAKoB,EAAEiL,WAAWjL,EAAEpB,EAAE8N,YAAY1M,EAAMuN,gBAAY,OAAO1M,KAAKsM,KAASK,cAAU,OAAO3M,KAAKuM,KAAKH,KAAKjN,EAAEpB,EAAEiC,MAAMb,EAAEf,EAAE4B,KAAKb,EAAEpB,GAAGL,EAAEyB,GAAGA,IAAI0J,GAAG,MAAM1J,GAAG,KAAKA,GAAGa,KAAKqM,OAAOxD,GAAG7I,KAAK4M,OAAO5M,KAAKqM,KAAKxD,GAAG1J,IAAIa,KAAKqM,MAAMlN,IAAIwJ,GAAG3I,KAAKsI,EAAEnJ,QAAG,IAASA,EAAEoJ,WAAWvI,KAAK8I,EAAE3J,QAAG,IAASA,EAAEiL,SAASpK,KAAKW,EAAExB,GAA56IA,CAAAA,IAAI,IAAIpB,EAAE,OAAOT,EAAE6B,IAAI,mBAAmB,QAAQpB,EAAEoB,SAAI,IAASpB,OAAE,EAAOA,EAAE4B,OAAOkN,YAA41I1D,CAAEhK,GAAGa,KAAKgJ,EAAE7J,GAAGa,KAAKsI,EAAEnJ,GAAG0L,EAAE1L,EAAEpB,EAAEiC,KAAKuM,MAAM,OAAOvM,KAAKsM,KAAKT,WAAWiB,aAAa3N,EAAEpB,GAAG4C,EAAExB,GAAGa,KAAKqM,OAAOlN,IAAIa,KAAK4M,OAAO5M,KAAKqM,KAAKrM,KAAK6K,EAAE1L,IAAImJ,EAAEnJ,GAAGa,KAAKqM,OAAOxD,GAAGnL,EAAEsC,KAAKqM,MAAMrM,KAAKsM,KAAKJ,YAAYjB,KAAK9L,EAAEa,KAAKW,EAAEc,EAAEsL,eAAe5N,IAAIa,KAAKqM,KAAKlN,EAAE2J,EAAE3J,GAAG,IAAIpB,EAAE,MAAM0K,OAAO3I,EAAEyI,WAAW1J,GAAGM,EAAEsB,EAAE,iBAAiB5B,EAAEmB,KAAKgN,KAAK7N,SAAI,IAASN,EAAE+K,KAAK/K,EAAE+K,GAAGF,EAAEjE,cAAc5G,EAAEkC,EAAEf,KAAKwM,UAAU3N,GAAG,IAAI,QAAQd,EAAEiC,KAAKqM,YAAO,IAAStO,OAAE,EAAOA,EAAE4N,QAAQlL,EAAET,KAAKqM,KAAKjE,EAAEtI,OAAO,CAAC,MAAMX,EAAE,IAAIsM,EAAEhL,EAAET,MAAMjC,EAAEoB,EAAEuJ,EAAE1I,KAAKwM,SAASrN,EAAEiJ,EAAEtI,GAAGE,KAAKW,EAAE5C,GAAGiC,KAAKqM,KAAKlN,GAAG6N,KAAK7N,GAAG,IAAIpB,EAAE+K,EAAEzI,IAAIlB,EAAEqJ,SAAS,YAAO,IAASzK,GAAG+K,EAAExI,IAAInB,EAAEqJ,QAAQzK,EAAE,IAAI2L,EAAEvK,IAAIpB,EAAEiL,EAAE7J,GAAG7B,EAAE0C,KAAKqM,QAAQrM,KAAKqM,KAAK,GAAGrM,KAAK4M,QAAQ,MAAM7O,EAAEiC,KAAKqM,KAAK,IAAIvM,EAAEjB,EAAE,EAAE,IAAI,MAAM4B,KAAKtB,EAAEN,IAAId,EAAEN,OAAOM,EAAEyE,KAAK1C,EAAE,IAAImM,EAAEjM,KAAK6K,EAAE9J,KAAKf,KAAK6K,EAAE9J,KAAKf,KAAKA,KAAKwM,UAAU1M,EAAE/B,EAAEc,GAAGiB,EAAEsM,KAAK3L,GAAG5B,IAAIA,EAAEd,EAAEN,SAASuC,KAAK4M,KAAK9M,GAAGA,EAAEyM,KAAKL,YAAYrN,GAAGd,EAAEN,OAAOoB,GAAG+N,KAAKzN,EAAEa,KAAKsM,KAAKJ,YAAYnO,GAAG,IAAI+B,EAAE,IAAI,QAAQA,EAAEE,KAAKiN,YAAO,IAASnN,GAAGA,EAAEgF,KAAK9E,MAAK,GAAG,EAAGjC,GAAGoB,GAAGA,IAAIa,KAAKuM,MAAM,CAAC,MAAMxO,EAAEoB,EAAE+M,YAAY/M,EAAE6K,SAAS7K,EAAEpB,GAAGmP,aAAa/N,GAAG,IAAIpB,OAAE,IAASiC,KAAK4L,OAAO5L,KAAKyM,KAAKtN,EAAE,QAAQpB,EAAEiC,KAAKiN,YAAO,IAASlP,GAAGA,EAAE+G,KAAK9E,KAAKb,KAAK,MAAMwB,EAAEZ,YAAYZ,EAAEpB,EAAE+B,EAAEjB,EAAE4B,GAAGT,KAAK2B,KAAK,EAAE3B,KAAKqM,KAAKxD,EAAE7I,KAAK0L,UAAK,EAAO1L,KAAKmN,QAAQhO,EAAEa,KAAK2K,KAAK5M,EAAEiC,KAAK4L,KAAK/M,EAAEmB,KAAKwM,QAAQ/L,EAAEX,EAAErC,OAAO,GAAG,KAAKqC,EAAE,IAAI,KAAKA,EAAE,IAAIE,KAAKqM,KAAKlL,MAAMrB,EAAErC,OAAO,GAAG2P,KAAK,IAAIxL,QAAQ5B,KAAKwI,QAAQ1I,GAAGE,KAAKqM,KAAKxD,EAAMmC,cAAU,OAAOhL,KAAKmN,QAAQnC,QAAYc,WAAO,OAAO9L,KAAK4L,KAAKE,KAAKM,KAAKjN,EAAEpB,EAAEiC,KAAKF,EAAEjB,GAAG,MAAM4B,EAAET,KAAKwI,QAAQ,IAAI5I,GAAE,EAAG,QAAG,IAASa,EAAEtB,EAAEf,EAAE4B,KAAKb,EAAEpB,EAAE,GAAG6B,GAAGlC,EAAEyB,IAAIA,IAAIa,KAAKqM,MAAMlN,IAAIwJ,EAAE/I,IAAII,KAAKqM,KAAKlN,OAAO,CAAC,MAAMN,EAAEM,EAAE,IAAIsC,EAAEV,EAAE,IAAI5B,EAAEsB,EAAE,GAAGgB,EAAE,EAAEA,EAAEhB,EAAEhD,OAAO,EAAEgE,IAAIV,EAAE3C,EAAE4B,KAAKnB,EAAEiB,EAAE2B,GAAG1D,EAAE0D,GAAGV,IAAI4H,IAAI5H,EAAEf,KAAKqM,KAAK5K,IAAI7B,IAAIA,GAAGlC,EAAEqD,IAAIA,IAAIf,KAAKqM,KAAK5K,IAAIV,IAAI8H,EAAE1J,EAAE0J,EAAE1J,IAAI0J,IAAI1J,IAAI,MAAM4B,EAAEA,EAAE,IAAIN,EAAEgB,EAAE,IAAIzB,KAAKqM,KAAK5K,GAAGV,EAAEnB,IAAIf,GAAGmB,KAAKqN,EAAElO,GAAGkO,EAAElO,GAAGA,IAAI0J,EAAE7I,KAAKmN,QAAQ/G,gBAAgBpG,KAAK2K,MAAM3K,KAAKmN,QAAQxH,aAAa3F,KAAK2K,KAAK,MAAMxL,EAAEA,EAAE,KAAK,MAAM0L,UAAUlK,EAAEZ,cAAcmC,SAAS1E,WAAWwC,KAAK2B,KAAK,EAAE0L,EAAElO,GAAGa,KAAKmN,QAAQnN,KAAK2K,MAAMxL,IAAI0J,OAAE,EAAO1J,GAAG,MAAMkO,EAAEtP,EAAEA,EAAE+C,YAAY,GAAG,MAAMgK,UAAUnK,EAAEZ,cAAcmC,SAAS1E,WAAWwC,KAAK2B,KAAK,EAAE0L,EAAElO,GAAGA,GAAGA,IAAI0J,EAAE7I,KAAKmN,QAAQxH,aAAa3F,KAAK2K,KAAK0C,GAAGrN,KAAKmN,QAAQ/G,gBAAgBpG,KAAK2K,OAAO,MAAMI,UAAUpK,EAAEZ,YAAYZ,EAAEpB,EAAE+B,EAAEjB,EAAE4B,GAAGyB,MAAM/C,EAAEpB,EAAE+B,EAAEjB,EAAE4B,GAAGT,KAAK2B,KAAK,EAAEyK,KAAKjN,EAAEpB,EAAEiC,MAAM,IAAIF,EAAE,IAAIX,EAAE,QAAQW,EAAE1B,EAAE4B,KAAKb,EAAEpB,EAAE,UAAK,IAAS+B,EAAEA,EAAE+I,KAAKF,EAAE,OAAO,MAAM9J,EAAEmB,KAAKqM,KAAK5L,EAAEtB,IAAI0J,GAAGhK,IAAIgK,GAAG1J,EAAEmO,UAAUzO,EAAEyO,SAASnO,EAAEoO,OAAO1O,EAAE0O,MAAMpO,EAAEqO,UAAU3O,EAAE2O,QAAQ5N,EAAET,IAAI0J,IAAIhK,IAAIgK,GAAGpI,GAAGA,GAAGT,KAAKmN,QAAQM,oBAAoBzN,KAAK2K,KAAK3K,KAAKnB,GAAGe,GAAGI,KAAKmN,QAAQO,iBAAiB1N,KAAK2K,KAAK3K,KAAKb,GAAGa,KAAKqM,KAAKlN,EAAEwO,YAAYxO,GAAG,IAAIpB,EAAE+B,EAAE,mBAAmBE,KAAKqM,KAAKrM,KAAKqM,KAAKvH,KAAK,QAAQhF,EAAE,QAAQ/B,EAAEiC,KAAKwM,eAAU,IAASzO,OAAE,EAAOA,EAAE6P,YAAO,IAAS9N,EAAEA,EAAEE,KAAKmN,QAAQhO,GAAGa,KAAKqM,KAAKsB,YAAYxO,IAAI,MAAMgN,EAAEpM,YAAYZ,EAAEpB,EAAE+B,GAAGE,KAAKmN,QAAQhO,EAAEa,KAAK2B,KAAK,EAAE3B,KAAK0L,UAAK,EAAO1L,KAAK4L,KAAK7N,EAAEiC,KAAKwM,QAAQ1M,EAAMgM,WAAO,OAAO9L,KAAK4L,KAAKE,KAAKM,KAAKjN,GAAGf,EAAE4B,KAAKb,IAAS,MAAmE0O,EAAEzO,OAAO0O;;;;;;ACA1jP,IAAIrM,EAAEhB,EDA2kP,MAAMoN,GAAGA,EAAEnE,EAAEuC,IAAI,QAAQ9M,EAAEsI,WAAWsG,uBAAkB,IAAS5O,EAAEA,EAAEsI,WAAWsG,gBAAgB,IAAIvL,KAAK,SCAxqP,MAAM1C,UAAUX,EAAEY,cAAcmC,SAAS1E,WAAWwC,KAAKgO,cAAc,CAACJ,KAAK5N,MAAMA,KAAKiO,UAAK,EAAO/I,mBAAmB,IAAI/F,EAAEN,EAAE,MAAMd,EAAEmE,MAAMgD,mBAAmB,OAAO,QAAQ/F,GAAGN,EAAEmB,KAAKgO,eAAeE,oBAAe,IAAS/O,IAAIN,EAAEqP,aAAanQ,EAAEgM,YAAYhM,EAAEgJ,OAAO5H,GAAG,MAAMpB,EAAEiC,KAAKmO,SAASnO,KAAKqC,aAAarC,KAAKgO,cAAcpJ,YAAY5E,KAAK4E,aAAa1C,MAAM6E,OAAO5H,GAAGa,KAAKiO,KDAiT,EAAC9O,EAAEpB,EAAE+B,KAAK,IAAIjB,EAAE4B,EAAE,MAAMb,EAAE,QAAQf,EAAE,MAAMiB,OAAE,EAAOA,EAAEoO,oBAAe,IAASrP,EAAEA,EAAEd,EAAE,IAAI0D,EAAE7B,EAAEwO,WAAW,QAAG,IAAS3M,EAAE,CAAC,MAAMtC,EAAE,QAAQsB,EAAE,MAAMX,OAAE,EAAOA,EAAEoO,oBAAe,IAASzN,EAAEA,EAAE,KAAKb,EAAEwO,WAAW3M,EAAE,IAAIwK,EAAElO,EAAE+O,aAAa/L,IAAI5B,GAAGA,OAAE,EAAO,MAAMW,EAAEA,EAAE,IAAI,OAAO2B,EAAE2K,KAAKjN,GAAGsC,GCAxjB5C,CAAEd,EAAEiC,KAAK2E,WAAW3E,KAAKgO,eAAelI,oBAAoB,IAAI3G,EAAE+C,MAAM4D,oBAAoB,QAAQ3G,EAAEa,KAAKiO,YAAO,IAAS9O,GAAGA,EAAE+N,cAAa,GAAInH,uBAAuB,IAAI5G,EAAE+C,MAAM6D,uBAAuB,QAAQ5G,EAAEa,KAAKiO,YAAO,IAAS9O,GAAGA,EAAE+N,cAAa,GAAIiB,SAAS,OAAOpQ,GAAG+B,EAAEwD,WAAU,EAAGxD,EAAEuO,eAAc,EAAG,QAAQ5M,EAAEgG,WAAW6G,gCAA2B,IAAS7M,GAAGA,EAAEqD,KAAK2C,WAAW,CAAC8G,WAAWzO,IAAI,MAAMF,EAAE6H,WAAW+G,0BAA0B,MAAM5O,GAAGA,EAAE,CAAC2O,WAAWzO,KAA0D,QAAQW,EAAEgH,WAAWgH,0BAAqB,IAAShO,EAAEA,EAAEgH,WAAWgH,mBAAmB,IAAIjM,KAAK;;;;;;ACApgC,MAAMzE,GAAE,CAACA,EAAEc,IAAI,WAAWA,EAAE6P,MAAM7P,EAAE8P,cAAc,UAAU9P,EAAE8P,YAAY,IAAI9P,EAAE+P,SAAShP,GAAGA,EAAE+D,eAAe9E,EAAEzB,IAAIW,KAAK,CAAC2Q,KAAK,QAAQtR,IAAIuC,SAASkP,UAAU,MAAMF,WAAW,GAAGG,YAAYjQ,EAAEzB,IAAI2R,cAAc,mBAAmBlQ,EAAEkQ,cAAc/O,KAAKnB,EAAEzB,KAAKyB,EAAEkQ,YAAYjK,KAAK9E,QAAQ4O,SAAShP,GAAGA,EAAE+D,eAAe9E,EAAEzB,IAAIW,KAAK,SAASc,GAAEA,GAAG,MAAM,CAACe,EAAET,SAAI,IAASA,EAAE,EAAEpB,EAAEc,EAAEe,KAAKf,EAAEkB,YAAY4D,eAAe/D,EAAE7B,IAA1C,CAA+Cc,EAAEe,EAAET,GAAGpB,GAAEc,EAAEe,GCL5Z,MAWMoP,GAAM,CAACC,EAAiBC,EAA0B,UAC3DC,QAAQD,GAAO,wBAA0BD,IAQvCG,GAAwBC,IAC1BA,EAAQA,EAAMC,QAAQ,IAAK,IACpB,CACH5R,EAAG6R,SAASF,EAAMG,OAAO,EAAG,GAAI,IAChCnH,EAAGkH,SAASF,EAAMG,OAAO,EAAG,GAAI,IAChC7G,EAAG4G,SAASF,EAAMG,OAAO,EAAG,GAAI,MA8C3BC,GAAY/Q,GAEC,OAAVA,GAA4B,KAAVA,IAAiBgR,MAAMnO,OAAO7C,IAMnDiR,GAAmBC,GACxBzO,MAAM4C,QAAQ6L,GACPA,EAGJA,EAAM,CAACA,GAAO,GAQXC,GAAsB,CAAInR,EAAmBoR,KAEvD,cAAepR,GACX,IAAK,SACD,MAAMK,EAAc,GAEpB,OADAA,EAAO+Q,GAAgBpR,EAChBK,EACX,IAAK,SAED,wBAAYL,GAGpB,OAAOA,GCpGEqR,GAAiBC,GAAkBA,GAAQC,CAAI,0BACnCD,UASZE,GAAO,CAACA,EAAeb,IAAmBa,GAAQD,CAAI,2CAG3CZ,YACRa,sBAKHC,GAAeC,IAA8BH,OAAAA,CAAI,GAC5DC,GAAKE,EAAMF,KAAME,EAAMC,wCAEnBD,EAAMzF,QACyB,iBAAxByF,EAAmB,cAAgBL,GAAcK,EAAML,gBAnBzCO,EAmB4EF,EAAME,KAnBjDC,EAmBuDH,EAAML,cAnB7CQ,GAAQN,CAAI,mDAEzDK,iBAAoBC,2DAoB7CH,EAAMrN,QAAQqN,EAAMI,aAtBA,IAACF,EAAiCC,SCDtCE,WAA8BlC,EAApDxO,kCAeYC,uBAAoC,GAKpCA,oBAAgB,EAKhBA,kBAAc,EASdA,mBFuEiB,SAA8B0Q,EAASC,GAChE,IAAIC,EACJ,UAAkBC,KACVD,IAEAE,aAAaF,GACbA,EAAc,MAIlBA,EAAcG,YAAW,IAAML,EAAKxR,MAAM,KAAM2R,IAAO,MEjFnCG,EAAc,4CAC5BhR,KAAKiR,eAAejR,KAAKkR,cAAelR,KAAKmR,aACnDnR,KAAKkR,eAAgB,EACrBlR,KAAKmR,aAAc,EACnBnR,KAAKoR,kBAAkBxO,SAAQhD,GAAKA,MACpCI,KAAKoR,kBAAoB,QAMzBd,SAAKA,GACLtQ,KAAKqR,MAAQf,EACbtQ,KAAKmR,aAAc,EACnBnR,KAAKsR,gBAMLhB,WACA,OAAOtQ,KAAKqR,MAMZE,kBACA,OAAO,IAAIjT,SAAeC,GAAYyB,KAAKoR,kBAAkB5O,KAAKjE,KAOtEiT,UAAUC,GAENzR,KAAKyR,OAASrQ,KAAKI,MAAMJ,KAAKC,UAAUoQ,IACxCzR,KAAKkR,eAAgB,EACrBlR,KAAKsR,8bC7Eb,MAAMI,GAAiG,CAEnG,YAAczG,IACV,MAAM0G,EAAW,IAAIC,MAAM,iBAAkB,CAAEC,UAAU,IACzDF,EAAIG,OAAS,CAAEC,SAAU9G,EAAK8G,UAC9B9G,EAAK+G,KAAKC,cAAcN,IAG5BO,SAAajH,IACT,IAAKA,EAAKwG,OAAOU,gBAEb,YADAnD,GAAI,uDAIR5P,OAAOgT,QAAQC,UAAU,KAAM,GAAIpH,EAAKwG,OAAOU,iBAC/C,MAAMR,EAAW,IAAIC,MAAM,mBAAoB,CAAEC,UAAU,IAC3DF,EAAIG,OAAS,CAAExC,SAAS,GACxBlQ,OAAO6S,cAAcN,IAGzB,eAAgB,CAAC1G,EAAMqF,KACnB,IAAKrF,EAAKwG,OAAOa,QAEb,YADAtD,GAAI,mDAIR,MAAOuD,EAAQD,GAAWrH,EAAKwG,OAAOa,QAAQ7H,MAAM,IAAK,GACnD+H,mBAAmBvH,EAAKwG,OAAOgB,cACrCnC,EAAKoC,YAAYH,EAAQD,EAASE,IAGtCG,IAAO1H,IACEA,EAAKwG,OAAOmB,SAKjBxT,OAAOyT,SAASC,KAAO7H,EAAKwG,OAAOmB,SAJ/B5D,GAAI,6CCvBV+D,GAAqB,qBAKrBC,GAAmB,0BAKZC,WAA2BxC,GA+ClB3M,oBACd,OAAOoP,EAAS,CAACC,kEAGflC,0DACFjR,KAAK2K,KAAOyI,GAAQpT,KAAKyR,OAAQzR,KAAKsQ,MACtCtQ,KAAK+C,MAAQsQ,GAAgBrT,KAAKyR,OAAQzR,KAAKsQ,MAC3Cb,GAASzP,KAAK+C,OACd/C,KAAKwQ,KAAO5O,OAAO0R,aAAa,MAAQtT,KAAKyR,OAAOjB,MAAQ,KAG5DxQ,KAAKwQ,UAAO+C,EAGhB,MAAMC,EAAaC,GAAiBzT,KAAKyR,OAAQzR,KAAK+C,MAAO/C,KAAKsQ,MAClEtQ,KAAK+P,cAAgB2D,GAAiB1T,KAAKyR,OAAQzR,KAAKsQ,KAAMkD,GAC9DxT,KAAKkQ,KAAOyD,GAAQ3T,KAAKyR,OAAQlQ,OAAOvB,KAAK+C,OAAQyQ,EAAYxT,KAAKsQ,MACtEtQ,KAAKqQ,UAAYuD,GAAa5T,KAAKyR,OAAQzR,KAAK+C,MAAOyQ,MAG3D1N,oBACI5D,MAAM4D,oBAEN9F,KAAK6T,aAAY,GAGrB9N,uBACI7D,MAAM6D,uBAEN/F,KAAK6T,aAAY,GAGrB1F,SACI,OAAOgC,GAAYnQ,MAOf6T,YAAYC,GAAkB,GAC9BA,EACI9T,KAAKyR,OAAOsC,aAAe/T,KAAKgU,SAChChU,KAAKgU,OAASrC,IDrEF,IAAC1G,EAAmBqF,ECsE5BqB,EAAIsC,kBDtEKhJ,ECuEI,CACT+G,KAAMhS,KACNyR,OAAQ5B,GAAoB7P,KAAKyR,OAAOsC,WAAa,UACrDhC,SAAU/R,KAAKyR,OAAOyC,QD1EE5D,EC2EzBtQ,KAAKsQ,KD1EnBrF,EAAKwG,QAAqC,QAA3BxG,EAAKwG,OAAOuC,SAI1B/I,EAAKwG,OAAOuC,UAAUtC,GAK5BA,GAAczG,EAAKwG,OAAOuC,QAAQ/I,EAAMqF,GAJpCtB,GAAI,4BAA8B/D,EAAKwG,OAAOuC,UCwEtChU,KAAK0N,iBAAiB,QAAS1N,KAAKgU,QACpChU,KAAKmU,UAAUC,IAAI,cAInBpU,KAAKgU,SACLhU,KAAKmU,UAAUnK,OAAO,aACtBhK,KAAKyN,oBAAoB,QAASzN,KAAKgU,QACvChU,KAAKgU,YAAST,IArG1BtW,GADCoX,GAAS,CAAE3S,WAAW,iCAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,0CAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,kCAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,iCAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,iCAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,sCAOvBzE,GADCoX,GAAS,CAAE3S,WAAW,mCA8E3B,MAAM0R,GAAU,CAAC3B,EAA8BnB,WAC3C,GAAImB,EAAO9G,KACP,OAAO8G,EAAO9G,KAGlB,IAAK2F,EACD,OAAOmB,EAAOyC,OAGlB,IAAIvJ,aAAO2F,EAAKgE,OAAO7C,EAAOyC,8BAASK,WAAWC,gBAAiB/C,EAAOyC,OAa1E,OAXoBvE,GAAa8B,EAAOgD,aAC5B7R,SAAQlF,IAGZiN,EAFa,KAAbjN,EAAEgX,KAAK,IAA0C,KAA7BhX,EAAEgX,KAAKhX,EAAEgX,KAAKjX,OAAS,GAEpCkN,EAAK2E,QAAQ,IAAI/F,OAAO7L,EAAEgX,KAAKlF,OAAO,EAAG9R,EAAEgX,KAAKjX,OAAS,IAAKC,EAAEiX,IAAM,IAGtEhK,EAAK2E,QAAQ5R,EAAEgX,KAAMhX,EAAEiX,IAAM,OAIrChK,GAUL+I,GAAmB,CAACjC,EAA8BnB,EAAiCkD,WACrF,GAAI/B,EAAOmD,eAAgB,CACvB,GAA6B,YAAzBnD,EAAOmD,eACP,OAAIpB,aACO/B,EAAOoD,qCAAgBC,sBAAuB,WAG7C,KAEX,CACD,IAAIlF,EAAM6B,EAAOmD,eAEjB,MAAMG,EAAazE,MAAAA,SAAAA,EAAMgE,OAAO7C,EAAOyC,QACnCa,IACAnF,EAAYmF,EAAYtD,EAAOmD,kBAAmBG,MAAAA,SAAAA,EAAYR,WAAW9C,EAAOmD,kBAAmBnD,EAAOmD,gBAG9G,MAAMI,EAAUC,KAAKzT,MAAMoO,GAC3B,OAAOF,MAAMsF,GAAWpF,EAAM,IAAIqF,KAAKD,IAI/C,OAAY,MASV3B,GAAkB,CAAC5B,EAA8BnB,WACnD,MAAM4E,GAAe5E,MAAAA,SAAAA,EAAM6E,SAAS,2BAA4B,UAChE,IAAIjG,EAEJ,QAA8BqE,IAA1B9B,EAAO2D,eACP,OAAO3D,EAAO2D,eAGlB,MAAML,EAAazE,MAAAA,SAAAA,EAAMgE,OAAO7C,EAAOyC,QAEvC,IAAKa,EACD,OAAOG,EAGX,GAAIzD,EAAO/P,UACPwN,EAAQ6F,EAAWR,WAAW9C,EAAO/P,WACxB6R,MAATrE,IACAF,GAAI,cAAcyC,EAAO/P,gCAAgC+P,EAAOyC,kBAChEhF,EAAQgG,OAGX,CACD,MAAMG,EAAuB,CACzBN,EAAWR,WAAWe,cACtBP,EAAWR,WAAWgB,QACtBR,EAAWhS,OAGfmM,EAAQmG,EAAWG,MAAK5F,GAAOH,GAASG,iBAChCyF,EAAWG,MAAK5F,GAAOA,MAAAA,0BAAoCpP,aAC3D0U,EAIZ,GAAIzD,EAAOgE,UAAW,CAClB,MAAMC,EAAejE,EAAOgE,UAAUD,MAAK1V,GAAKA,EAAE4U,OAASxF,SACtCqE,IAAjBmC,EACKjG,GAASP,IACVF,GAAI,uBAAuBE,qBAI/BA,EAAQwG,EAAaf,GAAGnU,WAKhC,IAAKiP,GAASP,GAAQ,CAClB,MAAMyG,EAAQ5C,GAAmB1J,KAAK6F,GACzB,MAATyG,IACAzG,EAAQyG,EAAM,IAkBtB,OAdIlG,GAASP,IACLuC,EAAOmE,aACP1G,GAASuC,EAAOmE,WAAarU,OAAO2N,IAAQ1O,YAGpB,iBAAjBiR,EAAOoE,QACd3G,EAAQ4G,WAAW5G,GAAO6G,QAAQtE,EAAOoE,OAAOrV,aAKpD0O,EAAQA,EAAM8G,OAAO,GAAGC,cAAgB/G,EAAMnH,MAAM,GAGjDmH,GAWLyE,GAAU,CAAClC,EAA8BvC,EAAesE,EAAqBlD,WAC/E,GAAIkD,cAAc/B,EAAOoD,qCAAgB3E,MACrC,OAAOuB,EAAOoD,eAAe3E,KAGjC,GAAIuB,EAAOvB,KAAM,CACb,MAAMgG,EAAe,aACrB,GAAI5F,GAAQmB,EAAOvB,KAAKzG,WAAWyM,GAAe,CAC9C,MAAMC,EAAa1E,EAAOvB,KAAKV,OAAO0G,EAAazY,QAC7CmS,EAAMU,EAAKgE,OAAO7C,EAAOyC,QAAQK,WAAW4B,GAClD,OAAKvG,IACDZ,GAAI,8BAA8ByC,EAAOyC,iBAAkB,SACpDzC,EAAOvB,MAMtB,OAAOuB,EAAOvB,KAGlB,GAAIR,MAAMR,IAAUA,EAAQ,KAAOA,EAAQ,EACvC,MAAO,sBAGX,MAAMkH,EAAwC,GAAzBvO,KAAKgO,MAAM3G,EAAQ,IACxC,OAAQkH,GACJ,KAAK,IACD,OAAO5C,EAAa,2BAA6B,cACrD,KAAK,EACD,OAAOA,EAAa,+BAAiC,sBACzD,QACI,OAAQA,EAAa,wBAA0B,gBAAkB4C,IAWvExC,GAAe,CAACnC,EAA8B4E,EAAsB7C,aAEtE,MAAM8C,EAAe,UACfpH,EAAQ3N,OAAO8U,GAErB,GAAI7C,cAAc/B,EAAOoD,qCAAgBxF,OACrC,OAAOoC,EAAOoD,eAAexF,MAGjC,GAAIK,MAAMR,IAAUA,EAAQ,KAAOA,EAAQ,EACvC,OAAOoH,EAGX,GAAI7E,EAAO8E,gBAAkBC,GAAqB/E,EAAO8E,gBACrD,OJlT0C,SAAUE,EAAkBC,GAE1EA,GAAY,IAEZ,MAAMC,EAAgBF,EAAOlR,KAAI,CAAC8J,EAAO3E,KAC9B,CACHgM,IAAM,GAAKD,EAAOhZ,OAAS,GAAMiN,EACjC2E,MAAOD,GAAqBC,OAIpC,IAAIuH,EAAc,EAClB,IAAKA,EAAc,EAAGA,EAAcD,EAAclZ,OAAS,KACnDiZ,EAAMC,EAAcC,GAAaF,KADqBE,KAM9D,MAAMC,EAAQF,EAAcC,EAAc,GACpCE,EAAQH,EAAcC,GACtBG,EAAQD,EAAMJ,IAAMG,EAAMH,IAC1BM,GAAYN,EAAMG,EAAMH,KAAOK,EAC/BE,EAAW,EAAID,EACfE,EAAWF,EACX3H,EAAQ,CACV3R,EAAGmK,KAAKsP,MAAMN,EAAMxH,MAAM3R,EAAIuZ,EAAWH,EAAMzH,MAAM3R,EAAIwZ,GACzD7O,EAAGR,KAAKsP,MAAMN,EAAMxH,MAAMhH,EAAI4O,EAAWH,EAAMzH,MAAMhH,EAAI6O,GACzDvO,EAAGd,KAAKsP,MAAMN,EAAMxH,MAAM1G,EAAIsO,EAAWH,EAAMzH,MAAM1G,EAAIuO,IAE7D,MAAO,OAAS,CAAC7H,EAAM3R,EAAG2R,EAAMhH,EAAGgH,EAAM1G,GAAGyO,KAAK,KAAO,IIqR7CC,CAAmC5F,EAAO8E,eAAgBrH,GAMrE,kBAHmBuC,EAAO6F,kBACtB,CAAC,CAAE5Y,MAAO,GAAI2Q,MAAO,0BAA4B,CAAE3Q,MAAO,GAAI2Q,MAAO,6BAA+B,CAAE3Q,MAAO,IAAK2Q,MAAO,8BAE3GmG,MAAK+B,GAAMrI,GAASqI,EAAG7Y,8BAAQ2Q,QAASiH,GAUxD7C,GAAmB,CAAChC,EAA8B1O,EAAeuN,KACnE,MAAMkH,EAAiB/F,EAAOoD,eAC9B,IAAK2C,IAAmBlH,EACpB,OAAO,EAGX,IAAImH,EAA0BnH,EAAKgE,OAAO7C,EAAOyC,QAGjD,GAAIsD,EAAeE,UAAW,CAE1B,GADAD,EAA0BnH,EAAKgE,OAAOkD,EAAeE,YAChDD,EAED,OADAzI,GAAI,+BAA+BwI,EAAeE,yBAC3C,EAGX3U,EAAQ0U,EAAwB1U,MAGpC,MAAM4U,EAAmBhI,GAAa6H,EAAe9V,WAErD,GAA+B,GAA3BiW,EAAiBla,OAAa,CAE9B,MAAMma,EAAiBD,EAAiBnC,MAAKqC,QAAgFtE,IAAxEuE,GAAqBL,EAAwBlD,WAAYsD,EAAKlN,QACnH,QAAIiN,SACgCrE,IAAzBqE,EAAelZ,OAClBoZ,GAAqBL,EAAwBlD,WAAYqD,EAAejN,OAASiN,EAAelZ,OAS5G,MAAMqZ,EAA2BpI,GAAa6H,EAAezU,OAE7D,OAA0C,GAAnCgV,EAAyBta,SAAgBsF,EAAQgV,EAAyBC,MAAKlY,GAAKA,GAAKiD,KAQ9FyT,GAAwByB,IAC1B,KAAIA,EAAexa,OAAS,GAA5B,CAKA,IAAK,MAAM4R,KAAS4I,EAChB,IAAKjF,GAAiB1J,KAAK+F,GAEvB,OADAL,GAAI,0FACG,EAIf,OAAO,EAXHA,GAAI,0EAoBN8I,GAAuB,CAAC7M,EAAWiN,UACxB3E,IAATtI,GAIJiN,EAAKzN,MAAM,KAAK7H,SAAQuV,IACpBlN,EAAOA,EAAOA,EAAKkN,QAAS5E,KAJrBtI,GCtYFmN,GAAYhI,IAA4BH,OAAAA,CAAI,YA7BzCD,EA+BHI,EAAMiI,OA/B0BrI,GAAQC,CAAI,kDAG/CD,4CA8BAI,EAAMkI,KAAK/S,KAAIgT,GAAMC,GAAepI,EAAMqI,UAAUF,SACpDnI,EAAMsI,OAAOnT,KAAI8C,GAzBO,EAAC+H,EAAsBqI,KACzB5Q,KAAKC,SAAStH,WAAWgP,OAAO,GACrDS,CAAI,0EAEoBpR,GAAgCA,EAAE8Z,cAAexE,UAAUyE,OAAO,gBAC3F1I,GAAKE,EAAMF,KAAME,EAAMC,wCAEnBD,EAAMyI,SACN9I,GAAcK,EAAML,gFAI4B,GAAhCpS,OAAOmb,KAAKL,GAAWhb,WAC3C2S,EAAM2I,WAAWxT,KAAIgT,GAAMC,GAAeC,EAAUF,qBAY9BS,CAAmB3Q,EAAG+H,EAAMqI,+BAlC7C,IAACzI,GAuCVwI,GAAkBjD,GAAgCtF,CAAI,+BAEtDsF,UCzCA0D,GAAuB,CAAE,aAAc,YAAa,iBAAkB,iBAAkB,mBAAoB,iBAAkB,cAAe,OAAQ,QAAS,QAE9JC,GAAgB,yBAKhBC,GAA6H,CAC/HC,OAAUxJ,QAAe2D,IAAR3D,EACjByJ,SAAY,CAACzJ,EAAK0J,SAAyB/F,IAAR3D,IAAyE,GAApDA,EAAIpP,WAAWyE,QAAQqU,EAAa9Y,YAC5F,IAAK,CAACoP,EAAK2J,IAAgB3J,GAAO2J,EAClC,IAAK,CAAC3J,EAAK2J,IAAgBhY,OAAOqO,GAAO2J,EACzC,IAAK,CAAC3J,EAAK2J,IAAgBhY,OAAOqO,GAAO2J,EACzC,KAAM,CAAC3J,EAAK2J,IAAgBhY,OAAOqO,IAAQ2J,EAC3C,KAAM,CAAC3J,EAAK2J,IAAgBhY,OAAOqO,IAAQ2J,EAC3CC,QAAW,CAAC5J,EAAK6J,KACb,QAAYlG,IAAR3D,EACA,OAAO,EAKX,IAAI8J,EACJ,MAAMC,GAHNF,EAAUA,EAAQjZ,YAGUmV,MAAMuD,IAQlC,OAPIS,EAEAD,EAAM,IAAInQ,OAAOoQ,EAAY,GAAIA,EAAY,KACb,GAAzBF,EAAQxU,QAAQ,OACvByU,EAAM,IAAInQ,OAAO,IAAMkQ,EAAQnK,QAAQ,MAAO,MAAQ,MAGnDoK,EAAMA,EAAIpQ,KAAKsG,EAAIpP,YAAcoP,IAAQ6J,IAOxD,MAAMG,GAYF7Z,YAAoB0R,GAAAzR,YAAAyR,EAJhBoI,mBACA,MAA2B,SAApB7Z,KAAKyR,OAAO9G,KAYvBmP,QAAQ5F,EAAanR,GACjB,MAAM6M,EAAM5P,KAAK+Z,SAAS7F,EAAQnR,GAClC,OAAO/C,KAAKga,kBAAkBpK,GAQ1BmK,SAAS7F,EAAanR,GAC1B,GAAK/C,KAAKyR,OAAO9G,KAKjB,OAA+C,GAA3C3K,KAAKyR,OAAO9G,KAAK1F,QAAQ,eAClBiP,EAAOK,WAAWvU,KAAKyR,OAAO9G,KAAK6E,OAAO,KAG7B,SAApBxP,KAAKyR,OAAO9G,WAA6B4I,IAAVxQ,EACxBA,EAGEmR,EAAQlU,KAAKyR,OAAO9G,MAZ7BqE,GAAI,kCAmBJgL,kBAAkBpK,GAEtB,IAAIqK,EAAWja,KAAKyR,OAAOwI,SAC3B,IAAKA,EACD,QAA0B1G,IAAtBvT,KAAKyR,OAAO/S,MACZub,EAAW,aAEV,CACD,MAAMV,EAAcvZ,KAAKyR,OAAO/S,MAAM8B,WACtCyZ,GAAwC,GAA7BV,EAAYtU,QAAQ,MAAiC,KAAlBsU,EAAY,IAAoD,KAAvCA,EAAYA,EAAY9b,OAAS,GACpG,UACA,IAIZ,MAAMiT,EAAOyI,GAAiBc,GAC9B,OAAKvJ,EAKEA,EAAKd,EAAK5P,KAAKyR,OAAO/S,QAJzBsQ,GAAI,aAAahP,KAAKyR,OAAOwI,iDAAiDtc,OAAOmb,KAAKK,IAAkB/B,KAAK,UAC1G,UAUN8C,GA6BTna,YAAoB0R,eAAAzR,YAAAyR,EAjBZzR,eAAgC,GAKhCA,qBAA4B,GAK7BA,gBAA4B,GAK3BA,kBAAuB,EAG3BA,KAAKma,4BAAU1I,EAAO2I,6BAAQD,8BAAS5U,KAAI2C,GAAK,IAAI0R,GAAO1R,KAC3DlI,KAAKqa,4BAAU5I,EAAO2I,6BAAQC,8BAAS9U,KAAI2C,GAAK,IAAI0R,GAAO1R,KAEtDlI,KAAKma,UACNna,KAAKsa,aAAc,GAGvBta,KAAKua,0BAGHxT,OAAOuJ,4CACJtQ,KAAKsa,cAENta,KAAKsa,aAAc,EACnBta,KAAKwa,qBAAqBlK,GAC1BtQ,KAAKya,gBAAgBnK,IAGzBtQ,KAAK0a,gBAAgBpK,GAErB,MAAMjJ,EAAiB1J,OAAOmb,KAAK9Y,KAAKyY,WAAWlT,KAAIgT,IACnD,MAAMhD,EAAUvV,KAAKyY,UAAUF,GAE/B,OADAhD,EAAQjF,KAAOA,EACRiF,EAAQhE,qBAGbjT,QAAQqc,IAAItT,MAOtBuT,eACI,OAAO5a,KAAKyY,UAMRoC,cAAcC,GAElB7B,GACKmB,QAAO1R,GAA+B6K,MAApBuH,EAAcpS,KAChC9F,SAAQ8F,GAAWoS,EAAcpS,GAAW1I,KAAKyR,OAAQ/I,KAE9D,MAAM6M,EAAkC,IAAItC,GAI5C,OAHAsC,EAAQxD,SAAW+I,EAAa5G,OAChCqB,EAAQ/D,UAAUsJ,GAEXvF,EAMHgF,0BACJ,IAAIQ,GAAY/a,KAAKyR,OAAOsJ,UAAY,IAAIxV,KAAK2O,IAEjB,qBACpBA,EAA+B,CAAEA,OAAQA,IAGtCA,KAIf6G,EAAWA,EAASX,QAAOvb,IACvB,IAAKA,EAAEqV,OACH,MAAM,IAAIhU,MAAM,0DAA4DkB,KAAKC,UAAUxC,IAG/F,OAAIA,EAAEqV,OAAOzK,WAAW,YACpBzJ,KAAKgb,gBAAgBxY,KAAK3D,EAAEqV,SACrB,MAQXlU,KAAKyR,OAAOwJ,UAAY9Z,MAAM4C,QAAQ/D,KAAKyR,OAAOwJ,WAClDjb,KAAKyR,OAAOwJ,SAASrY,SAAQsY,IACrBA,EAAMC,UAE+C,GAAjDnb,KAAKgb,gBAAgB/V,QAAQiW,EAAMC,WACnCnb,KAAKgb,gBAAgBxY,KAAK0Y,EAAMC,UAG/BD,EAAMH,UACXG,EAAMH,SAASnY,SAAQ8U,IAEdqD,EAAS/C,MAAKnZ,GAAKA,EAAEqV,QAAUwD,KAChCqD,EAASvY,KAAK,CAAE0R,OAAQwD,UAO5CqD,EAASnY,SAAQwY,IACbpb,KAAKyY,UAAU2C,EAAWlH,QAAUlU,KAAK6a,cAAcO,MAQvDX,gBAAgBnK,GACftQ,KAAKma,SAIVxc,OAAOmb,KAAKxI,EAAKgE,QAAQ1R,SAAQmP,qBAEzB/R,KAAKma,8BAASnC,MAAKoC,GAAUA,EAAON,QAAQxJ,EAAKgE,OAAOvC,SAEvD/R,KAAKyY,UAAU1G,KAEhB/R,KAAKyY,UAAU1G,GAAY/R,KAAK6a,cAAc,CAAE3G,OAAQnC,QAS5DyI,qBAAqBlK,GACzBtQ,KAAKgb,gBAAgBpY,SAAQuY,IACzB,MAAME,EAAc/K,EAAKgE,OAAO6G,GAChC,IAAKE,EAED,YADArM,GAAI,UAAUmM,gBAIlB,MAAMG,EAAYD,EAAY9G,WACzBpT,MAAM4C,QAAQuX,EAAU5D,YAK7B4D,EAAU5D,UAAU9U,SAAQ8U,IAEpB1X,KAAKyY,UAAUf,KAInB1X,KAAKyY,UAAUf,GAAa1X,KAAK6a,cAAc,CAAE3G,OAAQwD,QAG7D1X,KAAKub,WAAWJ,GAAYG,GAbxBtM,GAAI,0BAA0BmM,SAgBtCnb,KAAKgb,gBAAkB,GAOnBN,gBAAgBpK,GACpB,GAAoBiD,MAAhBvT,KAAKqa,QACL,OAGJ,MAAMmB,EAAUxb,KAAKqa,QACfoB,EAAwB,GAI9B9d,OAAOmb,KAAK9Y,KAAKyY,WAAW7V,SAASmP,IACjC,MAAMwD,EAAUvV,KAAKyY,UAAU1G,GAC/B,IAAI2J,GAAW,EACf,IAAK,IAAItB,KAAUoB,EAEf,GAAIpB,EAAON,QAAQxJ,EAAKgE,OAAOvC,GAAWwD,EAAQxS,OAAQ,CACtD,GAAIqX,EAAOP,aAAc,CAGrB4B,EAAYjZ,KAAKuP,GAEjB,MAGA2J,GAAW,EAOvBnG,EAAQmG,SAAWA,KAGvBD,EAAY7Y,SAAQmP,UAAmB/R,KAAKyY,UAAU1G,MC5UvD,MA0DD4J,GAAgB,CAAClK,EAAwB8D,EAAiCqG,IACrEnK,EAAOoK,WAAUX,YAEpB,GAAIA,EAAMC,gCAAaS,EAAYV,EAAMC,gCAAWzD,gCAAWM,MAAKO,GAAMhD,EAAQxD,UAAYwG,KAC1F,OAAO,EAGX,GAAI2C,EAAMH,WAAaG,EAAMH,SAAS/C,MAAKO,GAAMhD,EAAQxD,UAAYwG,IACjE,OAAO,EAGX,MAAMrJ,EAAQQ,MAAMnO,OAAOgU,EAAQxS,QAAU,EAAIxB,OAAOgU,EAAQxS,OAEhE,OAAOmM,GAASgM,EAAMY,KAAQ5M,GAASgM,EAAMa,OAQrD,IAAIC,GAAwBvK,GAAiCA,EAAO7O,SAAQqZ,IACjD1I,MAAnB0I,EAAYH,MACZG,EAAYH,IAAM,GAGCvI,MAAnB0I,EAAYF,KAAoBE,EAAYF,IAAME,EAAYH,IAC9D9M,GAAI,uDAAyD5N,KAAKC,UAAU4a,EAAa,KAAM,IAI5E1I,MAAnB0I,EAAYF,MACZE,EAAYF,IAAM,QAU1B,MAAMG,GAAc,CAACN,EAA4B7C,EAAsBtH,KAEnE,IAAIA,MAAAA,SAAAA,EAAQ0J,YAAaS,EAAYnK,EAAO0J,UACxC,MAAM,IAAIjb,MAAM,oBAAsBuR,EAAO0J,UAGjD,IAAIxQ,EAAO8G,MAAAA,SAAAA,EAAQ9G,MACdA,IAAQ8G,MAAAA,SAAAA,EAAQ0J,YACjBxQ,EAAOiR,EAAYnK,EAAO0J,UAAU3G,eAGxC,IAAItE,EAAOuB,MAAAA,SAAAA,EAAQvB,KAKnB,YAJaqD,IAATrD,IAAsBuB,MAAAA,SAAAA,EAAQ0J,YAC9BjL,EAAO0L,EAAYnK,EAAO0J,UAAUjL,MAGjC,CACH2I,MAAOlO,EACPuF,KAAMA,EACNG,UAAWoB,MAAAA,SAAAA,EAAQ0K,WACnBpD,WAAYA,EACZhJ,cAAe0B,MAAAA,SAAAA,EAAQmD,iBASzBwH,GAAkB,CAACpM,EAAckL,EAAsBzC,IACzDzI,EAAOA,EAAKV,QAAQ,eAAe+M,IAC/B,OAAQA,GACJ,IAAK,QACD,OAAOnB,EAAMnC,WAAWrY,QAAO,CAAC4b,EAAK/D,IAAO+D,EAAM/a,OAAOkX,EAAUF,GAAIxV,OAASxB,OAAOkX,EAAUF,GAAIxV,OAASuZ,GAAK,KAAK9b,WAC5H,IAAK,QACD,OAAO0a,EAAMnC,WAAWrY,QAAO,CAAC4b,EAAK/D,IAAO+D,EAAM/a,OAAOkX,EAAUF,GAAIxV,OAASxB,OAAOkX,EAAUF,GAAIxV,OAASuZ,GAAK,GAAG9b,WAC1H,IAAK,UACD,OAAO0a,EAAMnC,WAAWtb,OAAO+C,WACnC,IAAK,UACD,MAAMsb,EAAMZ,EAAMnC,WAAWrY,QAAO,CAAC4b,EAAK/D,IAAO+D,EAAM/a,OAAOkX,EAAUF,GAAIxV,OAASxB,OAAOkX,EAAUF,GAAIxV,OAASuZ,GAAK,KAAK9b,WACvHub,EAAMb,EAAMnC,WAAWrY,QAAO,CAAC4b,EAAK/D,IAAO+D,EAAM/a,OAAOkX,EAAUF,GAAIxV,OAASxB,OAAOkX,EAAUF,GAAIxV,OAASuZ,GAAK,GAAG9b,WAC3H,OAAOsb,GAAOC,EAAMD,EAAMA,EAAM,IAAMC,EAC1C,QACI,OAAOM,MAOjB1I,GAAU,CAACzD,EAA0BqM,EAA6B9D,KACpE,OAAQvI,GACJ,IAAK,QAEGA,EADAqM,EAAkB9e,OAAS,EACpBgb,EAAU8D,EAAkB,IAAIrM,UAGhCqD,EAEX,MACJ,IAAK,OACD,GAAIgJ,EAAkB9e,OAAS,EAAG,CAE9ByS,EAAOuI,EAAU8D,EADCA,EAAkB9e,OAAS,IACEyS,UAG/CA,OAAOqD,EAKnB,OAAOrD,GAGL0D,GAAe,CAACvD,EAA+BkM,EAA6B9D,KAC9E,OAAQpI,GACJ,IAAK,QAEGA,EADAkM,EAAkB9e,OAAS,EACfgb,EAAU8D,EAAkB,IAAIlM,eAGhCkD,EAEhB,MACJ,IAAK,OACD,GAAIgJ,EAAkB9e,OAAS,EAAG,CAE9B4S,EAAYoI,EAAU8D,EADJA,EAAkB9e,OAAS,IACO4S,eAGpDA,OAAYkD,EAKxB,OAAOlD,SC3MEmM,WAAyB/L,GAAtC1Q,kCAYWC,UAAiB,GAMjBA,YAA0B,GAU1BA,eAAgC,GAK5B8D,oBACP,OAAOoP,EAAS,CAACC,02BAGflC,eAAeC,EAAwBC,6CAEboC,MAAxBvT,KAAKyc,iBAAgCvL,KACrClR,KAAKyc,gBAAkB,IAAIvC,GAAgBla,KAAKyR,SAGhDN,UACMnR,KAAKyc,gBAAgB1V,OAAO/G,KAAKsQ,OAG3CtQ,KAAKqY,OAASrY,KAAKyR,OAAOoH,MAE1B7Y,KAAKyY,UAAYzY,KAAKyc,gBAAgB7B,eAEtC,MAAM8B,EAAUC,GAAwB3c,KAAKyR,OAAQzR,KAAKyY,WAErD2B,QAAO7B,IAAOvY,KAAKyY,UAAUF,GAAImD,WAEhCkB,EDhDkB,EAACnE,EAA+BoE,EAAqBpL,EAA6CmK,KAC9H,MAAM7c,EAA8B,CAChCuZ,KAAM,GACNI,OAAQ,IAGZ,IAAKjH,EAED,OADA1S,EAAOuZ,KAAOuE,EACP9d,EAGX,GAAqB,iBAAV0S,EAAoB,CAC3B1S,EAAOuZ,KAAOuE,EAAU9U,MAAM,EAAG0J,GACjC,MAAMqL,EAAqBD,EAAU9U,MAAM0J,GACvCqL,EAAmBrf,OAAS,GAC5BsB,EAAO2Z,OAAOlW,KAAK0Z,GAAYN,EAAakB,SAIhDd,GAAqBvK,GAErBoL,EAAUja,SAAQ2V,IACd,MAAMwE,EAAapB,GAAclK,EAAQgH,EAAUF,GAAKqD,IACrC,GAAfmB,EAEAhe,EAAOuZ,KAAK9V,KAAK+V,IAIjBxZ,EAAO2Z,OAAOqE,GAAche,EAAO2Z,OAAOqE,IAAeb,GAAYN,EAAa,GAAInK,EAAOsL,IAC7Fhe,EAAO2Z,OAAOqE,GAAYhE,WAAWvW,KAAK+V,OAmBtD,OAbAxZ,EAAO2Z,OAAO9V,SAAQyF,IACdA,EAAEwQ,QACFxQ,EAAEwQ,MAAQuD,GAAgB/T,EAAEwQ,MAAOxQ,EAAGoQ,IAGtCpQ,EAAE0H,gBACF1H,EAAE0H,cAAgBqM,GAAgB/T,EAAE0H,cAAe1H,EAAGoQ,IAG1DpQ,EAAE6H,KAAOyD,GAAQtL,EAAE6H,KAAM7H,EAAE0Q,WAAYN,GACvCpQ,EAAEgI,UAAYuD,GAAavL,EAAEgI,UAAWhI,EAAE0Q,WAAYN,MAGnD1Z,GCDoBie,CAAiBhd,KAAKyY,UAAWiE,EAAS1c,KAAKyR,OAAOwJ,SAAUjb,KAAKyc,gBAAgBlB,YAGxGna,KAAKC,UAAUub,EAAetE,OAASlX,KAAKC,UAAUrB,KAAKsY,QAC3DtY,KAAKsY,KAAOsE,EAAetE,MAI3BlX,KAAKC,UAAUub,EAAelE,SAAWtX,KAAKC,UAAUrB,KAAK0Y,UAC5D1Y,KAAK0Y,OAASkE,EAAelE,WAItCvK,SACI,OAAwB,GAApBnO,KAAKsY,KAAK7a,QAAqC,GAAtBuC,KAAK0Y,OAAOjb,OAE9BwS,CAAI,GAGRmI,GAASpY,MAanBid,oBACG,IAAIC,aAAOld,KAAKyR,OAAOsJ,+BAAUtd,SAAU,EAE3C,OAAIuC,KAAKyR,OAAOwJ,SACuB,iBAAxBjb,KAAKyR,OAAOwJ,SAEZjb,KAAKyR,OAAOwJ,SAAW,EAGvBjb,KAAKyR,OAAOwJ,SAASxd,OAAS,EAKtCyf,EAAO,GA/FlBjgB,GADCoX,GAAS,CAAC3S,WAAW,mCAOtBzE,GADCoX,GAAS,CAAC3S,WAAU,iCAOrBzE,GADCoX,GAAS,CAAC3S,WAAW,mCA8F1B,MAAMib,GAA0B,CAAClL,EAA4BgH,KACzD,IAAI0E,EAAkBxf,OAAOmb,KAAKL,GAAWlT,KAAIwM,GAAY0G,EAAU1G,KACvE,OAAQN,EAAO2L,eACX,IAAK,MACDD,EAAkBA,EAAgBE,MAAK,CAACrb,EAAG2G,IAAM2U,GAAiBtb,EAAEe,MAAO4F,EAAE5F,SAC7E,MACJ,IAAK,OACDoa,EAAkBA,EAAgBE,MAAK,CAACrb,EAAG2G,IAAM2U,GAAiB3U,EAAE5F,MAAOf,EAAEe,SAIrF,OAAOoa,EAAgB5X,KAAIoD,GAAKA,EAAEoJ,YAShCuL,GAAmB,CAACtb,EAAW2G,KACjC,IAAI4U,EAAOhc,OAAOS,GACdwb,EAAOjc,OAAOoH,GAGlB,OAFA4U,EAAO7N,MAAM6N,IAAS,EAAIA,EAC1BC,EAAO9N,MAAM8N,IAAS,EAAIA,EACnBD,EAAOC,QChJiCjK,IAA/CkK,eAAepd,IAAI,yBTLW8O,QAAQuO,KACtC,wJSMAD,eAAeE,OAAO,uBAAwB1K,IAC9CwK,eAAeE,OAAO,qBAAsBnB,KAG5CxN,GAAI,sCAAuC"} \ No newline at end of file diff --git a/www/better-thermostat-ui-card.js b/www/better-thermostat-ui-card.js new file mode 100644 index 00000000..591bfb56 --- /dev/null +++ b/www/better-thermostat-ui-card.js @@ -0,0 +1,483 @@ +function t(t,e,n,i){var r,o=arguments.length,s=o<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,n):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,n,i);else for(var a=t.length-1;a>=0;a--)(r=t[a])&&(s=(o<3?r(s):o>3?r(e,n,s):r(e,n))||s);return o>3&&s&&Object.defineProperty(e,n,s),s +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}const e=window,n=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),r=new WeakMap;let o=class{constructor(t,e,n){if(this._$cssResult$=!0,n!==i)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(n&&void 0===t){const n=void 0!==e&&1===e.length;n&&(t=r.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),n&&r.set(e,t))}return t}toString(){return this.cssText}};const s=(t,...e)=>{const n=1===t.length?t[0]:e.reduce(((e,n,i)=>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.")})(n)+t[i+1]),t[0]);return new o(n,t,i)},a=n?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return(t=>new o("string"==typeof t?t:t+"",void 0,i))(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var l;const u=window,c=u.trustedTypes,h=c?c.emptyScript:"",d=u.reactiveElementPolyfillSupport,p={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 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}},f=(t,e)=>e!==t&&(e==e||t==t),m={attribute:!0,type:String,converter:p,reflect:!1,hasChanged:f};let g=class 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,n)=>{const i=this._$Ep(n,e);void 0!==i&&(this._$Ev.set(i,n),t.push(i))})),t}static createProperty(t,e=m){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const n="symbol"==typeof t?Symbol():"__"+t,i=this.getPropertyDescriptor(t,n,e);void 0!==i&&Object.defineProperty(this.prototype,t,i)}}static getPropertyDescriptor(t,e,n){return{get(){return this[e]},set(i){const r=this[t];this[e]=i,this.requestUpdate(t,r,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||m}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 n of e)this.createProperty(n,t[n])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const n=new Set(t.flat(1/0).reverse());for(const t of n)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Ep(t,e){const n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"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,n;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(n=t.hostConnected)||void 0===n||n.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 i=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,i)=>{n?t.adoptedStyleSheets=i.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):i.forEach((n=>{const i=document.createElement("style"),r=e.litNonce;void 0!==r&&i.setAttribute("nonce",r),i.textContent=n.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._$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,n){this._$AK(t,n)}_$EO(t,e,n=m){var i;const r=this.constructor._$Ep(t,n);if(void 0!==r&&!0===n.reflect){const o=(void 0!==(null===(i=n.converter)||void 0===i?void 0:i.toAttribute)?n.converter:p).toAttribute(e,n.type);this._$El=t,null==o?this.removeAttribute(r):this.setAttribute(r,o),this._$El=null}}_$AK(t,e){var n;const i=this.constructor,r=i._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=i.getPropertyOptions(r),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(n=t.converter)||void 0===n?void 0:n.fromAttribute)?t.converter:p;this._$El=r,this[r]=o.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,n){let i=!0;void 0!==t&&(((n=n||this.constructor.getPropertyOptions(t)).hasChanged||f)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===n.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,n))):i=!1),!this.isUpdatePending&&i&&(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 n=this._$AL;try{e=this.shouldUpdate(n),e?(this.willUpdate(n),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(n)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(n)}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){}}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var _;g.finalized=!0,g.elementProperties=new Map,g.elementStyles=[],g.shadowRootOptions={mode:"open"},null==d||d({ReactiveElement:g}),(null!==(l=u.reactiveElementVersions)&&void 0!==l?l:u.reactiveElementVersions=[]).push("1.4.2");const v=window,y=v.trustedTypes,x=y?y.createPolicy("lit-html",{createHTML:t=>t}):void 0,b=`lit$${(Math.random()+"").slice(9)}$`,w="?"+b,T=`<${w}>`,A=document,C=(t="")=>A.createComment(t),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,k=Array.isArray,E=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,S=/-->/g,L=/>/g,$=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),O=/'/g,P=/"/g,D=/^(?:script|style|textarea|title)$/i,R=t=>(e,...n)=>({_$litType$:t,strings:e,values:n}),N=R(1),z=R(2),F=Symbol.for("lit-noChange"),I=Symbol.for("lit-nothing"),B=new WeakMap,V=A.createTreeWalker(A,129,null,!1),j=(t,e)=>{const n=t.length-1,i=[];let r,o=2===e?"":"",s=E;for(let e=0;e"===l[0]?(s=null!=r?r:E,u=-1):void 0===l[1]?u=-2:(u=s.lastIndex-l[2].length,a=l[1],s=void 0===l[3]?$:'"'===l[3]?P:O):s===P||s===O?s=$:s===S||s===L?s=E:(s=$,r=void 0);const h=s===$&&t[e+1].startsWith("/>")?" ":"";o+=s===E?n+T:u>=0?(i.push(a),n.slice(0,u)+"$lit$"+n.slice(u)+b+h):n+b+(-2===u?(i.push(void 0),e):h)}const a=o+(t[n]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==x?x.createHTML(a):a,i]};class X{constructor({strings:t,_$litType$:e},n){let i;this.parts=[];let r=0,o=0;const s=t.length-1,a=this.parts,[l,u]=j(t,e);if(this.el=X.createElement(l,n),V.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(i=V.nextNode())&&a.length0){i.textContent=y?y.emptyScript:"";for(let n=0;nk(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]))(t)?this.k(t):this.g(t)}O(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}g(t){this._$AH!==I&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(A.createTextNode(t)),this._$AH=t}$(t){var e;const{values:n,_$litType$:i}=t,r="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(i.h,this.options)),i);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.p(n);else{const t=new H(r,this),e=t.v(this.options);t.p(n),this.T(e),this._$AH=t}}_$AC(t){let e=B.get(t.strings);return void 0===e&&B.set(t.strings,e=new X(t)),e}k(t){k(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let n,i=0;for(const r of t)i===e.length?e.push(n=new U(this.O(C()),this.O(C()),this,this.options)):n=e[i],n._$AI(r),i++;i2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=I}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,n,i){const r=this.strings;let o=!1;if(void 0===r)t=Y(this,t,e,0),o=!M(t)||t!==this._$AH&&t!==F,o&&(this._$AH=t);else{const i=t;let s,a;for(t=r[0],s=0;s{var i,r;const o=null!==(i=null==n?void 0:n.renderBefore)&&void 0!==i?i:e;let s=o._$litPart$;if(void 0===s){const t=null!==(r=null==n?void 0:n.renderBefore)&&void 0!==r?r:null;o._$litPart$=s=new U(e.insertBefore(C(),t),t,void 0,null!=n?n:{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!1)}render(){return F}}nt.finalized=!0,nt._$litElement$=!0,null===(tt=globalThis.litElementHydrateSupport)||void 0===tt||tt.call(globalThis,{LitElement:nt});const it=globalThis.litElementPolyfillSupport;null==it||it({LitElement:nt}),(null!==(et=globalThis.litElementVersions)&&void 0!==et?et:globalThis.litElementVersions=[]).push("3.2.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const rt=t=>e=>"function"==typeof e?((t,e)=>(customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:n,elements:i}=e;return{kind:n,elements:i,finisher(e){customElements.define(t,e)}}})(t,e) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,ot=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(n){n.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(n){n.createProperty(e.key,t)}};function st(t){return(e,n)=>void 0!==n?((t,e,n)=>{e.constructor.createProperty(n,t)})(t,e,n):ot(t,e) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function at(t){return st({...t,state:!0})} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var lt;null===(lt=window.HTMLSlotElement)||void 0===lt||lt.prototype.assignedElements; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const ut=1;class ct{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,n){this._$Ct=t,this._$AM=e,this._$Ci=n}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const ht=(t=>(...e)=>({_$litDirective$:t,values:e}))(class extends ct{constructor(t){var e;if(super(t),t.type!==ut||"class"!==t.name||(null===(e=t.strings)||void 0===e?void 0:e.length)>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(t){return" "+Object.keys(t).filter((e=>t[e])).join(" ")+" "}update(t,[e]){var n,i;if(void 0===this.nt){this.nt=new Set,void 0!==t.strings&&(this.st=new Set(t.strings.join(" ").split(/\s/).filter((t=>""!==t))));for(const t in e)e[t]&&!(null===(n=this.st)||void 0===n?void 0:n.has(t))&&this.nt.add(t);return this.render(e)}const r=t.element.classList;this.nt.forEach((t=>{t in e||(r.remove(t),this.nt.delete(t))}));for(const t in e){const n=!!e[t];n===this.nt.has(t)||(null===(i=this.st)||void 0===i?void 0:i.has(t))||(n?(r.add(t),this.nt.add(t)):(r.remove(t),this.nt.delete(t)))}return F}});var dt="M8.5 4.5L5.4 9.5L8.5 14.7L5.2 20.5L3.4 19.6L6.1 14.7L3 9.5L6.7 3.6L8.5 4.5M14.7 4.4L11.6 9.5L14.7 14.5L11.4 20.3L9.6 19.4L12.3 14.5L9.2 9.5L12.9 3.5L14.7 4.4M21 4.4L17.9 9.5L21 14.5L17.7 20.3L15.9 19.4L18.6 14.5L15.5 9.5L19.2 3.5L21 4.4",pt="M10 2L7.6 5.4C8.4 5.2 9.2 5 10 5C10.8 5 11.6 5.2 12.4 5.4M19 5C17.89 5 17 5.89 17 7V13.76C16.36 14.33 16 15.15 16 16C16 17.66 17.34 19 19 19C20.66 19 22 17.66 22 16C22 15.15 21.64 14.33 21 13.77V7C21 5.89 20.11 5 19 5M19 6C19.55 6 20 6.45 20 7V8H18V7C18 6.45 18.45 6 19 6M5.5 6.7L1.3 7L3.1 10.8C3.2 10 3.5 9.2 3.9 8.5C4.4 7.8 4.9 7.2 5.5 6.7M10 7C7.2 7 5 9.2 5 12C5 14.8 7.2 17 10 17C12.8 17 15 14.8 15 12C15 9.2 12.8 7 10 7M3.2 13.2L1.4 17L5.5 17.4C5 16.9 4.4 16.2 4 15.5C3.5 14.8 3.3 14 3.2 13.2M7.6 18.6L10 22L12.4 18.6C11.6 18.8 10.8 19 10 19C9.1 19 8.3 18.8 7.6 18.6Z",ft="M12,3.25C12,3.25 6,10 6,14C6,17.32 8.69,20 12,20A6,6 0 0,0 18,14C18,10 12,3.25 12,3.25M14.47,9.97L15.53,11.03L9.53,17.03L8.47,15.97M9.75,10A1.25,1.25 0 0,1 11,11.25A1.25,1.25 0 0,1 9.75,12.5A1.25,1.25 0 0,1 8.5,11.25A1.25,1.25 0 0,1 9.75,10M14.25,14.5A1.25,1.25 0 0,1 15.5,15.75A1.25,1.25 0 0,1 14.25,17A1.25,1.25 0 0,1 13,15.75A1.25,1.25 0 0,1 14.25,14.5Z",mt="M21 20V2H3V20H1V23H23V20M19 4V11H17V4M5 4H7V11H5M5 20V13H7V20M9 20V4H15V20M17 20V13H19V20Z";var gt={version:"version",current:"current"},_t={card:{climate:{disable_window:"Disable window",disable_summer:"Disable summer",disable_eco:"Disable eco",disable_heat:"Disable heat",disable_off:"Disable off",eco_temperature:"Eco temperature",set_current_as_main:"Swap target with current temperature places"}}},vt={window_open:"Window open",night_mode:"Night mode",eco:"Eco",summer:"Summer"},yt={common:gt,editor:_t,extra_states:vt},xt=Object.freeze({__proto__:null,common:gt,editor:_t,extra_states:vt,default:yt}),bt={version:"Version",current:"Aktuell"},wt={card:{climate:{disable_window:"Fenster-offen-Anzeige deaktivieren",disable_summer:"Sommer-Anzeige deaktivieren",disable_eco:"Eco-Anzeige deaktivieren",disable_heat:"Heiz-Anzeige deaktivieren",disable_off:"Aus-Anzeige deaktivieren",eco_temperature:"Eco Temperatur",set_current_as_main:"Zieltemperatur mit aktueller Temperatur tauschen"}}},Tt={window_open:"Fenster offen",night_mode:"Nachtmodus",eco:"Eco",summer:"Sommer"},At={common:bt,editor:wt,extra_states:Tt},Ct=Object.freeze({__proto__:null,common:bt,editor:wt,extra_states:Tt,default:At}),Mt={version:"version",current:"Actuel"},kt={window_open:"Fenêtre ouverte",night_mode:"Mode nuit",eco:"Eco",summer:"Été"},Et={common:Mt,extra_states:kt},St=Object.freeze({__proto__:null,common:Mt,extra_states:kt,default:Et}),Lt={version:"версия",current:"текущий"},$t={window_open:"Окно открыто",night_mode:"Ночной режим",eco:"Эко",summer:"Лето"},Ot={common:Lt,extra_states:$t},Pt=Object.freeze({__proto__:null,common:Lt,extra_states:$t,default:Ot}),Dt={version:"wersja",current:"aktualna"},Rt={window_open:"otwarte okno",night_mode:"tryb nocny",eco:"tryb ekonomiczny",summer:"lato"},Nt={common:Dt,extra_states:Rt},zt=Object.freeze({__proto__:null,common:Dt,extra_states:Rt,default:Nt}),Ft={version:"verzia",current:"aktuálny"},It={window_open:"Okno otvorené",night_mode:"Nočný mód",eco:"Eco",summer:"Leto"},Bt={common:Ft,extra_states:It},Vt={version:"Verzió",current:"Aktuális"},jt={window_open:"Ablak nyitva",night_mode:"Éjszakai mód",eco:"Eco",summer:"Nyár"},Xt={common:Vt,extra_states:jt},Yt={version:"version",current:"nuværende"},Ht={window_open:"Vindue åben",night_mode:"Nattilstand",eco:"Eco",summer:"Sommer"},Ut={common:Yt,extra_states:Ht},Wt={version:"version",current:"Actual"},qt={window_open:"Ventana abierta",night_mode:"Modo noche",eco:"Eco",summer:"Verano"},Zt={common:Wt,extra_states:qt},Gt={version:"versiyon",current:"şimdiki"},Kt={window_open:"Pencere açık",night_mode:"Gece modu",eco:"Eco",summer:"Yaz"},Qt={common:Gt,extra_states:Kt},Jt={version:"versione",current:"Corrente"},te={window_open:"Finestra aperta",night_mode:"Modalità notturna",eco:"Eco",summer:"Estate"},ee={common:Jt,extra_states:te},ne={version:"versão",current:"actual"},ie={card:{climate:{disable_window:"Desactivar Janela",disable_summer:"Desactivar Verão",disable_eco:"Desactivar Eco",disable_heat:"Desactivar Aquecimento",disable_off:"Desactivar Off",eco_temperature:"Modo Eco",set_current_as_main:"Mudar para a temperatura local actual"}}},re={window_open:"Janela Aberta",night_mode:"Modo Noturno",eco:"Eco",summer:"Verão"},oe={common:ne,editor:ie,extra_states:re},se={version:"版本",current:"当前"},ae={window_open:"窗户打开",night_mode:"夜间模式",eco:"节能",summer:"夏季"},le={common:se,extra_states:ae},ue={version:"версія",current:"поточний"},ce={window_open:"Вікно відчинено",night_mode:"Нічний режим",eco:"Економія",summer:"Літо"},he={common:ue,extra_states:ce},de={version:"έκδοση",current:"τρέχουσα"},pe={window_open:"Παράθυρο ανοικτό",night_mode:"Λειτουργία νυκτός",eco:"Εξοικονόμηση",summer:"Καλοκαίρι"},fe={common:de,extra_states:pe},me={version:"versie",current:"huidig"},ge={window_open:"Raam open",night_mode:"Nacht modus",eco:"Eco",summer:"Zomer"},_e={common:me,extra_states:ge},ve={version:"versjon",current:"nåværende"},ye={window_open:"Vindu åpent",night_mode:"Nattmodus",eco:"Eco",summer:"Sommer"},xe={common:ve,extra_states:ye},be={version:"verze",current:"aktuální"},we={window_open:"Otevřené okno",night_mode:"Noční režim",eco:"Eco",summer:"Léto"},Te={common:be,extra_states:we},Ae={version:"različica",current:"trenutno"},Ce={window_open:"Okno odprto",night_mode:"Nočni način",eco:"Eko",summer:"Poletje"},Me={common:Ae,extra_states:Ce},ke={version:"version",current:"Nuvarande"},Ee={window_open:"Fönster öppet",night_mode:"Nattläge",eco:"Eco",summer:"Sommar"},Se={common:ke,extra_states:Ee},Le={version:"версия",currrent:"текущий"},$e={window_open:"Отворен прозорец",night_mode:"Нощен режим",eco:"Екологичен режим",summer:"Лято"},Oe={common:Le,extra_states:$e},Pe={version:"version",current:"Nykyinen"},De={window_open:"Ikkuna auki",night_mode:"Yötila",eco:"Eco",summer:"Kesä"},Re={common:Pe,extra_states:De},Ne={version:"versiune",current:"curent"},ze={window_open:"Fereastră deschisă",night_mode:"Mod noapte",eco:"Eco",summer:"Vară"},Fe={common:Ne,extra_states:ze},Ie={version:"versió",current:"Actual"},Be={window_open:"Finestra oberta",night_mode:"Mode nocturn",eco:"Eco",summer:"Estiu"},Ve={common:Ie,extra_states:Be};const je={en:xt,de:Ct,fr:St,ru:Pt,sk:Object.freeze({__proto__:null,common:Ft,extra_states:It,default:Bt}),hu:Object.freeze({__proto__:null,common:Vt,extra_states:jt,default:Xt}),pl:zt,da:Object.freeze({__proto__:null,common:Yt,extra_states:Ht,default:Ut}),es:Object.freeze({__proto__:null,common:Wt,extra_states:qt,default:Zt}),tr:Object.freeze({__proto__:null,common:Gt,extra_states:Kt,default:Qt}),it:Object.freeze({__proto__:null,common:Jt,extra_states:te,default:ee}),pt:Object.freeze({__proto__:null,common:ne,editor:ie,extra_states:re,default:oe}),cn:Object.freeze({__proto__:null,common:se,extra_states:ae,default:le}),uk:Object.freeze({__proto__:null,common:ue,extra_states:ce,default:he}),el:Object.freeze({__proto__:null,common:de,extra_states:pe,default:fe}),nl:Object.freeze({__proto__:null,common:me,extra_states:ge,default:_e}),no:Object.freeze({__proto__:null,common:ve,extra_states:ye,default:xe}),cs:Object.freeze({__proto__:null,common:be,extra_states:we,default:Te}),sl:Object.freeze({__proto__:null,common:Ae,extra_states:Ce,default:Me}),sv:Object.freeze({__proto__:null,common:ke,extra_states:Ee,default:Se}),bg:Object.freeze({__proto__:null,common:Le,extra_states:$e,default:Oe}),fi:Object.freeze({__proto__:null,common:Pe,extra_states:De,default:Re}),ro:Object.freeze({__proto__:null,common:Ne,extra_states:ze,default:Fe}),ca:Object.freeze({__proto__:null,common:Ie,extra_states:Be,default:Ve})};function Xe({hass:t,string:e,search:n="",replace:i=""}){var r;const o=null!==(r=null==t?void 0:t.locale.language)&&void 0!==r?r:"en";let s;try{s=e.split(".").reduce(((t,e)=>t[e]),je[o])}catch(t){s=e.split(".").reduce(((t,e)=>t[e]),je.en)}return void 0===s&&(s=e.split(".").reduce(((t,e)=>t[e]),je.en)),""!==n&&""!==i&&(s=s.replace(n,i)),s}function Ye(t,e){try{return t.split(".").reduce(((t,e)=>t[e]),je[e])}catch(t){return}}var He,Ue,We=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function qe(t,e){if(t.length!==e.length)return!1;for(var n=0;nnew Intl.DateTimeFormat(t.language,{weekday:"long",month:"long",day:"numeric"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"numeric",day:"numeric"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{day:"numeric",month:"short"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{month:"long",year:"numeric"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{month:"long"}))),Ze((t=>new Intl.DateTimeFormat(t.language,{year:"numeric"}))),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"}(He||(He={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(Ue||(Ue={}));const Ge=Ze((t=>{if(t.time_format===Ue.language||t.time_format===Ue.system){const e=t.time_format===Ue.language?t.language:void 0,n=(new Date).toLocaleString(e);return n.includes("AM")||n.includes("PM")}return t.time_format===Ue.am_pm}));Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:Ge(t)?"numeric":"2-digit",minute:"2-digit",hour12:Ge(t)}))),Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:Ge(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:Ge(t)}))),Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit",hour12:Ge(t)}))),Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{hour:"numeric",minute:"2-digit",hour12:Ge(t)}))),Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{hour:Ge(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:Ge(t)}))),Ze((t=>new Intl.DateTimeFormat("en"!==t.language||Ge(t)?t.language:"en-u-hc-h23",{weekday:"long",hour:Ge(t)?"numeric":"2-digit",minute:"2-digit",hour12:Ge(t)})));const Ke=(t,e,n,i)=>{i=i||{},n=null==n?{}:n;const r=new Event(e,{bubbles:void 0===i.bubbles||i.bubbles,cancelable:Boolean(i.cancelable),composed:void 0===i.composed||i.composed});return r.detail=n,t.dispatchEvent(r),r},Qe=(t,e,n)=>{const i=e?(t=>{switch(t.number_format){case He.comma_decimal:return["en-US","en"];case He.decimal_comma:return["de","es","it"];case He.space_comma:return["fr","sv","cs"];case He.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)!==He.none&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(i,Je(t,n)).format(Number(t))}catch(e){return console.error(e),new Intl.NumberFormat(void 0,Je(t,n)).format(Number(t))}return"string"==typeof t?t:`${((t,e=2)=>Math.round(t*10**e)/10**e)(t,null==n?void 0:n.maximumFractionDigits).toString()}${"currency"===(null==n?void 0:n.style)?` ${n.currency}`:""}`},Je=(t,e)=>{const n=Object.assign({maximumFractionDigits:2},e);if("string"!=typeof t)return n;if(!e||!e.minimumFractionDigits&&!e.maximumFractionDigits){const e=t.indexOf(".")>-1?t.split(".")[1].length:0;n.minimumFractionDigits=e,n.maximumFractionDigits=e}return n};class tn extends TypeError{constructor(t,e){let n;const{message:i,...r}=t,{path:o}=t;super(0===o.length?i:"At path: "+o.join(".")+" -- "+i),this.value=void 0,this.key=void 0,this.type=void 0,this.refinement=void 0,this.path=void 0,this.branch=void 0,this.failures=void 0,Object.assign(this,r),this.name=this.constructor.name,this.failures=()=>{var i;return null!=(i=n)?i:n=[t,...e()]}}}function en(t){return"object"==typeof t&&null!=t}function nn(t){return"string"==typeof t?JSON.stringify(t):""+t}function rn(t,e,n,i){if(!0===t)return;!1===t?t={}:"string"==typeof t&&(t={message:t});const{path:r,branch:o}=e,{type:s}=n,{refinement:a,message:l="Expected a value of type `"+s+"`"+(a?" with refinement `"+a+"`":"")+", but received: `"+nn(i)+"`"}=t;return{value:i,type:s,refinement:a,key:r[r.length-1],path:r,branch:o,...t,message:l}}function*on(t,e,n,i){(function(t){return en(t)&&"function"==typeof t[Symbol.iterator]})(t)||(t=[t]);for(const r of t){const t=rn(r,e,n,i);t&&(yield t)}}function*sn(t,e,n){void 0===n&&(n={});const{path:i=[],branch:r=[t],coerce:o=!1,mask:s=!1}=n,a={path:i,branch:r};if(o&&(t=e.coercer(t,a),s&&"type"!==e.type&&en(e.schema)&&en(t)&&!Array.isArray(t)))for(const n in t)void 0===e.schema[n]&&delete t[n];let l=!0;for(const n of e.validator(t,a))l=!1,yield[n,void 0];for(let[n,u,c]of e.entries(t,a)){const e=sn(u,c,{path:void 0===n?i:[...i,n],branch:void 0===n?r:[...r,u],coerce:o,mask:s});for(const i of e)i[0]?(l=!1,yield[i[0],void 0]):o&&(u=i[1],void 0===n?t=u:t instanceof Map?t.set(n,u):t instanceof Set?t.add(u):en(t)&&(t[n]=u))}if(l)for(const n of e.refiner(t,a))l=!1,yield[n,void 0];l&&(yield[void 0,t])}class an{constructor(t){this.TYPE=void 0,this.type=void 0,this.schema=void 0,this.coercer=void 0,this.validator=void 0,this.refiner=void 0,this.entries=void 0;const{type:e,schema:n,validator:i,refiner:r,coercer:o=(t=>t),entries:s=function*(){}}=t;this.type=e,this.schema=n,this.entries=s,this.coercer=o,this.validator=i?(t,e)=>on(i(t,e),e,this,t):()=>[],this.refiner=r?(t,e)=>on(r(t,e),e,this,t):()=>[]}assert(t){return ln(t,this)}create(t){return function(t,e){const n=un(t,e,{coerce:!0});if(n[0])throw n[0];return n[1]}(t,this)}is(t){return function(t,e){return!un(t,e)[0]}(t,this)}mask(t){return function(t,e){const n=un(t,e,{coerce:!0,mask:!0});if(n[0])throw n[0];return n[1]}(t,this)}validate(t,e){return void 0===e&&(e={}),un(t,this,e)}}function ln(t,e){const n=un(t,e);if(n[0])throw n[0]}function un(t,e,n){void 0===n&&(n={});const i=sn(t,e,n),r=function(t){const{done:e,value:n}=t.next();return e?void 0:n}(i);if(r[0]){const t=new tn(r[0],(function*(){for(const t of i)t[0]&&(yield t[0])}));return[t,void 0]}return[void 0,r[1]]}function cn(t,e){return new an({type:t,schema:null,validator:e})}function hn(t){return new an({type:"array",schema:t,*entries(e){if(t&&Array.isArray(e))for(const[n,i]of e.entries())yield[n,i,t]},coercer:t=>Array.isArray(t)?t.slice():t,validator:t=>Array.isArray(t)||"Expected an array value, but received: "+nn(t)})}function dn(){return cn("boolean",(t=>"boolean"==typeof t))}function pn(t){const e=nn(t),n=typeof t;return new an({type:"literal",schema:"string"===n||"number"===n||"boolean"===n?t:null,validator:n=>n===t||"Expected the literal `"+e+"`, but received: "+nn(n)})}function fn(){return cn("number",(t=>"number"==typeof t&&!isNaN(t)||"Expected a number, but received: "+nn(t)))}function mn(t){const e=t?Object.keys(t):[],n=cn("never",(()=>!1));return new an({type:"object",schema:t||null,*entries(i){if(t&&en(i)){const r=new Set(Object.keys(i));for(const n of e)r.delete(n),yield[n,i[n],t[n]];for(const t of r)yield[t,i[t],n]}},validator:t=>en(t)||"Expected an object, but received: "+nn(t),coercer:t=>en(t)?{...t}:t})}function gn(t){return new an({...t,validator:(e,n)=>void 0===e||t.validator(e,n),refiner:(e,n)=>void 0===e||t.refiner(e,n)})}function _n(){return cn("string",(t=>"string"==typeof t||"Expected a string, but received: "+nn(t)))}function vn(t){const e=Object.keys(t);return new an({type:"type",schema:t,*entries(n){if(en(n))for(const i of e)yield[i,n[i],t[i]]},validator:t=>en(t)||"Expected an object, but received: "+nn(t)})}function yn(t){const e=t.map((t=>t.type)).join(" | ");return new an({type:"union",schema:null,coercer(e,n){const i=t.find((t=>{const[n]=t.validate(e,{coerce:!0});return!n}))||cn("unknown",(()=>!0));return i.coercer(e,n)},validator(n,i){const r=[];for(const e of t){const[...t]=sn(n,e,i),[o]=t;if(!o[0])return[];for(const[e]of t)e&&r.push(e)}return["Expected the value to satisfy a union of `"+e+"`, but received: "+nn(n),...r]}})}const xn=mn({user:_n()}),bn=yn([dn(),mn({text:gn(_n()),excemptions:gn(hn(xn))})]),wn=mn({action:pn("url"),url_path:_n(),confirmation:gn(bn)}),Tn=mn({action:pn("call-service"),service:_n(),service_data:gn(mn()),data:gn(mn()),target:gn(mn({entity_id:gn(yn([_n(),hn(_n())])),device_id:gn(yn([_n(),hn(_n())])),area_id:gn(yn([_n(),hn(_n())]))})),confirmation:gn(bn)}),An=mn({action:pn("navigate"),navigation_path:_n(),confirmation:gn(bn)}),Cn=vn({action:pn("fire-dom-event")}),Mn=mn({action:function(t){const e={},n=t.map((t=>nn(t))).join();for(const n of t)e[n]=n;return new an({type:"enums",schema:e,validator:e=>t.includes(e)||"Expected one of `"+n+"`, but received: "+nn(e)})}(["none","toggle","more-info","call-service","url","navigate"]),confirmation:gn(bn)});var kn;kn=t=>{if(t&&"object"==typeof t&&"action"in t)switch(t.action){case"call-service":return Tn;case"fire-dom-event":return Cn;case"navigate":return An;case"url":return wn}return Mn},new an({type:"dynamic",schema:null,*entries(t,e){const n=kn(t,e);yield*n.entries(t,e)},validator:(t,e)=>kn(t,e).validator(t,e),coercer:(t,e)=>kn(t,e).coercer(t,e),refiner:(t,e)=>kn(t,e).refiner(t,e)}),s` + #sortable a:nth-of-type(2n) paper-icon-item { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable a:nth-of-type(2n-1) paper-icon-item { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable a { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block !important; + } + + .hidden-panel { + display: flex !important; + } + + .sortable-fallback { + display: none; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 4px; + --mdc-icon-button-size: 40px; + } + + :host([rtl]) .show-panel { + right: initial; + left: 4px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([rtl]) .hide-panel { + right: initial; + left: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-panel { + display: inline-flex; + } + + paper-icon-item.hidden-panel, + paper-icon-item.hidden-panel span, + paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } +`;function En(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function Sn(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e} +/*! + * GSAP 3.11.3 + * https://greensock.com + * + * @license Copyright 2008-2022, GreenSock. All rights reserved. + * Subject to the terms at https://greensock.com/standard-license or for + * Club GreenSock members, the agreement issued with that membership. + * @author: Jack Doyle, jack@greensock.com +*/var Ln,$n,On,Pn,Dn,Rn,Nn,zn,Fn,In,Bn,Vn={autoSleep:120,force3D:"auto",nullTargetWarn:1,units:{lineHeight:""}},jn={duration:.5,overwrite:!1,delay:0},Xn=2*Math.PI,Yn=Xn/4,Hn=0,Un=Math.sqrt,Wn=Math.cos,qn=Math.sin,Zn=function(t){return"string"==typeof t},Gn=function(t){return"function"==typeof t},Kn=function(t){return"number"==typeof t},Qn=function(t){return void 0===t},Jn=function(t){return"object"==typeof t},ti=function(t){return!1!==t},ei=function(){return"undefined"!=typeof window},ni=function(t){return Gn(t)||Zn(t)},ii="function"==typeof ArrayBuffer&&ArrayBuffer.isView||function(){},ri=Array.isArray,oi=/(?:-?\.?\d|\.)+/gi,si=/[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,ai=/[-+=.]*\d+[.e-]*\d*[a-z%]*/g,li=/[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,ui=/[+-]=-?[.\d]+/,ci=/[^,'"\[\]\s]+/gi,hi=/^[+\-=e\s\d]*\d+[.\d]*([a-z]*|%)\s*$/i,di={},pi={},fi=function(t){return(pi=Xi(t,di))&&Xo},mi=function(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")},gi=function(t,e){return!e&&console.warn(t)},_i=function(t,e){return t&&(di[t]=e)&&pi&&(pi[t]=e)||di},vi=function(){return 0},yi={suppressEvents:!0,isStart:!0,kill:!1},xi={suppressEvents:!0,kill:!1},bi={suppressEvents:!0},wi={},Ti=[],Ai={},Ci={},Mi={},ki=30,Ei=[],Si="",Li=function(t){var e,n,i=t[0];if(Jn(i)||Gn(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(n=Ei.length;n--&&!Ei[n].targetTest(i););e=Ei[n]}for(n=t.length;n--;)t[n]&&(t[n]._gsap||(t[n]._gsap=new io(t[n],e)))||t.splice(n,1);return t},$i=function(t){return t._gsap||Li(wr(t))[0]._gsap},Oi=function(t,e,n){return(n=t[e])&&Gn(n)?t[e]():Qn(n)&&t.getAttribute&&t.getAttribute(e)||n},Pi=function(t,e){return(t=t.split(",")).forEach(e)||t},Di=function(t){return Math.round(1e5*t)/1e5||0},Ri=function(t){return Math.round(1e7*t)/1e7||0},Ni=function(t,e){var n=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===n?t+i:"-"===n?t-i:"*"===n?t*i:t/i},zi=function(t,e){for(var n=e.length,i=0;t.indexOf(e[i])<0&&++io;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[n],t[n]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e},qi=function(t,e,n,i){void 0===n&&(n="_first"),void 0===i&&(i="_last");var r=e._prev,o=e._next;r?r._next=o:t[n]===e&&(t[n]=o),o?o._prev=r:t[i]===e&&(t[i]=r),e._next=e._prev=e.parent=null},Zi=function(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove(t),t._act=0},Gi=function(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var n=t;n;)n._dirty=1,n=n.parent;return t},Ki=function(t){for(var e=t.parent;e&&e.parent;)e._dirty=1,e.totalDuration(),e=e.parent;return t},Qi=function(t,e,n,i){return t._startAt&&($n?t._startAt.revert(xi):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))},Ji=function t(e){return!e||e._ts&&t(e.parent)},tr=function(t){return t._repeat?er(t._tTime,t=t.duration()+t._rDelay)*t:0},er=function(t,e){var n=Math.floor(t/=e);return t&&n===t?n-1:n},nr=function(t,e){return(t-e._start)*e._ts+(e._ts>=0?0:e._dirty?e.totalDuration():e._tDur)},ir=function(t){return t._end=Ri(t._start+(t._tDur/Math.abs(t._ts||t._rts||1e-8)||0))},rr=function(t,e){var n=t._dp;return n&&n.smoothChildTiming&&t._ts&&(t._start=Ri(n._time-(t._ts>0?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),ir(t),n._dirty||Gi(n,t)),t},or=function(t,e){var n;if((e._time||e._initted&&!e._dur)&&(n=nr(t.rawTime(),e),(!e._dur||_r(0,e.totalDuration(),n)-e._tTime>1e-8)&&e.render(n,!0)),Gi(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur=0&&n.totalTime(n._tTime),n=n._dp;t._zTime=-1e-8}},sr=function(t,e,n,i){return e.parent&&Zi(e),e._start=Ri((Kn(n)?n:n||t!==Pn?fr(t,n,e):t._time)+e._delay),e._end=Ri(e._start+(e.totalDuration()/Math.abs(e.timeScale())||0)),Wi(t,e,"_first","_last",t._sort?"_start":0),cr(e)||(t._recent=e),i||or(t,e),t._ts<0&&rr(t,t._tTime),t},ar=function(t,e){return(di.ScrollTrigger||mi("scrollTrigger",e))&&di.ScrollTrigger.create(e,t)},lr=function(t,e,n,i,r){return ho(t,e,r),t._initted?!n&&t._pt&&!$n&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&Fn!==Yr.frame?(Ti.push(t),t._lazy=[r,i],1):void 0:1},ur=function t(e){var n=e.parent;return n&&n._ts&&n._initted&&!n._lock&&(n.rawTime()<0||t(n))},cr=function(t){var e=t.data;return"isFromStart"===e||"isStart"===e},hr=function(t,e,n,i){var r=t._repeat,o=Ri(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=o/t._dur),t._dur=o,t._tDur=r?r<0?1e10:Ri(o*(r+1)+t._rDelay*r):o,s>0&&!i&&rr(t,t._tTime=t._tDur*s),t.parent&&ir(t),n||Gi(t.parent,t),t},dr=function(t){return t instanceof oo?Gi(t):hr(t,t._dur)},pr={_start:0,endTime:vi,totalDuration:vi},fr=function t(e,n,i){var r,o,s,a=e.labels,l=e._recent||pr,u=e.duration()>=1e8?l.endTime(!1):e._dur;return Zn(n)&&(isNaN(n)||n in a)?(o=n.charAt(0),s="%"===n.substr(-1),r=n.indexOf("="),"<"===o||">"===o?(r>=0&&(n=n.replace(/=/,"")),("<"===o?l._start:l.endTime(l._repeat>=0))+(parseFloat(n.substr(1))||0)*(s?(r<0?l:i).totalDuration()/100:1)):r<0?(n in a||(a[n]=u),a[n]):(o=parseFloat(n.charAt(r-1)+n.substr(r+1)),s&&i&&(o=o/100*(ri(i)?i[0]:i).totalDuration()),r>1?t(e,n.substr(0,r-1),i)+o:u+o)):null==n?u:+n},mr=function(t,e,n){var i,r,o=Kn(e[1]),s=(o?2:1)+(t<2?0:1),a=e[s];if(o&&(a.duration=e[1]),a.parent=n,t){for(i=a,r=n;r&&!("immediateRender"in i);)i=r.vars.defaults||{},r=ti(r.vars.inherit)&&r.parent;a.immediateRender=ti(i.immediateRender),t<2?a.runBackwards=1:a.startAt=e[s-1]}return new _o(e[0],a,e[s+1])},gr=function(t,e){return t||0===t?e(t):e},_r=function(t,e,n){return ne?e:n},vr=function(t,e){return Zn(t)&&(e=hi.exec(t))?e[1]:""},yr=[].slice,xr=function(t,e){return t&&Jn(t)&&"length"in t&&(!e&&!t.length||t.length-1 in t&&Jn(t[0]))&&!t.nodeType&&t!==Dn},br=function(t,e,n){return void 0===n&&(n=[]),t.forEach((function(t){var i;return Zn(t)&&!e||xr(t,1)?(i=n).push.apply(i,wr(t)):n.push(t)}))||n},wr=function(t,e,n){return On&&!e&&On.selector?On.selector(t):!Zn(t)||n||!Rn&&Hr()?ri(t)?br(t,n):xr(t)?yr.call(t,0):t?[t]:[]:yr.call((e||Nn).querySelectorAll(t),0)},Tr=function(t){return t=wr(t)[0]||gi("Invalid scope")||{},function(e){var n=t.current||t.nativeElement||t;return wr(e,n.querySelectorAll?n:n===t?gi("Invalid scope")||Nn.createElement("div"):t)}},Ar=function(t){return t.sort((function(){return.5-Math.random()}))},Cr=function(t){if(Gn(t))return t;var e=Jn(t)?t:{each:t},n=Qr(e.ease),i=e.from||0,r=parseFloat(e.base)||0,o={},s=i>0&&i<1,a=isNaN(i)||s,l=e.axis,u=i,c=i;return Zn(i)?u=c={center:.5,edges:.5,end:1}[i]||0:!s&&a&&(u=i[0],c=i[1]),function(t,s,h){var d,p,f,m,g,_,v,y,x,b=(h||e).length,w=o[b];if(!w){if(!(x="auto"===e.grid?0:(e.grid||[1,1e8])[1])){for(v=-1e8;v<(v=h[x++].getBoundingClientRect().left)&&xv&&(v=g),gb?b-1:l?"y"===l?b/x:x:Math.max(x,b/x))||0)*("edges"===i?-1:1),w.b=b<0?r-b:r,w.u=vr(e.amount||e.each)||0,n=n&&b<0?Gr(n):n}return b=(w[t]-w.min)/w.max||0,Ri(w.b+(n?n(b):b)*w.v)+w.u}},Mr=function(t){var e=Math.pow(10,((t+"").split(".")[1]||"").length);return function(n){var i=Ri(Math.round(parseFloat(n)/t)*t*e);return(i-i%1)/e+(Kn(n)?0:vr(n))}},kr=function(t,e){var n,i,r=ri(t);return!r&&Jn(t)&&(n=r=t.radius||1e8,t.values?(t=wr(t.values),(i=!Kn(t[0]))&&(n*=n)):t=Mr(t.increment)),gr(e,r?Gn(t)?function(e){return i=t(e),Math.abs(i-e)<=n?i:e}:function(e){for(var r,o,s=parseFloat(i?e.x:e),a=parseFloat(i?e.y:0),l=1e8,u=0,c=t.length;c--;)(r=i?(r=t[c].x-s)*r+(o=t[c].y-a)*o:Math.abs(t[c]-s))(r=Math.abs(r))&&(o=i,a=r);return o},Pr=function(t,e,n){var i,r,o,s=t.vars,a=s[e],l=On,u=t._ctx;if(a)return i=s[e+"Params"],r=s.callbackScope||t,n&&Ti.length&&Fi(),u&&(On=u),o=i?a.apply(r,i):a.call(r),On=l,o},Dr=function(t){return Zi(t),t.scrollTrigger&&t.scrollTrigger.kill(!!$n),t.progress()<1&&Pr(t,"onInterrupt"),t},Rr=function(t){var e=(t=!t.name&&t.default||t).name,n=Gn(t),i=e&&!n&&t.init?function(){this._props=[]}:t,r={init:vi,render:Mo,add:uo,kill:Eo,modifier:ko,rawVars:0},o={targetTest:0,get:0,getSetter:wo,aliases:{},register:0};if(Hr(),t!==i){if(Ci[e])return;ji(i,ji(Hi(t,r),o)),Xi(i.prototype,Xi(r,Hi(t,o))),Ci[i.prop=e]=i,t.targetTest&&(Ei.push(i),wi[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}_i(e,i),t.register&&t.register(Xo,i,$o)},Nr={aqua:[0,255,255],lime:[0,255,0],silver:[192,192,192],black:[0,0,0],maroon:[128,0,0],teal:[0,128,128],blue:[0,0,255],navy:[0,0,128],white:[255,255,255],olive:[128,128,0],yellow:[255,255,0],orange:[255,165,0],gray:[128,128,128],purple:[128,0,128],green:[0,128,0],red:[255,0,0],pink:[255,192,203],cyan:[0,255,255],transparent:[255,255,255,0]},zr=function(t,e,n){return 255*(6*(t+=t<0?1:t>1?-1:0)<1?e+(n-e)*t*6:t<.5?n:3*t<2?e+(n-e)*(2/3-t)*6:e)+.5|0},Fr=function(t,e,n){var i,r,o,s,a,l,u,c,h,d,p=t?Kn(t)?[t>>16,t>>8&255,255&t]:0:Nr.black;if(!p){if(","===t.substr(-1)&&(t=t.substr(0,t.length-1)),Nr[t])p=Nr[t];else if("#"===t.charAt(0)){if(t.length<6&&(i=t.charAt(1),r=t.charAt(2),o=t.charAt(3),t="#"+i+i+r+r+o+o+(5===t.length?t.charAt(4)+t.charAt(4):"")),9===t.length)return[(p=parseInt(t.substr(1,6),16))>>16,p>>8&255,255&p,parseInt(t.substr(7),16)/255];p=[(t=parseInt(t.substr(1),16))>>16,t>>8&255,255&t]}else if("hsl"===t.substr(0,3))if(p=d=t.match(oi),e){if(~t.indexOf("="))return p=t.match(si),n&&p.length<4&&(p[3]=1),p}else s=+p[0]%360/360,a=+p[1]/100,i=2*(l=+p[2]/100)-(r=l<=.5?l*(a+1):l+a-l*a),p.length>3&&(p[3]*=1),p[0]=zr(s+1/3,i,r),p[1]=zr(s,i,r),p[2]=zr(s-1/3,i,r);else p=t.match(oi)||Nr.transparent;p=p.map(Number)}return e&&!d&&(i=p[0]/255,r=p[1]/255,o=p[2]/255,l=((u=Math.max(i,r,o))+(c=Math.min(i,r,o)))/2,u===c?s=a=0:(h=u-c,a=l>.5?h/(2-u-c):h/(u+c),s=u===i?(r-o)/h+(ra&&(u+=y-l),((m=(_=(c+=y)-u)-d)>0||x)&&(v=++i.frame,r=_-1e3*i.time,i.time=_/=1e3,d+=m+(m>=h?4:h-m),g=1),x||(t=e(n)),g)for(o=0;o=e&&o--},_listeners:p},i}(),Hr=function(){return!Bn&&Yr.wake()},Ur={},Wr=/^[\d.\-M][\d.\-,\s]/,qr=/["']/g,Zr=function(t){for(var e,n,i,r={},o=t.substr(1,t.length-3).split(":"),s=o[0],a=1,l=o.length;a1&&s.config?s.config.apply(null,~t.indexOf("{")?[Zr(o[1])]:(e=t,n=e.indexOf("(")+1,i=e.indexOf(")"),r=e.indexOf("(",n),e.substring(n,~r&&r=1?n:1,o=(i||(e?.3:.45))/(n<1?n:1),s=o/Xn*(Math.asin(1/r)||0),a=function(t){return 1===t?1:r*Math.pow(2,-10*t)*qn((t-s)*o)+1},l="out"===e?a:"in"===e?function(t){return 1-a(1-t)}:to(a);return o=Xn/o,l.config=function(n,i){return t(e,n,i)},l},no=function t(e,n){void 0===n&&(n=1.70158);var i=function(t){return t?--t*t*((n+1)*t+n)+1:0},r="out"===e?i:"in"===e?function(t){return 1-i(1-t)}:to(i);return r.config=function(n){return t(e,n)},r};Pi("Linear,Quad,Cubic,Quart,Quint,Strong",(function(t,e){var n=e<5?e+1:e;Jr(t+",Power"+(n-1),e?function(t){return Math.pow(t,n)}:function(t){return t},(function(t){return 1-Math.pow(1-t,n)}),(function(t){return t<.5?Math.pow(2*t,n)/2:1-Math.pow(2*(1-t),n)/2}))})),Ur.Linear.easeNone=Ur.none=Ur.Linear.easeIn,Jr("Elastic",eo("in"),eo("out"),eo()),function(t,e){var n=1/e,i=function(i){return i0?t+(t+this._rDelay)*this._repeat:t):this.totalDuration()&&this._dur},e.totalDuration=function(t){return arguments.length?(this._dirty=0,hr(this,this._repeat<0?t:(t-this._repeat*this._rDelay)/(this._repeat+1))):this._tDur},e.totalTime=function(t,e){if(Hr(),!arguments.length)return this._tTime;var n=this._dp;if(n&&n.smoothChildTiming&&this._ts){for(rr(this,t),!n._dp||n.parent||or(n,this);n&&n.parent;)n.parent._time!==n._start+(n._ts>=0?n._tTime/n._ts:(n.totalDuration()-n._tTime)/-n._ts)&&n.totalTime(n._tTime,!0),n=n.parent;!this.parent&&this._dp.autoRemoveChildren&&(this._ts>0&&t0||!this._tDur&&!t)&&sr(this._dp,this,this._start-this._delay)}return(this._tTime!==t||!this._dur&&!e||this._initted&&1e-8===Math.abs(this._zTime)||!t&&!this._initted&&(this.add||this._ptLookup))&&(this._ts||(this._pTime=t),Ii(this,t,e)),this},e.time=function(t,e){return arguments.length?this.totalTime(Math.min(this.totalDuration(),t+tr(this))%(this._dur+this._rDelay)||(t?this._dur:0),e):this._time},e.totalProgress=function(t,e){return arguments.length?this.totalTime(this.totalDuration()*t,e):this.totalDuration()?Math.min(1,this._tTime/this._tDur):this.ratio},e.progress=function(t,e){return arguments.length?this.totalTime(this.duration()*(!this._yoyo||1&this.iteration()?t:1-t)+tr(this),e):this.duration()?Math.min(1,this._time/this._dur):this.ratio},e.iteration=function(t,e){var n=this.duration()+this._rDelay;return arguments.length?this.totalTime(this._time+(t-1)*n,e):this._repeat?er(this._tTime,n)+1:1},e.timeScale=function(t){if(!arguments.length)return-1e-8===this._rts?0:this._rts;if(this._rts===t)return this;var e=this.parent&&this._ts?nr(this.parent._time,this):this._tTime;return this._rts=+t||0,this._ts=this._ps||-1e-8===t?0:this._rts,this.totalTime(_r(-this._delay,this._tDur,e),!0),ir(this),Ki(this)},e.paused=function(t){return arguments.length?(this._ps!==t&&(this._ps=t,t?(this._pTime=this._tTime||Math.max(-this._delay,this.rawTime()),this._ts=this._act=0):(Hr(),this._ts=this._rts,this.totalTime(this.parent&&!this.parent.smoothChildTiming?this.rawTime():this._tTime||this._pTime,1===this.progress()&&1e-8!==Math.abs(this._zTime)&&(this._tTime-=1e-8)))),this):this._ps},e.startTime=function(t){if(arguments.length){this._start=t;var e=this.parent||this._dp;return e&&(e._sort||!this.parent)&&sr(e,this,t-this._delay),this}return this._start},e.endTime=function(t){return this._start+(ti(t)?this.totalDuration():this.duration())/Math.abs(this._ts||1)},e.rawTime=function(t){var e=this.parent||this._dp;return e?t&&(!this._ts||this._repeat&&this._time&&this.totalProgress()<1)?this._tTime%(this._dur+this._rDelay):this._ts?nr(e.rawTime(t),this):this._tTime:this._tTime},e.revert=function(t){void 0===t&&(t=bi);var e=$n;return $n=t,(this._initted||this._startAt)&&(this.timeline&&this.timeline.revert(t),this.totalTime(-.01,t.suppressEvents)),"nested"!==this.data&&!1!==t.kill&&this.kill(),$n=e,this},e.globalTime=function(t){for(var e=this,n=arguments.length?t:e.rawTime();e;)n=e._start+n/(e._ts||1),e=e._dp;return!this.parent&&this.vars.immediateRender?-1:n},e.repeat=function(t){return arguments.length?(this._repeat=t===1/0?-2:t,dr(this)):-2===this._repeat?1/0:this._repeat},e.repeatDelay=function(t){if(arguments.length){var e=this._time;return this._rDelay=t,dr(this),e?this.time(e):this}return this._rDelay},e.yoyo=function(t){return arguments.length?(this._yoyo=t,this):this._yoyo},e.seek=function(t,e){return this.totalTime(fr(this,t),ti(e))},e.restart=function(t,e){return this.play().totalTime(t?-this._delay:0,ti(e))},e.play=function(t,e){return null!=t&&this.seek(t,e),this.reversed(!1).paused(!1)},e.reverse=function(t,e){return null!=t&&this.seek(t||this.totalDuration(),e),this.reversed(!0).paused(!1)},e.pause=function(t,e){return null!=t&&this.seek(t,e),this.paused(!0)},e.resume=function(){return this.paused(!1)},e.reversed=function(t){return arguments.length?(!!t!==this.reversed()&&this.timeScale(-this._rts||(t?-1e-8:0)),this):this._rts<0},e.invalidate=function(){return this._initted=this._act=0,this._zTime=-1e-8,this},e.isActive=function(){var t,e=this.parent||this._dp,n=this._start;return!(e&&!(this._ts&&this._initted&&e.isActive()&&(t=e.rawTime(!0))>=n&&t1?(e?(i[t]=e,n&&(i[t+"Params"]=n),"onUpdate"===t&&(this._onUpdate=e)):delete i[t],this):i[t]},e.then=function(t){var e=this;return new Promise((function(n){var i=Gn(t)?t:Vi,r=function(){var t=e.then;e.then=null,Gn(i)&&(i=i(e))&&(i.then||i===e)&&(e.then=t),n(i),e.then=t};e._initted&&1===e.totalProgress()&&e._ts>=0||!e._tTime&&e._ts<0?r():e._prom=r}))},e.kill=function(){Dr(this)},t}();ji(ro.prototype,{_time:0,_start:0,_end:0,_tTime:0,_tDur:0,_dirty:0,_repeat:0,_yoyo:!1,parent:null,_initted:!1,_rDelay:0,_ts:1,_dp:0,ratio:0,_zTime:-1e-8,_prom:0,_ps:!1,_rts:1});var oo=function(t){function e(e,n){var i;return void 0===e&&(e={}),(i=t.call(this,e)||this).labels={},i.smoothChildTiming=!!e.smoothChildTiming,i.autoRemoveChildren=!!e.autoRemoveChildren,i._sort=ti(e.sortChildren),Pn&&sr(e.parent||Pn,En(i),n),e.reversed&&i.reverse(),e.paused&&i.paused(!0),e.scrollTrigger&&ar(En(i),e.scrollTrigger),i}Sn(e,t);var n=e.prototype;return n.to=function(t,e,n){return mr(0,arguments,this),this},n.from=function(t,e,n){return mr(1,arguments,this),this},n.fromTo=function(t,e,n,i){return mr(2,arguments,this),this},n.set=function(t,e,n){return e.duration=0,e.parent=this,Ui(e).repeatDelay||(e.repeat=0),e.immediateRender=!!e.immediateRender,new _o(t,e,fr(this,n),1),this},n.call=function(t,e,n){return sr(this,_o.delayedCall(0,t,e),n)},n.staggerTo=function(t,e,n,i,r,o,s){return n.duration=e,n.stagger=n.stagger||i,n.onComplete=o,n.onCompleteParams=s,n.parent=this,new _o(t,n,fr(this,r)),this},n.staggerFrom=function(t,e,n,i,r,o,s){return n.runBackwards=1,Ui(n).immediateRender=ti(n.immediateRender),this.staggerTo(t,e,n,i,r,o,s)},n.staggerFromTo=function(t,e,n,i,r,o,s,a){return i.startAt=n,Ui(i).immediateRender=ti(i.immediateRender),this.staggerTo(t,e,i,r,o,s,a)},n.render=function(t,e,n){var i,r,o,s,a,l,u,c,h,d,p,f,m=this._time,g=this._dirty?this.totalDuration():this._tDur,_=this._dur,v=t<=0?0:Ri(t),y=this._zTime<0!=t<0&&(this._initted||!_);if(this!==Pn&&v>g&&t>=0&&(v=g),v!==this._tTime||n||y){if(m!==this._time&&_&&(v+=this._time-m,t+=this._time-m),i=v,h=this._start,l=!(c=this._ts),y&&(_||(m=this._zTime),(t||!e)&&(this._zTime=t)),this._repeat){if(p=this._yoyo,a=_+this._rDelay,this._repeat<-1&&t<0)return this.totalTime(100*a+t,e,n);if(i=Ri(v%a),v===g?(s=this._repeat,i=_):((s=~~(v/a))&&s===v/a&&(i=_,s--),i>_&&(i=_)),d=er(this._tTime,a),!m&&this._tTime&&d!==s&&(d=s),p&&1&s&&(i=_-i,f=1),s!==d&&!this._lock){var x=p&&1&d,b=x===(p&&1&s);if(se)for(i=t._first;i&&i._start<=n;){if("isPause"===i.data&&i._start>e)return i;i=i._next}else for(i=t._last;i&&i._start>=n;){if("isPause"===i.data&&i._start=m&&t>=0)for(r=this._first;r;){if(o=r._next,(r._act||i>=r._start)&&r._ts&&u!==r){if(r.parent!==this)return this.render(t,e,n);if(r.render(r._ts>0?(i-r._start)*r._ts:(r._dirty?r.totalDuration():r._tDur)+(i-r._start)*r._ts,e,n),i!==this._time||!this._ts&&!l){u=0,o&&(v+=this._zTime=-1e-8);break}}r=o}else{r=this._last;for(var w=t<0?t:i;r;){if(o=r._prev,(r._act||w<=r._end)&&r._ts&&u!==r){if(r.parent!==this)return this.render(t,e,n);if(r.render(r._ts>0?(w-r._start)*r._ts:(r._dirty?r.totalDuration():r._tDur)+(w-r._start)*r._ts,e,n||$n&&(r._initted||r._startAt)),i!==this._time||!this._ts&&!l){u=0,o&&(v+=this._zTime=w?-1e-8:1e-8);break}}r=o}}if(u&&!e&&(this.pause(),u.render(i>=m?0:-1e-8)._zTime=i>=m?1:-1,this._ts))return this._start=h,ir(this),this.render(t,e,n);this._onUpdate&&!e&&Pr(this,"onUpdate",!0),(v===g&&this._tTime>=this.totalDuration()||!v&&m)&&(h!==this._start&&Math.abs(c)===Math.abs(this._ts)||this._lock||((t||!_)&&(v===g&&this._ts>0||!v&&this._ts<0)&&Zi(this,1),e||t<0&&!m||!v&&!m&&g||(Pr(this,v===g&&t>=0?"onComplete":"onReverseComplete",!0),this._prom&&!(v0)&&this._prom())))}return this},n.add=function(t,e){var n=this;if(Kn(e)||(e=fr(this,e,t)),!(t instanceof ro)){if(ri(t))return t.forEach((function(t){return n.add(t,e)})),this;if(Zn(t))return this.addLabel(t,e);if(!Gn(t))return this;t=_o.delayedCall(0,t)}return this!==t?sr(this,t,e):this},n.getChildren=function(t,e,n,i){void 0===t&&(t=!0),void 0===e&&(e=!0),void 0===n&&(n=!0),void 0===i&&(i=-1e8);for(var r=[],o=this._first;o;)o._start>=i&&(o instanceof _o?e&&r.push(o):(n&&r.push(o),t&&r.push.apply(r,o.getChildren(!0,e,n)))),o=o._next;return r},n.getById=function(t){for(var e=this.getChildren(1,1,1),n=e.length;n--;)if(e[n].vars.id===t)return e[n]},n.remove=function(t){return Zn(t)?this.removeLabel(t):Gn(t)?this.killTweensOf(t):(qi(this,t),t===this._recent&&(this._recent=this._last),Gi(this))},n.totalTime=function(e,n){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=Ri(Yr.time-(this._ts>0?e/this._ts:(this.totalDuration()-e)/-this._ts))),t.prototype.totalTime.call(this,e,n),this._forcing=0,this):this._tTime},n.addLabel=function(t,e){return this.labels[t]=fr(this,e),this},n.removeLabel=function(t){return delete this.labels[t],this},n.addPause=function(t,e,n){var i=_o.delayedCall(0,e||vi,n);return i.data="isPause",this._hasPause=1,sr(this,i,fr(this,t))},n.removePause=function(t){var e=this._first;for(t=fr(this,t);e;)e._start===t&&"isPause"===e.data&&Zi(e),e=e._next},n.killTweensOf=function(t,e,n){for(var i=this.getTweensOf(t,n),r=i.length;r--;)so!==i[r]&&i[r].kill(t,e);return this},n.getTweensOf=function(t,e){for(var n,i=[],r=wr(t),o=this._first,s=Kn(e);o;)o instanceof _o?zi(o._targets,r)&&(s?(!so||o._initted&&o._ts)&&o.globalTime(0)<=e&&o.globalTime(o.totalDuration())>e:!e||o.isActive())&&i.push(o):(n=o.getTweensOf(r,e)).length&&i.push.apply(i,n),o=o._next;return i},n.tweenTo=function(t,e){e=e||{};var n,i=this,r=fr(i,t),o=e,s=o.startAt,a=o.onStart,l=o.onStartParams,u=o.immediateRender,c=_o.to(i,ji({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:r,overwrite:"auto",duration:e.duration||Math.abs((r-(s&&"time"in s?s.time:i._time))/i.timeScale())||1e-8,onStart:function(){if(i.pause(),!n){var t=e.duration||Math.abs((r-(s&&"time"in s?s.time:i._time))/i.timeScale());c._dur!==t&&hr(c,t,0,1).render(c._time,!0,!0),n=1}a&&a.apply(c,l||[])}},e));return u?c.render(0):c},n.tweenFromTo=function(t,e,n){return this.tweenTo(e,ji({startAt:{time:fr(this,t)}},n))},n.recent=function(){return this._recent},n.nextLabel=function(t){return void 0===t&&(t=this._time),Or(this,fr(this,t))},n.previousLabel=function(t){return void 0===t&&(t=this._time),Or(this,fr(this,t),1)},n.currentLabel=function(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+1e-8)},n.shiftChildren=function(t,e,n){void 0===n&&(n=0);for(var i,r=this._first,o=this.labels;r;)r._start>=n&&(r._start+=t,r._end+=t),r=r._next;if(e)for(i in o)o[i]>=n&&(o[i]+=t);return Gi(this)},n.invalidate=function(e){var n=this._first;for(this._lock=0;n;)n.invalidate(e),n=n._next;return t.prototype.invalidate.call(this,e)},n.clear=function(t){void 0===t&&(t=!0);for(var e,n=this._first;n;)e=n._next,this.remove(n),n=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Gi(this)},n.totalDuration=function(t){var e,n,i,r=0,o=this,s=o._last,a=1e8;if(arguments.length)return o.timeScale((o._repeat<0?o.duration():o.totalDuration())/(o.reversed()?-t:t));if(o._dirty){for(i=o.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),(n=s._start)>a&&o._sort&&s._ts&&!o._lock?(o._lock=1,sr(o,s,n-s._delay,1)._lock=0):a=n,n<0&&s._ts&&(r-=n,(!i&&!o._dp||i&&i.smoothChildTiming)&&(o._start+=n/o._ts,o._time-=n,o._tTime-=n),o.shiftChildren(-n,!1,-Infinity),a=0),s._end>r&&s._ts&&(r=s._end),s=e;hr(o,o===Pn&&o._time>r?o._time:r,1,1),o._dirty=0}return o._tDur},e.updateRoot=function(t){if(Pn._ts&&(Ii(Pn,nr(t,Pn)),Fn=Yr.frame),Yr.frame>=ki){ki+=Vn.autoSleep||120;var e=Pn._first;if((!e||!e._ts)&&Vn.autoSleep&&Yr._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Yr.sleep()}}},e}(ro);ji(oo.prototype,{_lock:0,_hasPause:0,_forcing:0});var so,ao,lo=function(t,e,n,i,r,o,s){var a,l,u,c,h,d,p,f,m=new $o(this._pt,t,e,0,1,Co,null,r),g=0,_=0;for(m.b=n,m.e=i,n+="",(p=~(i+="").indexOf("random("))&&(i=Lr(i)),o&&(o(f=[n,i],t,e),n=f[0],i=f[1]),l=n.match(li)||[];a=li.exec(i);)c=a[0],h=i.substring(g,a.index),u?u=(u+1)%5:"rgba("===h.substr(-5)&&(u=1),c!==l[_++]&&(d=parseFloat(l[_-1])||0,m._pt={_next:m._pt,p:h||1===_?h:",",s:d,c:"="===c.charAt(1)?Ni(d,c)-d:parseFloat(c)-d,m:u&&u<4?Math.round:0},g=li.lastIndex);return m.c=g")})),s.duration();else{for(c in l={},x)"ease"===c||"easeEach"===c||po(c,x[c],l,x.easeEach);for(c in l)for(M=l[c].sort((function(t,e){return t.t-e.t})),S=0,a=0;ap-1e-8&&!m?p:t<1e-8?0:t;if(f){if(g!==this._tTime||!t||n||!this._initted&&this._tTime||this._startAt&&this._zTime<0!==m){if(i=g,c=this.timeline,this._repeat){if(s=f+this._rDelay,this._repeat<-1&&m)return this.totalTime(100*s+t,e,n);if(i=Ri(g%s),g===p?(o=this._repeat,i=f):((o=~~(g/s))&&o===g/s&&(i=f,o--),i>f&&(i=f)),(l=this._yoyo&&1&o)&&(h=this._yEase,i=f-i),a=er(this._tTime,s),i===d&&!n&&this._initted)return this._tTime=g,this;o!==a&&(c&&this._yEase&&Kr(c,l),!this.vars.repeatRefresh||l||this._lock||(this._lock=n=1,this.render(Ri(s*o),!0).invalidate()._lock=0))}if(!this._initted){if(lr(this,m?t:i,n,e,g))return this._tTime=0,this;if(d!==this._time)return this;if(f!==this._dur)return this.render(t,e,n)}if(this._tTime=g,this._time=i,!this._act&&this._ts&&(this._act=1,this._lazy=0),this.ratio=u=(h||this._ease)(i/f),this._from&&(this.ratio=u=1-u),i&&!d&&!e&&(Pr(this,"onStart"),this._tTime!==g))return this;for(r=this._pt;r;)r.r(u,r.d),r=r._next;c&&c.render(t<0?t:!i&&l?-1e-8:c._dur*c._ease(i/this._dur),e,n)||this._startAt&&(this._zTime=t),this._onUpdate&&!e&&(m&&Qi(this,t,0,n),Pr(this,"onUpdate")),this._repeat&&o!==a&&this.vars.onRepeat&&!e&&this.parent&&Pr(this,"onRepeat"),g!==this._tDur&&g||this._tTime!==g||(m&&!this._onUpdate&&Qi(this,t,0,!0),(t||!f)&&(g===this._tDur&&this._ts>0||!g&&this._ts<0)&&Zi(this,1),e||m&&!d||!(g||d||l)||(Pr(this,g===p?"onComplete":"onReverseComplete",!0),this._prom&&!(g0)&&this._prom()))}}else!function(t,e,n,i){var r,o,s,a=t.ratio,l=e<0||!e&&(!t._start&&ur(t)&&(t._initted||!cr(t))||(t._ts<0||t._dp._ts<0)&&!cr(t))?0:1,u=t._rDelay,c=0;if(u&&t._repeat&&(c=_r(0,t._tDur,e),o=er(c,u),t._yoyo&&1&o&&(l=1-l),o!==er(t._tTime,u)&&(a=1-l,t.vars.repeatRefresh&&t._initted&&t.invalidate())),l!==a||$n||i||1e-8===t._zTime||!e&&t._zTime){if(!t._initted&&lr(t,e,i,n,c))return;for(s=t._zTime,t._zTime=e||(n?1e-8:0),n||(n=e&&!s),t.ratio=l,t._from&&(l=1-l),t._time=0,t._tTime=c,r=t._pt;r;)r.r(l,r.d),r=r._next;e<0&&Qi(t,e,0,!0),t._onUpdate&&!n&&Pr(t,"onUpdate"),c&&t._repeat&&!n&&t.parent&&Pr(t,"onRepeat"),(e>=t._tDur||e<0)&&t.ratio===l&&(l&&Zi(t,1),n||$n||(Pr(t,l?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,n);return this},n.targets=function(){return this._targets},n.invalidate=function(e){return(!e||!this.vars.runBackwards)&&(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(e),t.prototype.invalidate.call(this,e)},n.resetTo=function(t,e,n,i){Bn||Yr.wake(),this._ts||this.play();var r=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||ho(this,r),function(t,e,n,i,r,o,s){var a,l,u,c,h=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!h)for(h=t._ptCache[e]=[],u=t._ptLookup,c=t._targets.length;c--;){if((a=u[c][e])&&a.d&&a.d._pt)for(a=a.d._pt;a&&a.p!==e&&a.fp!==e;)a=a._next;if(!a)return ao=1,t.vars[e]="+=0",ho(t,s),ao=0,1;h.push(a)}for(c=h.length;c--;)(a=(l=h[c])._pt||l).s=!i&&0!==i||r?a.s+(i||0)+o*a.c:i,a.c=n-a.s,l.e&&(l.e=Di(n)+vr(l.e)),l.b&&(l.b=a.s+vr(l.b))}(this,t,e,n,i,this._ease(r/this._dur),r)?this.resetTo(t,e,n,i):(rr(this,0),this.parent||Wi(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},n.kill=function(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?Dr(this):this;if(this.timeline){var n=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,so&&!0!==so.vars.overwrite)._first||Dr(this),this.parent&&n!==this.timeline.totalDuration()&&hr(this,this._dur*this.timeline._tDur/n,0,1),this}var i,r,o,s,a,l,u,c=this._targets,h=t?wr(t):c,d=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function(t,e){for(var n=t.length,i=n===e.length;i&&n--&&t[n]===e[n];);return n<0}(c,h))return"all"===e&&(this._pt=0),Dr(this);for(i=this._op=this._op||[],"all"!==e&&(Zn(e)&&(a={},Pi(e,(function(t){return a[t]=1})),e=a),e=function(t,e){var n,i,r,o,s=t[0]?$i(t[0]).harness:0,a=s&&s.aliases;if(!a)return e;for(i in n=Xi({},e),a)if(i in n)for(r=(o=a[i].split(",")).length;r--;)n[o[r]]=n[i];return n}(c,e)),u=c.length;u--;)if(~h.indexOf(c[u]))for(a in r=d[u],"all"===e?(i[u]=e,s=r,o={}):(o=i[u]=i[u]||{},s=e),s)(l=r&&r[a])&&("kill"in l.d&&!0!==l.d.kill(a)||qi(this,l,"_pt"),delete r[a]),"all"!==o&&(o[a]=1);return this._initted&&!this._pt&&p&&Dr(this),this},e.to=function(t,n){return new e(t,n,arguments[2])},e.from=function(t,e){return mr(1,arguments)},e.delayedCall=function(t,n,i,r){return new e(n,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:n,onReverseComplete:n,onCompleteParams:i,onReverseCompleteParams:i,callbackScope:r})},e.fromTo=function(t,e,n){return mr(2,arguments)},e.set=function(t,n){return n.duration=0,n.repeatDelay||(n.repeat=0),new e(t,n)},e.killTweensOf=function(t,e,n){return Pn.killTweensOf(t,e,n)},e}(ro);ji(_o.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),Pi("staggerTo,staggerFrom,staggerFromTo",(function(t){_o[t]=function(){var e=new oo,n=yr.call(arguments,0);return n.splice("staggerFromTo"===t?5:4,0,0),e[t].apply(e,n)}}));var vo=function(t,e,n){return t[e]=n},yo=function(t,e,n){return t[e](n)},xo=function(t,e,n,i){return t[e](i.fp,n)},bo=function(t,e,n){return t.setAttribute(e,n)},wo=function(t,e){return Gn(t[e])?yo:Qn(t[e])&&t.setAttribute?bo:vo},To=function(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},Ao=function(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},Co=function(t,e){var n=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;n;)i=n.p+(n.m?n.m(n.s+n.c*t):Math.round(1e4*(n.s+n.c*t))/1e4)+i,n=n._next;i+=e.c}e.set(e.t,e.p,i,e)},Mo=function(t,e){for(var n=e._pt;n;)n.r(t,n.d),n=n._next},ko=function(t,e,n,i){for(var r,o=this._pt;o;)r=o._next,o.p===i&&o.modifier(t,e,n),o=r},Eo=function(t){for(var e,n,i=this._pt;i;)n=i._next,i.p===t&&!i.op||i.op===t?qi(this,i,"_pt"):i.dep||(e=1),i=n;return!e},So=function(t,e,n,i){i.mSet(t,e,i.m.call(i.tween,n,i.mt),i)},Lo=function(t){for(var e,n,i,r,o=t._pt;o;){for(e=o._next,n=i;n&&n.pr>o.pr;)n=n._next;(o._prev=n?n._prev:r)?o._prev._next=o:i=o,(o._next=n)?n._prev=o:r=o,o=e}t._pt=i},$o=function(){function t(t,e,n,i,r,o,s,a,l){this.t=e,this.s=i,this.c=r,this.p=n,this.r=o||To,this.d=s||this,this.set=a||vo,this.pr=l||0,this._next=t,t&&(t._prev=this)}return t.prototype.modifier=function(t,e,n){this.mSet=this.mSet||this.set,this.set=So,this.m=t,this.mt=n,this.tween=e},t}();Pi(Si+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",(function(t){return wi[t]=1})),di.TweenMax=di.TweenLite=_o,di.TimelineLite=di.TimelineMax=oo,Pn=new oo({sortChildren:!1,defaults:jn,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),Vn.stringFilter=Xr;var Oo=[],Po={},Do=[],Ro=0,No=function(t){return(Po[t]||Do).map((function(t){return t()}))},zo=function(){var t=Date.now(),e=[];t-Ro>2&&(No("matchMediaInit"),Oo.forEach((function(t){var n,i,r,o,s=t.queries,a=t.conditions;for(i in s)(n=Dn.matchMedia(s[i]).matches)&&(r=1),n!==a[i]&&(a[i]=n,o=1);o&&(t.revert(),r&&e.push(t))})),No("matchMediaRevert"),e.forEach((function(t){return t.onMatch(t)})),Ro=t,No("matchMedia"))},Fo=function(){function t(t,e){this.selector=e&&Tr(e),this.data=[],this._r=[],this.isReverted=!1,t&&this.add(t)}var e=t.prototype;return e.add=function(t,e,n){Gn(t)&&(n=e,e=t,t=Gn);var i=this,r=function(){var t,r=On,o=i.selector;return r&&r!==i&&r.data.push(i),n&&(i.selector=Tr(n)),On=i,t=e.apply(i,arguments),Gn(t)&&i._r.push(t),On=r,i.selector=o,i.isReverted=!1,t};return i.last=r,t===Gn?r(i):t?i[t]=r:r},e.ignore=function(t){var e=On;On=null,t(this),On=e},e.getTweens=function(){var e=[];return this.data.forEach((function(n){return n instanceof t?e.push.apply(e,n.getTweens()):n instanceof _o&&!(n.parent&&"nested"===n.parent.data)&&e.push(n)})),e},e.clear=function(){this._r.length=this.data.length=0},e.kill=function(t,e){var n=this;if(t){var i=this.getTweens();this.data.forEach((function(t){"isFlip"===t.data&&(t.revert(),t.getChildren(!0,!0,!1).forEach((function(t){return i.splice(i.indexOf(t),1)})))})),i.map((function(t){return{g:t.globalTime(0),t:t}})).sort((function(t,e){return e.g-t.g||-1})).forEach((function(e){return e.t.revert(t)})),this.data.forEach((function(e){return!(e instanceof ro)&&e.revert&&e.revert(t)})),this._r.forEach((function(e){return e(t,n)})),this.isReverted=!0}else this.data.forEach((function(t){return t.kill&&t.kill()}));if(this.clear(),e){var r=Oo.indexOf(this);~r&&Oo.splice(r,1)}},e.revert=function(t){this.kill(t||{})},t}(),Io=function(){function t(t){this.contexts=[],this.scope=t}var e=t.prototype;return e.add=function(t,e,n){Jn(t)||(t={matches:t});var i,r,o,s=new Fo(0,n||this.scope),a=s.conditions={};for(r in this.contexts.push(s),e=s.add("onMatch",e),s.queries=t,t)"all"===r?o=1:(i=Dn.matchMedia(t[r]))&&(Oo.indexOf(s)<0&&Oo.push(s),(a[r]=i.matches)&&(o=1),i.addListener?i.addListener(zo):i.addEventListener("change",zo));return o&&e(s),this},e.revert=function(t){this.kill(t||{})},e.kill=function(t){this.contexts.forEach((function(e){return e.kill(t,!0)}))},t}(),Bo={registerPlugin:function(){for(var t=arguments.length,e=new Array(t),n=0;n1){var i=t.map((function(t){return Xo.quickSetter(t,e,n)})),r=i.length;return function(t){for(var e=r;e--;)i[e](t)}}t=t[0]||{};var o=Ci[e],s=$i(t),a=s.harness&&(s.harness.aliases||{})[e]||e,l=o?function(e){var i=new o;In._pt=0,i.init(t,n?e+n:e,In,0,[t]),i.render(1,i),In._pt&&Mo(1,In)}:s.set(t,a);return o?l:function(e){return l(t,a,n?e+n:e,s,1)}},quickTo:function(t,e,n){var i,r=Xo.to(t,Xi(((i={})[e]="+=0.1",i.paused=!0,i),n||{})),o=function(t,n,i){return r.resetTo(e,t,n,i)};return o.tween=r,o},isTweening:function(t){return Pn.getTweensOf(t,!0).length>0},defaults:function(t){return t&&t.ease&&(t.ease=Qr(t.ease,jn.ease)),Yi(jn,t||{})},config:function(t){return Yi(Vn,t||{})},registerEffect:function(t){var e=t.name,n=t.effect,i=t.plugins,r=t.defaults,o=t.extendTimeline;(i||"").split(",").forEach((function(t){return t&&!Ci[t]&&!di[t]&&gi(e+" effect requires "+t+" plugin.")})),Mi[e]=function(t,e,i){return n(wr(t),ji(e||{},r),i)},o&&(oo.prototype[e]=function(t,n,i){return this.add(Mi[e](t,Jn(n)?n:(i=n)&&{},this),i)})},registerEase:function(t,e){Ur[t]=Qr(e)},parseEase:function(t,e){return arguments.length?Qr(t,e):Ur},getById:function(t){return Pn.getById(t)},exportRoot:function(t,e){void 0===t&&(t={});var n,i,r=new oo(t);for(r.smoothChildTiming=ti(t.smoothChildTiming),Pn.remove(r),r._dp=0,r._time=r._tTime=Pn._time,n=Pn._first;n;)i=n._next,!e&&!n._dur&&n instanceof _o&&n.vars.onComplete===n._targets[0]||sr(r,n,n._start-n._delay),n=i;return sr(Pn,r,0),r},context:function(t,e){return t?new Fo(t,e):On},matchMedia:function(t){return new Io(t)},matchMediaRefresh:function(){return Oo.forEach((function(t){var e,n,i=t.conditions;for(n in i)i[n]&&(i[n]=!1,e=1);e&&t.revert()}))||zo()},addEventListener:function(t,e){var n=Po[t]||(Po[t]=[]);~n.indexOf(e)||n.push(e)},removeEventListener:function(t,e){var n=Po[t],i=n&&n.indexOf(e);i>=0&&n.splice(i,1)},utils:{wrap:function t(e,n,i){var r=n-e;return ri(e)?Sr(e,t(0,e.length),n):gr(i,(function(t){return(r+(t-e)%r)%r+e}))},wrapYoyo:function t(e,n,i){var r=n-e,o=2*r;return ri(e)?Sr(e,t(0,e.length-1),n):gr(i,(function(t){return e+((t=(o+(t-e)%o)%o||0)>r?o-t:t)}))},distribute:Cr,random:Er,snap:kr,normalize:function(t,e,n){return $r(t,e,0,1,n)},getUnit:vr,clamp:function(t,e,n){return gr(n,(function(n){return _r(t,e,n)}))},splitColor:Fr,toArray:wr,selector:Tr,mapRange:$r,pipe:function(){for(var t=arguments.length,e=new Array(t),n=0;n=0)return;i._gsap.svg&&(this.svgo=i.getAttribute("data-svg-origin"),this.props.push(ys,e,"")),t=vs}(r||e)&&this.props.push(t,e,r[t])},bs=function(t){t.translate&&(t.removeProperty("translate"),t.removeProperty("scale"),t.removeProperty("rotate"))},ws=function(){var t,e,n=this.props,i=this.target,r=i.style,o=i._gsap;for(t=0;t=0?Ms[r]:"")+t},Es=function(){"undefined"!=typeof window&&window.document&&(Yo=window,Ho=Yo.document,Uo=Ho.documentElement,qo=As("div")||{style:{}},As("div"),vs=ks(vs),ys=vs+"Origin",qo.style.cssText="border-width:0;line-height:0;position:absolute;padding:0",Ko=!!ks("perspective"),Go=Xo.core.reverting,Wo=1)},Ss=function t(e){var n,i=As("svg",this.ownerSVGElement&&this.ownerSVGElement.getAttribute("xmlns")||"http://www.w3.org/2000/svg"),r=this.parentNode,o=this.nextSibling,s=this.style.cssText;if(Uo.appendChild(i),i.appendChild(this),this.style.display="block",e)try{n=this.getBBox(),this._gsapBBox=this.getBBox,this.getBBox=t}catch(t){}else this._gsapBBox&&(n=this._gsapBBox());return r&&(o?r.insertBefore(this,o):r.appendChild(this)),Uo.removeChild(i),this.style.cssText=s,n},Ls=function(t,e){for(var n=e.length;n--;)if(t.hasAttribute(e[n]))return t.getAttribute(e[n])},$s=function(t){var e;try{e=t.getBBox()}catch(n){e=Ss.call(t,!0)}return e&&(e.width||e.height)||t.getBBox===Ss||(e=Ss.call(t,!0)),!e||e.width||e.x||e.y?e:{x:+Ls(t,["x","cx","x1"])||0,y:+Ls(t,["y","cy","y1"])||0,width:0,height:0}},Os=function(t){return!(!t.getCTM||t.parentNode&&!t.ownerSVGElement||!$s(t))},Ps=function(t,e){if(e){var n=t.style;e in Qo&&e!==ys&&(e=vs),n.removeProperty?("ms"!==e.substr(0,2)&&"webkit"!==e.substr(0,6)||(e="-"+e),n.removeProperty(e.replace(ns,"-$1").toLowerCase())):n.removeAttribute(e)}},Ds=function(t,e,n,i,r,o){var s=new $o(t._pt,e,n,0,1,o?hs:cs);return t._pt=s,s.b=i,s.e=r,t._props.push(n),s},Rs={deg:1,rad:1,turn:1},Ns={grid:1,flex:1},zs=function t(e,n,i,r){var o,s,a,l,u=parseFloat(i)||0,c=(i+"").trim().substr((u+"").length)||"px",h=qo.style,d=is.test(n),p="svg"===e.tagName.toLowerCase(),f=(p?"client":"offset")+(d?"Width":"Height"),m=100,g="px"===r,_="%"===r;return r===c||!u||Rs[r]||Rs[c]?u:("px"!==c&&!g&&(u=t(e,n,i,"px")),l=e.getCTM&&Os(e),!_&&"%"!==c||!Qo[n]&&!~n.indexOf("adius")?(h[d?"width":"height"]=m+(g?c:r),s=~n.indexOf("adius")||"em"===r&&e.appendChild&&!p?e:e.parentNode,l&&(s=(e.ownerSVGElement||{}).parentNode),s&&s!==Ho&&s.appendChild||(s=Ho.body),(a=s._gsap)&&_&&a.width&&d&&a.time===Yr.time&&!a.uncache?Di(u/a.width*m):((_||"%"===c)&&!Ns[Cs(s,"display")]&&(h.position=Cs(e,"position")),s===e&&(h.position="static"),s.appendChild(qo),o=qo[f],s.removeChild(qo),h.position="absolute",d&&_&&((a=$i(s)).time=Yr.time,a.width=s[f]),Di(g?o*u/m:o&&u?m/o*u:0))):(o=l?e.getBBox()[d?"width":"height"]:e[f],Di(_?u/o*m:u/100*o)))},Fs=function(t,e,n,i){var r;return Wo||Es(),e in os&&"transform"!==e&&~(e=os[e]).indexOf(",")&&(e=e.split(",")[0]),Qo[e]&&"transform"!==e?(r=Gs(t,i),r="transformOrigin"!==e?r[e]:r.svg?r.origin:Ks(Cs(t,ys))+" "+r.zOrigin+"px"):(!(r=t.style[e])||"auto"===r||i||~(r+"").indexOf("calc("))&&(r=Xs[e]&&Xs[e](t,e,n)||Cs(t,e)||Oi(t,e)||("opacity"===e?1:0)),n&&!~(r+"").trim().indexOf(" ")?zs(t,e,r,n)+n:r},Is=function(t,e,n,i){if(!n||"none"===n){var r=ks(e,t,1),o=r&&Cs(t,r,1);o&&o!==n?(e=r,n=o):"borderColor"===e&&(n=Cs(t,"borderTopColor"))}var s,a,l,u,c,h,d,p,f,m,g,_=new $o(this._pt,t.style,e,0,1,Co),v=0,y=0;if(_.b=n,_.e=i,n+="","auto"===(i+="")&&(t.style[e]=i,i=Cs(t,e)||i,t.style[e]=n),Xr(s=[n,i]),i=s[1],l=(n=s[0]).match(ai)||[],(i.match(ai)||[]).length){for(;a=ai.exec(i);)d=a[0],f=i.substring(v,a.index),c?c=(c+1)%5:"rgba("!==f.substr(-5)&&"hsla("!==f.substr(-5)||(c=1),d!==(h=l[y++]||"")&&(u=parseFloat(h)||0,g=h.substr((u+"").length),"="===d.charAt(1)&&(d=Ni(u,d)+g),p=parseFloat(d),m=d.substr((p+"").length),v=ai.lastIndex-m.length,m||(m=m||Vn.units[e]||g,v===i.length&&(i+=m,_.e+=m)),g!==m&&(u=zs(t,e,h,m)||0),_._pt={_next:_._pt,p:f||1===y?f:",",s:u,c:p-u,m:c&&c<4||"zIndex"===e?Math.round:0});_.c=v-1;)n=a[r],Qo[n]&&(i=1,n="transformOrigin"===n?ys:vs),Ps(o,n);i&&(Ps(o,vs),l&&(l.svg&&o.removeAttribute("transform"),Gs(o,1),l.uncache=1,bs(s)))}},Xs={clearProps:function(t,e,n,i,r){if("isFromStart"!==r.data){var o=t._pt=new $o(t._pt,e,n,0,0,js);return o.u=i,o.pr=-10,o.tween=r,t._props.push(n),1}}},Ys=[1,0,0,1,0,0],Hs={},Us=function(t){return"matrix(1, 0, 0, 1, 0, 0)"===t||"none"===t||!t},Ws=function(t){var e=Cs(t,vs);return Us(e)?Ys:e.substr(7).match(si).map(Di)},qs=function(t,e){var n,i,r,o,s=t._gsap||$i(t),a=t.style,l=Ws(t);return s.svg&&t.getAttribute("transform")?"1,0,0,1,0,0"===(l=[(r=t.transform.baseVal.consolidate().matrix).a,r.b,r.c,r.d,r.e,r.f]).join(",")?Ys:l:(l!==Ys||t.offsetParent||t===Uo||s.svg||(r=a.display,a.display="block",(n=t.parentNode)&&t.offsetParent||(o=1,i=t.nextElementSibling,Uo.appendChild(t)),l=Ws(t),r?a.display=r:Ps(t,"display"),o&&(i?n.insertBefore(t,i):n?n.appendChild(t):Uo.removeChild(t))),e&&l.length>6?[l[0],l[1],l[4],l[5],l[12],l[13]]:l)},Zs=function(t,e,n,i,r,o){var s,a,l,u=t._gsap,c=r||qs(t,!0),h=u.xOrigin||0,d=u.yOrigin||0,p=u.xOffset||0,f=u.yOffset||0,m=c[0],g=c[1],_=c[2],v=c[3],y=c[4],x=c[5],b=e.split(" "),w=parseFloat(b[0])||0,T=parseFloat(b[1])||0;n?c!==Ys&&(a=m*v-g*_)&&(l=w*(-g/a)+T*(m/a)-(m*x-g*y)/a,w=w*(v/a)+T*(-_/a)+(_*x-v*y)/a,T=l):(w=(s=$s(t)).x+(~b[0].indexOf("%")?w/100*s.width:w),T=s.y+(~(b[1]||b[0]).indexOf("%")?T/100*s.height:T)),i||!1!==i&&u.smooth?(y=w-h,x=T-d,u.xOffset=p+(y*m+x*_)-y,u.yOffset=f+(y*g+x*v)-x):u.xOffset=u.yOffset=0,u.xOrigin=w,u.yOrigin=T,u.smooth=!!i,u.origin=e,u.originIsAbsolute=!!n,t.style[ys]="0px 0px",o&&(Ds(o,u,"xOrigin",h,w),Ds(o,u,"yOrigin",d,T),Ds(o,u,"xOffset",p,u.xOffset),Ds(o,u,"yOffset",f,u.yOffset)),t.setAttribute("data-svg-origin",w+" "+T)},Gs=function(t,e){var n=t._gsap||new io(t);if("x"in n&&!e&&!n.uncache)return n;var i,r,o,s,a,l,u,c,h,d,p,f,m,g,_,v,y,x,b,w,T,A,C,M,k,E,S,L,$,O,P,D,R=t.style,N=n.scaleX<0,z="px",F="deg",I=getComputedStyle(t),B=Cs(t,ys)||"0";return i=r=o=l=u=c=h=d=p=0,s=a=1,n.svg=!(!t.getCTM||!Os(t)),I.translate&&("none"===I.translate&&"none"===I.scale&&"none"===I.rotate||(R[vs]=("none"!==I.translate?"translate3d("+(I.translate+" 0 0").split(" ").slice(0,3).join(", ")+") ":"")+("none"!==I.rotate?"rotate("+I.rotate+") ":"")+("none"!==I.scale?"scale("+I.scale.split(" ").join(",")+") ":"")+("none"!==I[vs]?I[vs]:"")),R.scale=R.rotate=R.translate="none"),g=qs(t,n.svg),n.svg&&(n.uncache?(k=t.getBBox(),B=n.xOrigin-k.x+"px "+(n.yOrigin-k.y)+"px",M=""):M=!e&&t.getAttribute("data-svg-origin"),Zs(t,M||B,!!M||n.originIsAbsolute,!1!==n.smooth,g)),f=n.xOrigin||0,m=n.yOrigin||0,g!==Ys&&(x=g[0],b=g[1],w=g[2],T=g[3],i=A=g[4],r=C=g[5],6===g.length?(s=Math.sqrt(x*x+b*b),a=Math.sqrt(T*T+w*w),l=x||b?es(b,x)*Jo:0,(h=w||T?es(w,T)*Jo+l:0)&&(a*=Math.abs(Math.cos(h*ts))),n.svg&&(i-=f-(f*x+m*w),r-=m-(f*b+m*T))):(D=g[6],O=g[7],S=g[8],L=g[9],$=g[10],P=g[11],i=g[12],r=g[13],o=g[14],u=(_=es(D,$))*Jo,_&&(M=A*(v=Math.cos(-_))+S*(y=Math.sin(-_)),k=C*v+L*y,E=D*v+$*y,S=A*-y+S*v,L=C*-y+L*v,$=D*-y+$*v,P=O*-y+P*v,A=M,C=k,D=E),c=(_=es(-w,$))*Jo,_&&(v=Math.cos(-_),P=T*(y=Math.sin(-_))+P*v,x=M=x*v-S*y,b=k=b*v-L*y,w=E=w*v-$*y),l=(_=es(b,x))*Jo,_&&(M=x*(v=Math.cos(_))+b*(y=Math.sin(_)),k=A*v+C*y,b=b*v-x*y,C=C*v-A*y,x=M,A=k),u&&Math.abs(u)+Math.abs(l)>359.9&&(u=l=0,c=180-c),s=Di(Math.sqrt(x*x+b*b+w*w)),a=Di(Math.sqrt(C*C+D*D)),_=es(A,C),h=Math.abs(_)>2e-4?_*Jo:0,p=P?1/(P<0?-P:P):0),n.svg&&(M=t.getAttribute("transform"),n.forceCSS=t.setAttribute("transform","")||!Us(Cs(t,vs)),M&&t.setAttribute("transform",M))),Math.abs(h)>90&&Math.abs(h)<270&&(N?(s*=-1,h+=l<=0?180:-180,l+=l<=0?180:-180):(a*=-1,h+=h<=0?180:-180)),e=e||n.uncache,n.x=i-((n.xPercent=i&&(!e&&n.xPercent||(Math.round(t.offsetWidth/2)===Math.round(-i)?-50:0)))?t.offsetWidth*n.xPercent/100:0)+z,n.y=r-((n.yPercent=r&&(!e&&n.yPercent||(Math.round(t.offsetHeight/2)===Math.round(-r)?-50:0)))?t.offsetHeight*n.yPercent/100:0)+z,n.z=o+z,n.scaleX=Di(s),n.scaleY=Di(a),n.rotation=Di(l)+F,n.rotationX=Di(u)+F,n.rotationY=Di(c)+F,n.skewX=h+F,n.skewY=d+F,n.transformPerspective=p+z,(n.zOrigin=parseFloat(B.split(" ")[2])||0)&&(R[ys]=Ks(B)),n.xOffset=n.yOffset=0,n.force3D=Vn.force3D,n.renderTransform=n.svg?ea:Ko?ta:Js,n.uncache=0,n},Ks=function(t){return(t=t.split(" "))[0]+" "+t[1]},Qs=function(t,e,n){var i=vr(e);return Di(parseFloat(e)+parseFloat(zs(t,"x",n+"px",i)))+i},Js=function(t,e){e.z="0px",e.rotationY=e.rotationX="0deg",e.force3D=0,ta(t,e)},ta=function(t,e){var n=e||this,i=n.xPercent,r=n.yPercent,o=n.x,s=n.y,a=n.z,l=n.rotation,u=n.rotationY,c=n.rotationX,h=n.skewX,d=n.skewY,p=n.scaleX,f=n.scaleY,m=n.transformPerspective,g=n.force3D,_=n.target,v=n.zOrigin,y="",x="auto"===g&&t&&1!==t||!0===g;if(v&&("0deg"!==c||"0deg"!==u)){var b,w=parseFloat(u)*ts,T=Math.sin(w),A=Math.cos(w);w=parseFloat(c)*ts,b=Math.cos(w),o=Qs(_,o,T*b*-v),s=Qs(_,s,-Math.sin(w)*-v),a=Qs(_,a,A*b*-v+v)}"0px"!==m&&(y+="perspective("+m+") "),(i||r)&&(y+="translate("+i+"%, "+r+"%) "),(x||"0px"!==o||"0px"!==s||"0px"!==a)&&(y+="0px"!==a||x?"translate3d("+o+", "+s+", "+a+") ":"translate("+o+", "+s+") "),"0deg"!==l&&(y+="rotate("+l+") "),"0deg"!==u&&(y+="rotateY("+u+") "),"0deg"!==c&&(y+="rotateX("+c+") "),"0deg"===h&&"0deg"===d||(y+="skew("+h+", "+d+") "),1===p&&1===f||(y+="scale("+p+", "+f+") "),_.style[vs]=y||"translate(0, 0)"},ea=function(t,e){var n,i,r,o,s,a=e||this,l=a.xPercent,u=a.yPercent,c=a.x,h=a.y,d=a.rotation,p=a.skewX,f=a.skewY,m=a.scaleX,g=a.scaleY,_=a.target,v=a.xOrigin,y=a.yOrigin,x=a.xOffset,b=a.yOffset,w=a.forceCSS,T=parseFloat(c),A=parseFloat(h);d=parseFloat(d),p=parseFloat(p),(f=parseFloat(f))&&(p+=f=parseFloat(f),d+=f),d||p?(d*=ts,p*=ts,n=Math.cos(d)*m,i=Math.sin(d)*m,r=Math.sin(d-p)*-g,o=Math.cos(d-p)*g,p&&(f*=ts,s=Math.tan(p-f),r*=s=Math.sqrt(1+s*s),o*=s,f&&(s=Math.tan(f),n*=s=Math.sqrt(1+s*s),i*=s)),n=Di(n),i=Di(i),r=Di(r),o=Di(o)):(n=m,o=g,i=r=0),(T&&!~(c+"").indexOf("px")||A&&!~(h+"").indexOf("px"))&&(T=zs(_,"x",c,"px"),A=zs(_,"y",h,"px")),(v||y||x||b)&&(T=Di(T+v-(v*n+y*r)+x),A=Di(A+y-(v*i+y*o)+b)),(l||u)&&(s=_.getBBox(),T=Di(T+l/100*s.width),A=Di(A+u/100*s.height)),s="matrix("+n+","+i+","+r+","+o+","+T+","+A+")",_.setAttribute("transform",s),w&&(_.style[vs]=s)},na=function(t,e,n,i,r){var o,s,a=360,l=Zn(r),u=parseFloat(r)*(l&&~r.indexOf("rad")?Jo:1)-i,c=i+u+"deg";return l&&("short"===(o=r.split("_")[1])&&(u%=a)!==u%180&&(u+=u<0?a:-360),"cw"===o&&u<0?u=(u+36e9)%a-~~(u/a)*a:"ccw"===o&&u>0&&(u=(u-36e9)%a-~~(u/a)*a)),t._pt=s=new $o(t._pt,e,n,i,u,as),s.e=c,s.u="deg",t._props.push(n),s},ia=function(t,e){for(var n in e)t[n]=e[n];return t},ra=function(t,e,n){var i,r,o,s,a,l,u,c=ia({},n._gsap),h=n.style;for(r in c.svg?(o=n.getAttribute("transform"),n.setAttribute("transform",""),h[vs]=e,i=Gs(n,1),Ps(n,vs),n.setAttribute("transform",o)):(o=getComputedStyle(n)[vs],h[vs]=e,i=Gs(n,1),h[vs]=o),Qo)(o=c[r])!==(s=i[r])&&"perspective,force3D,transformOrigin,svgOrigin".indexOf(r)<0&&(a=vr(o)!==(u=vr(s))?zs(n,r,o,u):parseFloat(o),l=parseFloat(s),t._pt=new $o(t._pt,i,r,a,l-a,ss),t._pt.u=u||0,t._props.push(r));ia(i,c)};Pi("padding,margin,Width,Radius",(function(t,e){var n="Top",i="Right",r="Bottom",o="Left",s=(e<3?[n,i,r,o]:[n+o,n+i,r+i,r+o]).map((function(n){return e<2?t+n:"border"+n+t}));Xs[e>1?"border"+t:t]=function(t,e,n,i,r){var o,a;if(arguments.length<4)return o=s.map((function(e){return Fs(t,e,n)})),5===(a=o.join(" ")).split(o[0]).length?o[0]:a;o=(i+"").split(" "),a={},s.forEach((function(t,e){return a[t]=o[e]=o[e]||o[(e-1)/2|0]})),t.init(e,a,r)}}));var oa,sa,aa,la={name:"css",register:Es,targetTest:function(t){return t.style&&t.nodeType},init:function(t,e,n,i,r){var o,s,a,l,u,c,h,d,p,f,m,g,_,v,y,x,b=this._props,w=t.style,T=n.vars.startAt;for(h in Wo||Es(),this.styles=this.styles||Ts(t),x=this.styles.props,this.tween=n,e)if("autoRound"!==h&&(s=e[h],!Ci[h]||!co(h,e,n,i,t,r)))if(u=typeof s,c=Xs[h],"function"===u&&(u=typeof(s=s.call(n,i,t,r))),"string"===u&&~s.indexOf("random(")&&(s=Lr(s)),c)c(this,t,h,s,n)&&(y=1);else if("--"===h.substr(0,2))o=(getComputedStyle(t).getPropertyValue(h)+"").trim(),s+="",Vr.lastIndex=0,Vr.test(o)||(d=vr(o),p=vr(s)),p?d!==p&&(o=zs(t,h,o,p)+p):d&&(s+=d),this.add(w,"setProperty",o,s,i,r,0,0,h),b.push(h),x.push(h,0,w[h]);else if("undefined"!==u){if(T&&h in T?(o="function"==typeof T[h]?T[h].call(n,i,t,r):T[h],Zn(o)&&~o.indexOf("random(")&&(o=Lr(o)),vr(o+"")||(o+=Vn.units[h]||vr(Fs(t,h))||""),"="===(o+"").charAt(1)&&(o=Fs(t,h))):o=Fs(t,h),l=parseFloat(o),(f="string"===u&&"="===s.charAt(1)&&s.substr(0,2))&&(s=s.substr(2)),a=parseFloat(s),h in os&&("autoAlpha"===h&&(1===l&&"hidden"===Fs(t,"visibility")&&a&&(l=0),x.push("visibility",0,w.visibility),Ds(this,w,"visibility",l?"inherit":"hidden",a?"inherit":"hidden",!a)),"scale"!==h&&"transform"!==h&&~(h=os[h]).indexOf(",")&&(h=h.split(",")[0])),m=h in Qo)if(this.styles.save(h),g||((_=t._gsap).renderTransform&&!e.parseTransform||Gs(t,e.parseTransform),v=!1!==e.smoothOrigin&&_.smooth,(g=this._pt=new $o(this._pt,w,vs,0,1,_.renderTransform,_,0,-1)).dep=1),"scale"===h)this._pt=new $o(this._pt,_,"scaleY",l,(f?Ni(l,f+a):a)-l||0,ss),this._pt.u=0,b.push("scaleY",h),h+="X";else{if("transformOrigin"===h){x.push(ys,0,w[ys]),s=Vs(s),_.svg?Zs(t,s,0,v,0,this):((p=parseFloat(s.split(" ")[2])||0)!==_.zOrigin&&Ds(this,_,"zOrigin",_.zOrigin,p),Ds(this,w,h,Ks(o),Ks(s)));continue}if("svgOrigin"===h){Zs(t,s,1,v,0,this);continue}if(h in Hs){na(this,_,h,l,f?Ni(l,f+s):s);continue}if("smoothOrigin"===h){Ds(this,_,"smooth",_.smooth,s);continue}if("force3D"===h){_[h]=s;continue}if("transform"===h){ra(this,s,t);continue}}else h in w||(h=ks(h)||h);if(m||(a||0===a)&&(l||0===l)&&!rs.test(s)&&h in w)a||(a=0),(d=(o+"").substr((l+"").length))!==(p=vr(s)||(h in Vn.units?Vn.units[h]:d))&&(l=zs(t,h,o,p)),this._pt=new $o(this._pt,m?_:w,h,l,(f?Ni(l,f+a):a)-l,m||"px"!==p&&"zIndex"!==h||!1===e.autoRound?ss:us),this._pt.u=p||0,d!==p&&"%"!==p&&(this._pt.b=o,this._pt.r=ls);else if(h in w)Is.call(this,t,h,o,f?f+s:s);else{if(!(h in t)){mi(h,s);continue}this.add(t,h,o||t[h],f?f+s:s,i,r)}m||(h in w?x.push(h,0,w[h]):x.push(h,1,o||t[h])),b.push(h)}y&&Lo(this)},render:function(t,e){if(e.tween._time||!Go())for(var n=e._pt;n;)n.r(t,n.d),n=n._next;else e.styles.revert()},get:Fs,aliases:os,getSetter:function(t,e,n){var i=os[e];return i&&i.indexOf(",")<0&&(e=i),e in Qo&&e!==ys&&(t._gsap.x||Fs(t,"x"))?n&&Zo===n?"scale"===e?ms:fs:(Zo=n||{})&&("scale"===e?gs:_s):t.style&&!Qn(t.style[e])?ds:~e.indexOf("-")?ps:wo(t,e)},core:{_removeProperty:Ps,_getMatrix:qs}};Xo.utils.checkPrefix=ks,Xo.core.getStyleSaver=Ts,aa=Pi((oa="x,y,z,scale,scaleX,scaleY,xPercent,yPercent")+","+(sa="rotation,rotationX,rotationY,skewX,skewY")+",transform,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective",(function(t){Qo[t]=1})),Pi(sa,(function(t){Vn.units[t]="deg",Hs[t]=1})),os[aa[13]]=oa+","+sa,Pi("0:translateX,1:translateY,2:translateZ,8:rotate,8:rotationZ,8:rotateZ,9:rotateX,10:rotateY",(function(t){var e=t.split(":");os[e[1]]=aa[e[0]]})),Pi("x,y,z,top,right,bottom,left,width,height,fontSize,padding,margin,perspective",(function(t){Vn.units[t]="px"})),Xo.registerPlugin(la);var ua=Xo.registerPlugin(la)||Xo;ua.core.Tween;var ca=/[achlmqstvz]|(-?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/gi,ha=/(?:(-)?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/gi,da=/[\+\-]?\d*\.?\d+e[\+\-]?\d+/gi,pa=/(^[#\.][a-z]|[a-y][a-z])/i,fa=Math.PI/180,ma=180/Math.PI,ga=Math.sin,_a=Math.cos,va=Math.abs,ya=Math.sqrt,xa=Math.atan2,ba=function(t){return"string"==typeof t},wa=function(t){return"number"==typeof t},Ta={},Aa={},Ca=function(t){return Math.round((t+1e8)%1*1e5)/1e5||(t<0?0:1)},Ma=function(t){return Math.round(1e5*t)/1e5||0},ka=function(t){return Math.round(1e10*t)/1e10||0},Ea=function(t,e,n,i){var r=t[e],o=1===i?6:Ba(r,n,i);if(o&&o+n+2e){for(;--r&&t[r]>e;);r<0&&(r=0)}else for(;t[++r] element or an SVG path data string")}function Pa(t){var e,n=0;for(t.reverse();n-1;)n=r[o].nodeName.toLowerCase(),e.indexOf(","+n+",")<0&&i.setAttributeNS(null,n,r[o].nodeValue);return i}(t,"x,y,width,height,cx,cy,rx,ry,r,x1,x2,y1,y2,points"),T=function(t,e){for(var n=e?e.split(","):[],i={},r=n.length;--r>-1;)i[n[r]]=+t.getAttribute(n[r])||0;return i}(t,Da[A]),"rect"===A?(o=T.rx,s=T.ry||o,i=T.x,r=T.y,h=T.width-2*o,d=T.height-2*s,n=o||s?"M"+(_=(m=(f=i+o)+h)+o)+","+(y=r+s)+" V"+(x=y+d)+" C"+[_,b=x+s*C,g=m+o*C,w=x+s,m,w,m-(m-f)/3,w,f+(m-f)/3,w,f,w,p=i+o*(1-C),w,i,b,i,x,i,x-(x-y)/3,i,y+(x-y)/3,i,y,i,v=r+s*(1-C),p,r,f,r,f+(m-f)/3,r,m-(m-f)/3,r,m,r,g,r,_,v,_,y].join(",")+"z":"M"+(i+h)+","+r+" v"+d+" h"+-h+" v"+-d+" h"+h+"z"):"circle"===A||"ellipse"===A?("circle"===A?u=(o=s=T.r)*C:(o=T.rx,u=(s=T.ry)*C),n="M"+((i=T.cx)+o)+","+(r=T.cy)+" C"+[i+o,r+u,i+(l=o*C),r+s,i,r+s,i-l,r+s,i-o,r+u,i-o,r,i-o,r-u,i-l,r-s,i,r-s,i+l,r-s,i+o,r-u,i+o,r].join(",")+"z"):"line"===A?n="M"+T.x1+","+T.y1+" L"+T.x2+","+T.y2:"polyline"!==A&&"polygon"!==A||(n="M"+(i=(c=(t.getAttribute("points")+"").match(ha)||[]).shift())+","+(r=c.shift())+" L"+c.join(","),"polygon"===A&&(n+=","+i+","+r+"z")),a.setAttribute("d",qa(a._gsRawPath=Ha(n))),e&&t.parentNode&&(t.parentNode.insertBefore(a,t),t.parentNode.removeChild(t)),a):t}function Na(t,e,n){var i,r=t[e],o=t[e+2],s=t[e+4];return r+=(o-r)*n,r+=((o+=(s-o)*n)-r)*n,i=o+(s+(t[e+6]-s)*n-o)*n-r,r=t[e+1],r+=((o=t[e+3])-r)*n,r+=((o+=((s=t[e+5])-o)*n)-r)*n,Ma(xa(o+(s+(t[e+7]-s)*n-o)*n-r,i)*ma)}function za(t,e,n){n=void 0===n?1:ka(n)||0,e=ka(e)||0;var i=Math.max(0,~~(va(n-e)-1e-8)),r=function(t){for(var e=[],n=0;nn&&(e=1-e,n=1-n,function(t,e){var n=t.length;for(e||t.reverse();n--;)t[n].reversed||Pa(t[n])}(r),r.totalLength=0),e<0||n<0){var o=Math.abs(~~Math.min(e,n))+1;e+=o,n+=o}r.totalLength||Ia(r);var s,a,l,u,c,h,d,p,f=n>1,m=Va(r,e,Ta,!0),g=Va(r,n,Aa),_=g.segment,v=m.segment,y=g.segIndex,x=m.segIndex,b=g.i,w=m.i,T=x===y,A=b===w&&T;if(f||i){for(s=yy)&&r.splice(u,1);else _.angle=Na(_,b+l,0),m=_[b+=l],g=_[b+1],_.length=_.totalLength=0,_.totalPoints=r.totalPoints=8,_.push(m,g,m,g,m,g,m,g);return r.totalLength=0,r}function Fa(t,e,n){e=e||0,t.samples||(t.samples=[],t.lookup=[]);var i,r,o,s,a,l,u,c,h,d,p,f,m,g,_,v,y,x=~~t.resolution||12,b=1/x,w=n?e+6*n+1:t.length,T=t[e],A=t[e+1],C=e?e/6*x:0,M=t.samples,k=t.lookup,E=(e?t.minLength:1e8)||1e8,S=M[C+n*x-1],L=e?M[C-1]:0;for(M.length=k.length=0,r=e+2;r8&&(t.splice(r,6),r-=6,w-=6);else for(i=1;i<=x;i++)l=u-(u=((g=b*i)*g*o+3*(m=1-g)*(g*s+m*a))*g),p=f-(f=(g*g*c+3*m*(g*h+m*d))*g),(v=ya(p*p+l*l))=1)return 0;var i=t[e],r=t[e+1],o=t[e+2],s=t[e+3],a=t[e+4],l=t[e+5],u=i+(o-i)*n,c=o+(a-o)*n,h=r+(s-r)*n,d=s+(l-s)*n,p=u+(c-u)*n,f=h+(d-h)*n,m=a+(t[e+6]-a)*n,g=l+(t[e+7]-l)*n;return c+=(m-c)*n,d+=(g-d)*n,t.splice(e+2,4,Ma(u),Ma(h),Ma(p),Ma(f),Ma(p+(c-p)*n),Ma(f+(d-f)*n),Ma(c),Ma(d),Ma(m),Ma(g)),t.samples&&t.samples.splice(e/6*t.resolution|0,0,0,0,0,0,0,0),6}function Va(t,e,n,i){n=n||{},t.totalLength||Ia(t),(e<0||e>1)&&(e=Ca(e));var r,o,s,a,l,u,c,h=0,d=t[0];if(e)if(1===e)c=1,u=(d=t[h=t.length-1]).length-8;else{if(t.length>1){for(s=t.totalLength*e,l=u=0;(l+=t[u++].totalLength)1)&&(e=Ca(e)),t.length>1){for(s=t.totalLength*e,l=u=0;(l+=t[u++].totalLength)=1?1-1e-9:c||1e-9):p.angle||0),f}function Xa(t,e,n,i,r,o,s){for(var a,l,u,c,h,d=t.length;--d>-1;)for(l=(a=t[d]).length,u=0;u1&&(n=ya(x)*n,i=ya(x)*i);var b=n*n,w=i*i,T=(b*w-b*y-w*v)/(b*y+w*v);T<0&&(T=0);var A=(o===s?-1:1)*ya(T),C=A*(n*_/i),M=A*(-i*g/n),k=(t+a)/2+(c*C-h*M),E=(e+l)/2+(h*C+c*M),S=(g-C)/n,L=(_-M)/i,$=(-g-C)/n,O=(-_-M)/i,P=S*S+L*L,D=(L<0?-1:1)*Math.acos(S/ya(P)),R=(S*O-L*$<0?-1:1)*Math.acos((S*$+L*O)/ya(P*($*$+O*O)));isNaN(R)&&(R=d),!s&&R>0?R-=p:s&&R<0&&(R+=p),D%=p,R%=p;var N,z=Math.ceil(va(R)/(p/4)),F=[],I=R/z,B=4/3*ga(I/2)/(1+_a(I/2)),V=c*n,j=h*n,X=h*-i,Y=c*i;for(N=0;N-1e-4?0:e})).match(ca)||[],_=[],v=0,y=0,x=2/3,b=g.length,w=0,T="ERROR: malformed path: "+t,A=function(t,e,n,i){c=(n-t)/3,h=(i-e)/3,a.push(t+c,e+h,n-c,i-h,n,i)};if(!t||!isNaN(g[0])||isNaN(g[1]))return console.log(T),_;for(e=0;e.5||va(y-r)>.5)&&(A(v,y,i,r),"L"===o&&(e+=2)),v=i,y=r;else if("A"===o){if(f=g[e+4],m=g[e+5],c=g[e+6],h=g[e+7],n=7,f.length>1&&(f.length<3?(h=c,c=m,n--):(h=m,c=f.substr(2),n-=2),m=f.charAt(1),f=f.charAt(0)),d=Ya(v,y,+g[e+1],+g[e+2],+g[e+3],+f,+m,(s?v:0)+1*c,(s?y:0)+1*h),e+=n,d)for(n=0;n1?function(t){for(var e=new fl,n=0;n4&&(o=r.offsetLeft,s=r.offsetTop,r=0);if("absolute"!==(a=Ga.getComputedStyle(t)).position&&"fixed"!==a.position)for(i=t.offsetParent;h&&h!==i;)o+=h.scrollLeft||0,s+=h.scrollTop||0,h=h.parentNode;(r=n.style).top=t.offsetTop-s+"px",r.left=t.offsetLeft-o+"px",r[rl]=a[rl],r[ol]=a[ol],r.position="fixed"===a.position?"fixed":"absolute",t.parentNode.appendChild(n)}return n},pl=function(t,e,n,i,r,o,s){return t.a=e,t.b=n,t.c=i,t.d=r,t.e=o,t.f=s,t},fl=function(){function t(t,e,n,i,r,o){void 0===t&&(t=1),void 0===e&&(e=0),void 0===n&&(n=0),void 0===i&&(i=1),void 0===r&&(r=0),void 0===o&&(o=0),pl(this,t,e,n,i,r,o)}var e=t.prototype;return e.inverse=function(){var t=this.a,e=this.b,n=this.c,i=this.d,r=this.e,o=this.f,s=t*i-e*n||1e-10;return pl(this,i/s,-e/s,-n/s,t/s,(n*o-i*r)/s,-(t*o-e*r)/s)},e.multiply=function(t){var e=this.a,n=this.b,i=this.c,r=this.d,o=this.e,s=this.f,a=t.a,l=t.c,u=t.b,c=t.d,h=t.e,d=t.f;return pl(this,a*e+u*i,a*n+u*r,l*e+c*i,l*n+c*r,o+h*e+d*i,s+h*n+d*r)},e.clone=function(){return new t(this.a,this.b,this.c,this.d,this.e,this.f)},e.equals=function(t){var e=this.a,n=this.b,i=this.c,r=this.d,o=this.e,s=this.f;return e===t.a&&n===t.b&&i===t.c&&r===t.d&&o===t.e&&s===t.f},e.apply=function(t,e){void 0===e&&(e={});var n=t.x,i=t.y,r=this.a,o=this.b,s=this.c,a=this.d,l=this.e,u=this.f;return e.x=n*r+i*s+l||0,e.y=n*o+i*a+u||0,e},t}();function ml(t,e,n,i){if(!t||!t.parentNode||(Za||sl(t)).documentElement===t)return new fl;var r=function(t){for(var e,n;t&&t!==Qa;)(n=t._gsap)&&n.uncache&&n.get(t,"x"),n&&!n.scaleX&&!n.scaleY&&n.renderTransform&&(n.scaleX=n.scaleY=1e-4,n.renderTransform(1,n),e?e.push(n):e=[n]),t=t.parentNode;return e}(t),o=ul(t)?al:ll,s=dl(t,n),a=o[0].getBoundingClientRect(),l=o[1].getBoundingClientRect(),u=o[2].getBoundingClientRect(),c=s.parentNode,h=!i&&cl(t),d=new fl((l.left-a.left)/100,(l.top-a.top)/100,(u.left-a.left)/100,(u.top-a.top)/100,a.left+(h?0:Ga.pageXOffset||Za.scrollLeft||Ka.scrollLeft||Qa.scrollLeft||0),a.top+(h?0:Ga.pageYOffset||Za.scrollTop||Ka.scrollTop||Qa.scrollTop||0));if(c.removeChild(s),r)for(a=r.length;a--;)(l=r[a]).scaleX=l.scaleY=0,l.renderTransform(1,l);return e?d.inverse():d} +/*! + * MotionPathPlugin 3.11.3 + * https://greensock.com + * + * @license Copyright 2008-2022, GreenSock. All rights reserved. + * Subject to the terms at https://greensock.com/standard-license or for + * Club GreenSock members, the agreement issued with that membership. + * @author: Jack Doyle, jack@greensock.com +*/var gl,_l,vl,yl,xl,bl,wl="x,translateX,left,marginLeft,xPercent".split(","),Tl="y,translateY,top,marginTop,yPercent".split(","),Al=Math.PI/180,Cl=function(t,e,n,i){for(var r=e.length,o=2===i?0:i,s=0;s1?t=1:t<0&&(t=0);i--;)ja(n[i],t,!i&&e.rotate,n[i]);for(;r;)r.set(r.t,r.p,r.path[r.pp]+r.u,r.d,t),r=r._next;e.rotate&&e.rSet(e.target,e.rProp,n[0].angle*(e.radians?Al:1)+e.rOffset+e.ru,e,t)}else e.styles.revert()},getLength:function(t){return Ia(Oa(t)).totalLength},sliceRawPath:za,getRawPath:Oa,pointsToSegment:Wa,stringToRawPath:Ha,rawPathToString:qa,transformRawPath:Xa,getGlobalMatrix:ml,getPositionOnPath:ja,cacheRawPathMeasurements:Ia,convertToPath:function(t,e){return yl(t).map((function(t){return Ra(t,!1!==e)}))},convertCoordinates:function(t,e,n){var i=ml(e,!0,!0).multiply(ml(t));return n?i.apply(n):i},getAlignMatrix:Ol,getRelativePosition:function(t,e,n,i){var r=Ol(t,e,n,i);return{x:r.e,y:r.f}},arrayToRawPath:function(t,e){var n=Cl(Cl([],t,(e=e||{}).x||"x",0),t,e.y||"y",1);return e.relative&&kl(n),["cubic"===e.type?n:Wa(n,e.curviness)]}};(gl||"undefined"!=typeof window&&(gl=window.gsap)&&gl.registerPlugin&&gl)&&gl.registerPlugin(Rl);var Nl,zl,Fl,Il,Bl,Vl,jl,Xl,Yl,Hl,Ul,Wl,ql,Zl,Gl,Kl,Ql,Jl,tu,eu,nu=0,iu=function(){return"undefined"!=typeof window},ru=function(){return Nl||iu()&&(Nl=window.gsap)&&Nl.registerPlugin&&Nl},ou=function(t){return"function"==typeof t},su=function(t){return"object"==typeof t},au=function(t){return void 0===t},lu=function(){return!1},uu="transform",cu="transformOrigin",hu=function(t){return Math.round(1e4*t)/1e4},du=Array.isArray,pu=function(t,e){var n=Fl.createElementNS?Fl.createElementNS((e||"http://www.w3.org/1999/xhtml").replace(/^https/,"http"),t):Fl.createElement(t);return n.style?n:Fl.createElement(t)},fu=180/Math.PI,mu=1e20,gu=new fl,_u=Date.now||function(){return(new Date).getTime()},vu=[],yu={},xu=0,bu=/^(?:a|input|textarea|button|select)$/i,wu=0,Tu={},Au={},Cu=function(t,e){var n,i={};for(n in t)i[n]=e?t[n]*e:t[n];return i},Mu=function t(e,n){for(var i,r=e.length;r--;)n?e[r].style.touchAction=n:e[r].style.removeProperty("touch-action"),(i=e[r].children)&&i.length&&t(i,n)},ku=function(){return vu.forEach((function(t){return t()}))},Eu=function(){return!vu.length&&Nl.ticker.remove(ku)},Su=function(t){for(var e=vu.length;e--;)vu[e]===t&&vu.splice(e,1);Nl.to(Eu,{overwrite:!0,delay:15,duration:0,onComplete:Eu,data:"_draggable"})},Lu=function(t,e,n,i){if(t.addEventListener){var r=ql[e];i=i||(Ul?{passive:!1}:null),t.addEventListener(r||e,n,i),r&&e!==r&&t.addEventListener(e,n,i)}},$u=function(t,e,n){if(t.removeEventListener){var i=ql[e];t.removeEventListener(i||e,n),i&&e!==i&&t.removeEventListener(e,n)}},Ou=function(t){t.preventDefault&&t.preventDefault(),t.preventManipulation&&t.preventManipulation()},Pu=function t(e){Zl=e.touches&&nu2||r<-2)&&!i)return f=t.scrollLeft,Nl.killTweensOf(this,{left:1,scrollLeft:1}),this.left(-f),void(e.onKill&&e.onKill());(n=-n)<0?(d=n-.5|0,n=0):n>v?(d=n-v|0,n=v):d=0,(d||o)&&(this._skip||(u[uu]=s+-d+"px,"+-h+a),d+_>=0&&(u.paddingRight=d+_+"px")),t.scrollLeft=0|n,f=t.scrollLeft},this.top=function(n,i){if(!arguments.length)return-(t.scrollTop+h);var r=t.scrollTop-p,o=h;if((r>2||r<-2)&&!i)return p=t.scrollTop,Nl.killTweensOf(this,{top:1,scrollTop:1}),this.top(-p),void(e.onKill&&e.onKill());(n=-n)<0?(h=n-.5|0,n=0):n>y?(h=n-y|0,n=y):h=0,(h||o)&&(this._skip||(u[uu]=s+-d+"px,"+-h+a)),t.scrollTop=0|n,p=t.scrollTop},this.maxScrollTop=function(){return y},this.maxScrollLeft=function(){return v},this.disable=function(){for(c=l.firstChild;c;)o=c.nextSibling,t.appendChild(c),c=o;t===l.parentNode&&t.removeChild(l)},this.enable=function(){if((c=t.firstChild)!==l){for(;c;)o=c.nextSibling,l.appendChild(c),c=o;t.appendChild(l),this.calibrate()}},this.calibrate=function(e){var o,s,a,c=t.clientWidth===n;p=t.scrollTop,f=t.scrollLeft,c&&t.clientHeight===i&&l.offsetHeight===r&&m===t.scrollWidth&&g===t.scrollHeight&&!e||((h||d)&&(s=this.left(),a=this.top(),this.left(-t.scrollLeft),this.top(-t.scrollTop)),o=Xu(t),c&&!e||(u.display="block",u.width="auto",u.paddingRight="0px",(_=Math.max(0,t.scrollWidth-t.clientWidth))&&(_+=parseFloat(o.paddingLeft)+(eu?parseFloat(o.paddingRight):0))),u.display="inline-block",u.position="relative",u.overflow="visible",u.verticalAlign="top",u.boxSizing="content-box",u.width="100%",u.paddingRight=_+"px",eu&&(u.paddingBottom=o.paddingBottom),n=t.clientWidth,i=t.clientHeight,m=t.scrollWidth,g=t.scrollHeight,v=t.scrollWidth-n,y=t.scrollHeight-i,r=l.offsetHeight,u.display="block",(s||a)&&(this.left(s),this.top(a)))},this.content=l,this.element=t,this._skip=!1,this.enable()},ec=function(t){if(iu()&&document.body){var e=window&&window.navigator;zl=window,Fl=document,Il=Fl.documentElement,Bl=Fl.body,Vl=pu("div"),Jl=!!window.PointerEvent,(jl=pu("div")).style.cssText="visibility:hidden;height:1px;top:-1px;pointer-events:none;position:relative;clear:both;cursor:grab",Ql="grab"===jl.style.cursor?"grab":"move",Gl=e&&-1!==e.userAgent.toLowerCase().indexOf("android"),Wl="ontouchstart"in Il&&"orientation"in zl||e&&(e.MaxTouchPoints>0||e.msMaxTouchPoints>0),i=pu("div"),r=pu("div"),o=r.style,s=Bl,o.display="inline-block",o.position="relative",i.style.cssText="width:90px;height:40px;padding:10px;overflow:auto;visibility:hidden",i.appendChild(r),s.appendChild(i),n=r.offsetHeight+18>i.scrollHeight,s.removeChild(i),eu=n,ql=function(t){for(var e=t.split(","),n=(("onpointerdown"in Vl?"pointerdown,pointermove,pointerup,pointercancel":"onmspointerdown"in Vl?"MSPointerDown,MSPointerMove,MSPointerUp,MSPointerCancel":t).split(",")),i={},r=4;--r>-1;)i[e[r]]=n[r],i[n[r]]=e[r];try{Il.addEventListener("test",null,Object.defineProperty({},"passive",{get:function(){Ul=1}}))}catch(t){}return i}("touchstart,touchmove,touchend,touchcancel"),Lu(Fl,"touchcancel",lu),Lu(zl,"touchmove",lu),Bl&&Bl.addEventListener("touchstart",lu),Lu(Fl,"contextmenu",(function(){for(var t in yu)yu[t].isPressed&&yu[t].endDrag()})),Nl=Xl=ru()}var n,i,r,o,s;Nl?(Kl=Nl.plugins.inertia,Yl=Nl.utils.checkPrefix,uu=Yl(uu),cu=Yl(cu),Hl=Nl.utils.toArray,tu=!!Yl("perspective")):t&&console.warn("Please gsap.registerPlugin(Draggable)")},nc=function(){function t(t){this._listeners={},this.target=t||this}var e=t.prototype;return e.addEventListener=function(t,e){var n=this._listeners[t]||(this._listeners[t]=[]);~n.indexOf(e)||n.push(e)},e.removeEventListener=function(t,e){var n=this._listeners[t],i=n&&n.indexOf(e);i>=0&&n.splice(i,1)},e.dispatchEvent=function(t){var e,n=this;return(this._listeners[t]||[]).forEach((function(i){return!1===i.call(n,{type:t,target:n.target})&&(e=!1)})),e},t}(),ic=function(t){var e,n;function i(e,n){var r;r=t.call(this)||this,Xl||ec(1),e=Hl(e)[0],Kl||(Kl=Nl.plugins.inertia),r.vars=n=Cu(n||{}),r.target=e,r.x=r.y=r.rotation=0,r.dragResistance=parseFloat(n.dragResistance)||0,r.edgeResistance=isNaN(n.edgeResistance)?1:parseFloat(n.edgeResistance)||0,r.lockAxis=n.lockAxis,r.autoScroll=n.autoScroll||0,r.lockedAxis=null,r.allowEventDefault=!!n.allowEventDefault,Nl.getProperty(e,"x");var o,s,a,l,u,c,h,d,p,f,m,g,_,v,y,x,b,w,T,A,C,M,k,E,S,L,$,O,P,D,R,N,z,F=(n.type||"x,y").toLowerCase(),I=~F.indexOf("x")||~F.indexOf("y"),B=-1!==F.indexOf("rotation"),V=B?"rotation":I?"x":"left",j=I?"y":"top",X=!(!~F.indexOf("x")&&!~F.indexOf("left")&&"scroll"!==F),Y=!(!~F.indexOf("y")&&!~F.indexOf("top")&&"scroll"!==F),H=n.minimumMovement||2,U=function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(r),W=Hl(n.trigger||n.handle||e),q={},Z=0,G=!1,K=n.autoScrollMarginTop||40,Q=n.autoScrollMarginRight||40,J=n.autoScrollMarginBottom||40,tt=n.autoScrollMarginLeft||40,et=n.clickableTest||Ku,nt=0,it=e._gsap||Nl.core.getCache(e),rt=Ju(e),ot=function(t,n){return parseFloat(it.get(e,t,n))},st=e.ownerDocument||Fl,at=function(t){return Ou(t),t.stopImmediatePropagation&&t.stopImmediatePropagation(),!1},lt=function t(n){if(U.autoScroll&&U.isDragging&&(G||b)){var i,r,o,a,l,u,c,h,p=e,f=15*U.autoScroll;for(G=!1,Au.scrollTop=null!=zl.pageYOffset?zl.pageYOffset:null!=st.documentElement.scrollTop?st.documentElement.scrollTop:st.body.scrollTop,Au.scrollLeft=null!=zl.pageXOffset?zl.pageXOffset:null!=st.documentElement.scrollLeft?st.documentElement.scrollLeft:st.body.scrollLeft,a=U.pointerX-Au.scrollLeft,l=U.pointerY-Au.scrollTop;p&&!r;)i=(r=Iu(p.parentNode))?Au:p.parentNode,o=r?{bottom:Math.max(Il.clientHeight,zl.innerHeight||0),right:Math.max(Il.clientWidth,zl.innerWidth||0),left:0,top:0}:i.getBoundingClientRect(),u=c=0,Y&&((h=i._gsMaxScrollY-i.scrollTop)<0?c=h:l>o.bottom-J&&h?(G=!0,c=Math.min(h,f*(1-Math.max(0,o.bottom-l)/J)|0)):lo.right-Q&&h?(G=!0,u=Math.min(h,f*(1-Math.max(0,o.right-a)/Q)|0)):an?n+(r-n)*o:r-1;)(o=(r=t[s])-i)<0&&(o=-o),o=e&&r<=n&&(a=s,l=o);return t[a]}:isNaN(t)?function(t){return t}:function(){return t*i}},ht=function(){var t,i,r,o;h=!1,s?(s.calibrate(),U.minX=m=-s.maxScrollLeft(),U.minY=_=-s.maxScrollTop(),U.maxX=f=U.maxY=g=0,h=!0):n.bounds&&(t=Wu(n.bounds,e.parentNode),B?(U.minX=m=t.left,U.maxX=f=t.left+t.width,U.minY=_=U.maxY=g=0):au(n.bounds.maxX)&&au(n.bounds.maxY)?(i=Wu(e,e.parentNode),U.minX=m=Math.round(ot(V,"px")+t.left-i.left),U.minY=_=Math.round(ot(j,"px")+t.top-i.top),U.maxX=f=Math.round(m+(t.width-i.width)),U.maxY=g=Math.round(_+(t.height-i.height))):(t=n.bounds,U.minX=m=t.minX,U.minY=_=t.minY,U.maxX=f=t.maxX,U.maxY=g=t.maxY),m>f&&(U.minX=f,U.maxX=f=m,m=U.minX),_>g&&(U.minY=g,U.maxY=g=_,_=U.minY),B&&(U.minRotation=m,U.maxRotation=f),h=!0),n.liveSnap&&(r=!0===n.liveSnap?n.snap||{}:n.liveSnap,o=du(r)||ou(r),B?(T=ct(o?r:r.rotation,m,f,1),A=null):r.points?C=function(t,e,n,i,r,o,s){return o=o&&on?n+(d-n)*h:dr?r+(p-r)*h:po&&(a.x=d,a.y=p),a}:du(t)?function(e){for(var n,i,r,s,a=t.length,l=0,u=mu;--a>-1;)(s=(n=(r=t[a]).x-e.x)*n+(i=r.y-e.y)*i)1e3?0:.5:n.minDuration,overshoot:l}),U.tween=a=Nl.to(s||e,{inertia:t,data:"_draggable",onComplete:dt,onInterrupt:pt,onUpdate:n.fastMode?Uu:ut,onUpdateParams:n.fastMode?[U,"onthrowupdate","onThrowUpdate"]:r&&r.radius?[!1,!0]:[]}),n.fastMode||(s&&(s._skip=!0),a.render(1e9,!0,!0),ut(!0,!0),U.endX=U.x,U.endY=U.y,B&&(U.endRotation=U.x),a.play(0),ut(!0,!0),s&&(s._skip=!1))):h&&U.applyBounds()},mt=function(t){var n,i=E;E=ml(e.parentNode,!0),t&&U.isPressed&&!E.equals(i||new fl)&&(n=i.inverse().apply({x:a,y:l}),E.apply(n,n),a=n.x,l=n.y),E.equals(gu)&&(E=null)},gt=function(){var t,n,i,r=1-U.edgeResistance,o=rt?Nu(st):0,d=rt?Ru(st):0;I&&(it.x=ot(V,"px")+"px",it.y=ot(j,"px")+"px",it.renderTransform()),mt(!1),qu.x=U.pointerX-o,qu.y=U.pointerY-d,E&&E.apply(qu,qu),a=qu.x,l=qu.y,b&&(bt(U.pointerX,U.pointerY),lt(!0)),N=ml(e),s?(ht(),c=s.top(),u=s.left()):(_t()?(ut(!0,!0),ht()):U.applyBounds(),B?(t=e.ownerSVGElement?[it.xOrigin-e.getBBox().x,it.yOrigin-e.getBBox().y]:(Xu(e)[cu]||"0 0").split(" "),x=U.rotationOrigin=ml(e).apply({x:parseFloat(t[0])||0,y:parseFloat(t[1])||0}),ut(!0,!0),n=U.pointerX-x.x-o,i=x.y-U.pointerY+d,u=U.x,c=U.y=Math.atan2(i,n)*fu):(c=ot(j,"px"),u=ot(V,"px"))),h&&r&&(u>f?u=f+(u-f)/r:ug?c=g+(c-g)/r:c<_&&(c=_-(_-c)/r))),U.startX=u=hu(u),U.startY=c=hu(c)},_t=function(){return U.tween&&U.tween.isActive()},vt=function(){!jl.parentNode||_t()||U.isDragging||jl.parentNode.removeChild(jl)},yt=function(t,r){var u;if(!o||U.isPressed||!t||!("mousedown"!==t.type&&"pointerdown"!==t.type||r)&&_u()-nt<30&&ql[U.pointerEvent.type])R&&t&&o&&Ou(t);else{if(S=_t(),z=!1,U.pointerEvent=t,ql[t.type]?(k=~t.type.indexOf("touch")?t.currentTarget||t.target:st,Lu(k,"touchend",wt),Lu(k,"touchmove",xt),Lu(k,"touchcancel",wt),Lu(st,"touchstart",Du)):(k=null,Lu(st,"mousemove",xt)),$=null,Jl&&k||(Lu(st,"mouseup",wt),t&&t.target&&Lu(t.target,"mouseup",wt)),M=et.call(U,t.target)&&!1===n.dragClickables&&!r)return Lu(t.target,"change",wt),Uu(U,"pressInit","onPressInit"),Uu(U,"press","onPress"),Qu(W,!0),void(R=!1);var c;if(L=!(!k||X===Y||!1===U.vars.allowNativeTouchScrolling||U.vars.allowContextMenu&&t&&(t.ctrlKey||t.which>2))&&(X?"y":"x"),(R=!L&&!U.allowEventDefault)&&(Ou(t),Lu(zl,"touchforcechange",Ou)),t.changedTouches?(t=v=t.changedTouches[0],y=t.identifier):t.pointerId?y=t.pointerId:v=y=null,nu++,c=lt,vu.push(c),1===vu.length&&Nl.ticker.add(ku),l=U.pointerY=t.pageY,a=U.pointerX=t.pageX,Uu(U,"pressInit","onPressInit"),(L||U.autoScroll)&&Vu(e.parentNode),!e.parentNode||!U.autoScroll||s||B||!e.parentNode._gsMaxScrollX||jl.parentNode||e.getBBox||(jl.style.width=e.parentNode.scrollWidth+"px",e.parentNode.appendChild(jl)),gt(),U.tween&&U.tween.kill(),U.isThrowing=!1,Nl.killTweensOf(s||e,q,!0),s&&Nl.killTweensOf(e,{scrollTo:1},!0),U.tween=U.lockedAxis=null,(n.zIndexBoost||!B&&!s&&!1!==n.zIndexBoost)&&(e.style.zIndex=i.zIndex++),U.isPressed=!0,d=!(!n.onDrag&&!U._listeners.drag),p=!(!n.onMove&&!U._listeners.move),!1!==n.cursor||n.activeCursor)for(u=W.length;--u>-1;)Nl.set(W[u],{cursor:n.activeCursor||n.cursor||("grab"===Ql?"grabbing":Ql)});Uu(U,"press","onPress")}},xt=function(t){var n,i,r,s,u,c,h=t;if(o&&!Zl&&U.isPressed&&t){if(U.pointerEvent=t,n=t.changedTouches){if((t=n[0])!==v&&t.identifier!==y){for(s=n.length;--s>-1&&(t=n[s]).identifier!==y&&t.target!==e;);if(s<0)return}}else if(t.pointerId&&y&&t.pointerId!==y)return;k&&L&&!$&&(qu.x=t.pageX-(rt?Nu(st):0),qu.y=t.pageY-(rt?Ru(st):0),E&&E.apply(qu,qu),i=qu.x,r=qu.y,((u=Math.abs(i-a))!==(c=Math.abs(r-l))&&(u>H||c>H)||Gl&&L===$)&&($=u>c&&X?"x":"y",L&&$!==L&&Lu(zl,"touchforcechange",Ou),!1!==U.vars.lockAxisOnTouchScroll&&X&&Y&&(U.lockedAxis="x"===$?"y":"x",ou(U.vars.onLockAxis)&&U.vars.onLockAxis.call(U,h)),Gl&&L===$))?wt(h):(U.allowEventDefault||L&&(!$||L===$)||!1===h.cancelable?R&&(R=!1):(Ou(h),R=!0),U.autoScroll&&(G=!0),bt(t.pageX,t.pageY,p))}else R&&t&&o&&Ou(t)},bt=function(t,e,n){var i,r,o,s,d,p,v=1-U.dragResistance,y=1-U.edgeResistance,w=U.pointerX,M=U.pointerY,k=c,S=U.x,L=U.y,$=U.endX,O=U.endY,P=U.endRotation,D=b;U.pointerX=t,U.pointerY=e,rt&&(t-=Nu(st),e-=Ru(st)),B?(s=Math.atan2(x.y-e,t-x.x)*fu,(d=U.y-s)>180?(c-=360,U.y=s):d<-180&&(c+=360,U.y=s),U.x!==u||Math.abs(c-s)>H?(U.y=s,o=u+(c-s)*v):o=u):(E&&(p=t*E.a+e*E.c+E.e,e=t*E.b+e*E.d+E.f,t=p),(r=e-l)-H&&(r=0),(i=t-a)-H&&(i=0),(U.lockAxis||U.lockedAxis)&&(i||r)&&((p=U.lockedAxis)||(U.lockedAxis=p=X&&Math.abs(i)>Math.abs(r)?"y":Y?"x":null,p&&ou(U.vars.onLockAxis)&&U.vars.onLockAxis.call(U,U.pointerEvent)),"y"===p?r=0:"x"===p&&(i=0)),o=hu(u+i*v),s=hu(c+r*v)),(T||A||C)&&(U.x!==o||U.y!==s&&!B)&&(C&&(Tu.x=o,Tu.y=s,p=C(Tu),o=hu(p.x),s=hu(p.y)),T&&(o=hu(T(o))),A&&(s=hu(A(s)))),h&&(o>f?o=f+Math.round((o-f)*y):og?s=Math.round(g+(s-g)*y):s<_&&(s=Math.round(_+(s-_)*y)))),(U.x!==o||U.y!==s&&!B)&&(B?(U.endRotation=U.x=U.endX=o,b=!0):(Y&&(U.y=U.endY=s,b=!0),X&&(U.x=U.endX=o,b=!0)),n&&!1===Uu(U,"move","onMove")?(U.pointerX=w,U.pointerY=M,c=k,U.x=S,U.y=L,U.endX=$,U.endY=O,U.endRotation=P,b=D):!U.isDragging&&U.isPressed&&(U.isDragging=z=!0,Uu(U,"dragstart","onDragStart")))},wt=function t(i,r){if(o&&U.isPressed&&(!i||null==y||r||!(i.pointerId&&i.pointerId!==y&&i.target!==e||i.changedTouches&&!function(t,e){for(var n=t.length;n--;)if(t[n].identifier===e)return!0}(i.changedTouches,y)))){U.isPressed=!1;var s,a,l,u,c,h=i,d=U.isDragging,p=U.vars.allowContextMenu&&i&&(i.ctrlKey||i.which>2),f=Nl.delayedCall(.001,vt);if(k?($u(k,"touchend",t),$u(k,"touchmove",xt),$u(k,"touchcancel",t),$u(st,"touchstart",Du)):$u(st,"mousemove",xt),$u(zl,"touchforcechange",Ou),Jl&&k||($u(st,"mouseup",t),i&&i.target&&$u(i.target,"mouseup",t)),b=!1,d&&(Z=wu=_u(),U.isDragging=!1),Su(lt),M&&!p)return i&&($u(i.target,"change",t),U.pointerEvent=h),Qu(W,!1),Uu(U,"release","onRelease"),Uu(U,"click","onClick"),void(M=!1);for(a=W.length;--a>-1;)ju(W[a],"cursor",n.cursor||(!1!==n.cursor?Ql:null));if(nu--,i){if((s=i.changedTouches)&&(i=s[0])!==v&&i.identifier!==y){for(a=s.length;--a>-1&&(i=s[a]).identifier!==y&&i.target!==e;);if(a<0&&!r)return}U.pointerEvent=h,U.pointerX=i.pageX,U.pointerY=i.pageY}return p&&h?(Ou(h),R=!0,Uu(U,"release","onRelease")):h&&!d?(R=!1,S&&(n.snap||n.bounds)&&ft(n.inertia||n.throwProps),Uu(U,"release","onRelease"),Gl&&"touchmove"===h.type||-1!==h.type.indexOf("cancel")||(Uu(U,"click","onClick"),_u()-nt<300&&Uu(U,"doubleclick","onDoubleClick"),u=h.target||e,nt=_u(),c=function(){nt===P||!U.enabled()||U.isPressed||h.defaultPrevented||(u.click?u.click():st.createEvent&&((l=st.createEvent("MouseEvents")).initMouseEvent("click",!0,!0,zl,1,U.pointerEvent.screenX,U.pointerEvent.screenY,U.pointerX,U.pointerY,!1,!1,!1,!1,0,null),u.dispatchEvent(l)))},Gl||h.defaultPrevented||Nl.delayedCall(.05,c))):(ft(n.inertia||n.throwProps),U.allowEventDefault||!h||!1===n.dragClickables&&et.call(U,h.target)||!d||L&&(!$||L!==$)||!1===h.cancelable?R=!1:(R=!0,Ou(h)),Uu(U,"release","onRelease")),_t()&&f.duration(U.tween.duration()),d&&Uu(U,"dragend","onDragEnd"),!0}R&&i&&o&&Ou(i)},Tt=function(t){if(t&&U.isDragging&&!s){var n=t.target||e.parentNode,i=n.scrollLeft-n._gsScrollX,r=n.scrollTop-n._gsScrollY;(i||r)&&(E?(a-=i*E.a+r*E.c,l-=r*E.d+i*E.b):(a-=i,l-=r),n._gsScrollX+=i,n._gsScrollY+=r,bt(U.pointerX,U.pointerY))}},At=function(t){var e=_u(),n=e-nt<100,i=e-Z<50,r=n&&P===nt,o=U.pointerEvent&&U.pointerEvent.defaultPrevented,s=n&&D===nt,a=t.isTrusted||null==t.isTrusted&&n&&r;if((r||i&&!1!==U.vars.suppressClickOnDrag)&&t.stopImmediatePropagation&&t.stopImmediatePropagation(),n&&(!U.pointerEvent||!U.pointerEvent.defaultPrevented)&&(!r||a&&!s))return a&&r&&(D=nt),void(P=nt);(U.isPressed||i||n)&&(a&&t.detail&&n&&!o||Ou(t)),n||i||z||(t&&t.target&&(U.pointerEvent=t),Uu(U,"click","onClick"))},Ct=function(t){return E?{x:t.x*E.a+t.y*E.c+E.e,y:t.x*E.b+t.y*E.d+E.f}:{x:t.x,y:t.y}};return(w=i.get(e))&&w.kill(),r.startDrag=function(t,n){var i,r,o,s;yt(t||U.pointerEvent,!0),n&&!U.hitTest(t||U.pointerEvent)&&(i=Hu(t||U.pointerEvent),r=Hu(e),o=Ct({x:i.left+i.width/2,y:i.top+i.height/2}),s=Ct({x:r.left+r.width/2,y:r.top+r.height/2}),a-=o.x-s.x,l-=o.y-s.y),U.isDragging||(U.isDragging=z=!0,Uu(U,"dragstart","onDragStart"))},r.drag=xt,r.endDrag=function(t){return wt(t||U.pointerEvent,!0)},r.timeSinceDrag=function(){return U.isDragging?0:(_u()-Z)/1e3},r.timeSinceClick=function(){return(_u()-nt)/1e3},r.hitTest=function(t,e){return i.hitTest(U.target,t,e)},r.getDirection=function(t,n){var i,r,o,s,a,l,h="velocity"===t&&Kl?t:su(t)&&!B?"element":"start";return"element"===h&&(a=Hu(U.target),l=Hu(t)),i="start"===h?U.x-u:"velocity"===h?Kl.getVelocity(e,V):a.left+a.width/2-(l.left+l.width/2),B?i<0?"counter-clockwise":"clockwise":(n=n||2,r="start"===h?U.y-c:"velocity"===h?Kl.getVelocity(e,j):a.top+a.height/2-(l.top+l.height/2),s=(o=Math.abs(i/r))<1/n?"":i<0?"left":"right",of?r=f:rg?o=g:o<_&&(o=_),(U.x!==r||U.y!==o)&&(s=!0,U.x=U.endX=r,B?U.endRotation=r:U.y=U.endY=o,b=!0,lt(!0),U.autoScroll&&!U.isDragging))for(Vu(e.parentNode),a=e,Au.scrollTop=null!=zl.pageYOffset?zl.pageYOffset:null!=st.documentElement.scrollTop?st.documentElement.scrollTop:st.body.scrollTop,Au.scrollLeft=null!=zl.pageXOffset?zl.pageXOffset:null!=st.documentElement.scrollLeft?st.documentElement.scrollLeft:st.body.scrollLeft;a&&!u;)l=(u=Iu(a.parentNode))?Au:a.parentNode,Y&&l.scrollTop>l._gsMaxScrollY&&(l.scrollTop=l._gsMaxScrollY),X&&l.scrollLeft>l._gsMaxScrollX&&(l.scrollLeft=l._gsMaxScrollX),a=l;U.isThrowing&&(s||U.endX>f||U.endXg||U.endY<_)&&ft(n.inertia||n.throwProps,s)}return U},r.update=function(t,n,i){if(n&&U.isPressed){var r=ml(e),o=N.apply({x:U.x-u,y:U.y-c}),s=ml(e.parentNode,!0);s.apply({x:r.e-o.x,y:r.f-o.y},o),U.x-=o.x-s.e,U.y-=o.y-s.f,lt(!0),gt()}var a=U.x,l=U.y;return mt(!n),t?U.applyBounds():(b&&i&<(!0),ut(!0)),n&&(bt(U.pointerX,U.pointerY),b&<(!0)),U.isPressed&&!n&&(X&&Math.abs(a-U.x)>.01||Y&&Math.abs(l-U.y)>.01&&!B)&>(),U.autoScroll&&(Vu(e.parentNode,U.isDragging),G=U.isDragging,lt(!0),Fu(e,Tt),zu(e,Tt)),U},r.enable=function(t){var i,r,a,l={lazy:!0};if(!1!==n.cursor&&(l.cursor=n.cursor||Ql),Nl.utils.checkPrefix("touchCallout")&&(l.touchCallout="none"),"soft"!==t){for(Mu(W,X===Y?"none":n.allowNativeTouchScrolling&&e.scrollHeight===e.clientHeight==(e.scrollWidth===e.clientHeight)||n.allowEventDefault?"manipulation":X?"pan-y":"pan-x"),r=W.length;--r>-1;)a=W[r],Jl||Lu(a,"mousedown",yt),Lu(a,"touchstart",yt),Lu(a,"click",At,!0),Nl.set(a,l),a.getBBox&&a.ownerSVGElement&&X!==Y&&Nl.set(a.ownerSVGElement,{touchAction:n.allowNativeTouchScrolling||n.allowEventDefault?"manipulation":X?"pan-y":"pan-x"}),n.allowContextMenu||Lu(a,"contextmenu",at);Qu(W,!1)}return zu(e,Tt),o=!0,Kl&&"soft"!==t&&Kl.track(s||e,I?"x,y":B?"rotation":"top,left"),e._gsDragID=i="d"+xu++,yu[i]=U,s&&(s.enable(),s.element._gsDragID=i),(n.bounds||B)&>(),n.bounds&&U.applyBounds(),U},r.disable=function(t){for(var n,i=U.isDragging,r=W.length;--r>-1;)ju(W[r],"cursor",null);if("soft"!==t){for(Mu(W,null),r=W.length;--r>-1;)n=W[r],ju(n,"touchCallout",null),$u(n,"mousedown",yt),$u(n,"touchstart",yt),$u(n,"click",At),$u(n,"contextmenu",at);Qu(W,!0),k&&($u(k,"touchcancel",wt),$u(k,"touchend",wt),$u(k,"touchmove",xt)),$u(st,"mouseup",wt),$u(st,"mousemove",xt)}return Fu(e,Tt),o=!1,Kl&&"soft"!==t&&Kl.untrack(s||e,I?"x,y":B?"rotation":"top,left"),s&&s.disable(),Su(lt),U.isDragging=U.isPressed=M=!1,i&&Uu(U,"dragend","onDragEnd"),U},r.enabled=function(t,e){return arguments.length?t?U.enable(e):U.disable(e):o},r.kill=function(){return U.isThrowing=!1,U.tween&&U.tween.kill(),U.disable(),Nl.set(W,{clearProps:"userSelect"}),delete yu[e._gsDragID],U},~F.indexOf("scroll")&&(s=r.scrollProxy=new tc(e,function(t,e){for(var n in e)n in t||(t[n]=e[n]);return t}({onKill:function(){U.isPressed&&wt(null)}},n)),e.style.overflowY=Y&&!Wl?"auto":"hidden",e.style.overflowX=X&&!Wl?"auto":"hidden",e=s.content),B?q.rotation=1:(X&&(q[V]=1),Y&&(q[j]=1)),it.force3D=!("force3D"in n)||n.force3D,r.enable(),r}return n=t,(e=i).prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n,i.register=function(t){Nl=t,ec()},i.create=function(t,e){return Xl||ec(!0),Hl(t).map((function(t){return new i(t,e)}))},i.get=function(t){return yu[(Hl(t)[0]||{})._gsDragID]},i.timeSinceDrag=function(){return(_u()-wu)/1e3},i.hitTest=function(t,e,n){if(t===e)return!1;var i,r,o,s=Hu(t),a=Hu(e),l=s.top,u=s.left,c=s.right,h=s.bottom,d=s.width,p=s.height,f=a.left>c||a.righth||a.bottom=d*p*n||r>=a.width*a.height*n):i.width>n&&i.height>n))},i}(nc);!function(t,e){for(var n in e)n in t||(t[n]=e[n])}(ic.prototype,{pointerX:0,pointerY:0,startX:0,startY:0,deltaX:0,deltaY:0,isDragging:!1,isPressed:!1}),ic.zIndex=1e3,ic.version="3.11.3",ru()&&Nl.registerPlugin(ic),ua.registerPlugin(Rl),ua.registerPlugin(ic);const rc={auto:"M18,11V12.5C21.19,12.5 23.09,16.05 21.33,18.71L20.24,17.62C21.06,15.96 19.85,14 18,14V15.5L15.75,13.25L18,11M18,22V20.5C14.81,20.5 12.91,16.95 14.67,14.29L15.76,15.38C14.94,17.04 16.15,19 18,19V17.5L20.25,19.75L18,22M19,3H18V1H16V3H8V1H6V3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H14C13.36,20.45 12.86,19.77 12.5,19H5V8H19V10.59C19.71,10.7 20.39,10.94 21,11.31V5A2,2 0 0,0 19,3Z",heat_cool:"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z",heat:"M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z",cool:"M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z",off:"M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13",fan_only:"M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12.5,2C17,2 17.11,5.57 14.75,6.75C13.76,7.24 13.32,8.29 13.13,9.22C13.61,9.42 14.03,9.73 14.35,10.13C18.05,8.13 22.03,8.92 22.03,12.5C22.03,17 18.46,17.1 17.28,14.73C16.78,13.74 15.72,13.3 14.79,13.11C14.59,13.59 14.28,14 13.88,14.34C15.87,18.03 15.08,22 11.5,22C7,22 6.91,18.42 9.27,17.24C10.25,16.75 10.69,15.71 10.89,14.79C10.4,14.59 9.97,14.27 9.65,13.87C5.96,15.85 2,15.07 2,11.5C2,7 5.56,6.89 6.74,9.26C7.24,10.25 8.29,10.68 9.22,10.87C9.41,10.39 9.73,9.97 10.14,9.65C8.15,5.96 8.94,2 12.5,2Z",dry:ft,window_open:mt,eco:"M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z",summer:pt,temperature:"M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z",humidity:ft};function oc(t){const e=window;e.customCards=e.customCards||[],e.customCards.push(Object.assign(Object.assign({},t),{preview:!0}))}console.info("%c BetterThermostatUI-CARD \n%c version: 1.0.3 ","color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),oc({type:"better-thermostat-ui-card",name:"Better Thermostat Climate Card",description:"Card for climate entity"});let sc=class extends nt{constructor(){super(),this.value=0,this.current=0,this.humidity=0,this.min=0,this.max=35,this.step=1,this.window=!1,this.summer=!1,this.status="loading",this.mode="off",this.dragging=!1,this._init=!0,this._firstRender=!0,this._ignore=!1,this._hasWindow=!1,this._hasSummer=!1,this._oldValueMin=0,this._oldValueMax=0,this._display_bottom=0,this._display_top=0,this.modes=[],this.render=()=>{var t,e,n,i,r,o,s,a,l,u,c,h,d,p,f,m,g,_,v;return N` + + +
${null===(t=this._config)||void 0===t?void 0:t.name}
+
+ + + ${this._hasWindow&&!(null===(o=this._config)||void 0===o?void 0:o.disable_window)?z` + + `:""} + ${this._hasSummer&&!(null===(a=this._config)||void 0===a?void 0:a.disable_summer)?z` + + `:""} + + + + + + + ${z`${Qe(this._display_top,this.hass.locale,{minimumFractionDigits:1,maximumFractionDigits:1})}`} + + ${z` + ${this.hass.config.unit_system.temperature} + `} + + + ${"unavailable"===(null===(u=null==this?void 0:this.stateObj)||void 0===u?void 0:u.state)||"unknown"===(null===(c=null==this?void 0:this.stateObj)||void 0===c?void 0:c.state)?z` + ${this.hass.localize("state.default.unavailable")} + `:""} + + + ${0===this.humidity?z` + + ${z`${Qe(this.current,this.hass.locale,{minimumFractionDigits:1,maximumFractionDigits:1})}`} + + ${z` + ${this.hass.config.unit_system.temperature} + `} + + + + `:z` + + ${z`${Qe(this._display_bottom,this.hass.locale,{minimumFractionDigits:1,maximumFractionDigits:1})}`} + + ${z` + ${this.hass.config.unit_system.temperature} + `} + + + + ${z`${Qe(this.humidity,this.hass.locale,{minimumFractionDigits:1,maximumFractionDigits:1})}`} + + % + + + + `} + + + +
+ ${(null==this?void 0:this._hasSummer)?z` + ${(null===(h=null==this?void 0:this._config)||void 0===h?void 0:h.disable_heat)?N``:this._renderIcon("heat",this.mode)} + ${(null===(d=null==this?void 0:this._config)||void 0===d?void 0:d.disable_eco)?N``:(null===(f=null===(p=null==this?void 0:this.stateObj)||void 0===p?void 0:p.attributes)||void 0===f?void 0:f.saved_temperature)&&"none"!==(null===(g=null===(m=null==this?void 0:this.stateObj)||void 0===m?void 0:m.attributes)||void 0===g?void 0:g.saved_temperature)&&"unavailable"!==(null===(_=null==this?void 0:this.stateObj)||void 0===_?void 0:_.state)?this._renderIcon("eco","eco"):this._renderIcon("eco","none")} + ${(null===(v=null==this?void 0:this._config)||void 0===v?void 0:v.disable_off)?N``:this._renderIcon("off",this.mode)} + `:z` + ${this.modes.map((t=>{var e,n,i;return(null===(e=this._config)||void 0===e?void 0:e.disable_heat)&&"heat"===t||(null===(n=this._config)||void 0===n?void 0:n.disable_eco)&&"eco"===t||(null===(i=this._config)||void 0===i?void 0:i.disable_off)&&"off"===t?N``:this._renderIcon(t,this.mode)}))} + `} + +
+
+
+ `}}connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback()}static async getConfigElement(){return await Promise.resolve().then((function(){return pc})),document.createElement("better-thermostat-ui-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>["climate"].includes(t.split(".")[0]))),n=e.filter((e=>{var n;return null===(n=t.states[e].attributes)||void 0===n?void 0:n.call_for_heat}));return{type:"custom:better-thermostat-ui-card",entity:n[0]||e[0]}}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}getCardSize(){return 1}_percent2bar(t){return 176-1.76*t}_value2percent(t){return(t-this.min)/(this.max-this.min)*100}_percent2value(t){return t/100*(this.max-this.min)+this.min}_updateValue(t){const e=Math.round(t/this.step)*this.step;this.value!==e&&(this.value=e,this._updateDisplay(),this._vibrate(2))}_updateDragger(t){this.dragging=t}_liveSnapPont(t,e){var n;const i=180/Math.PI,r=null===(n=null==t?void 0:t.shadowRoot)||void 0===n?void 0:n.querySelector("#shadowpath"),o=(null==r?void 0:r.getTotalLength())||0;return function(e){const n=function(t,e,n){let r,o,s=8,a=1/0;for(var l,u,c=0;c<=e;c+=s)(u=f(l=t.getPointAtLength(c))).5;){let n,i,l,u,c,h;(l=o-s)>=0&&(c=f(n=t.getPointAtLength(l))){clearTimeout(u._timeout),0===u._oldValueMin&&(u._oldValueMin=u.value),u._ignore=!0;let t=u.value;t-=u.step,t{t._ignore=!1,t._setTemperature(),t.requestUpdate("value",t._oldValueMin),t._oldValueMin=0}),600,u)})),null===(s=null===(o=null==this?void 0:this.shadowRoot)||void 0===o?void 0:o.querySelector("#c-plus"))||void 0===s||s.addEventListener("click",(()=>{clearTimeout(u._timeout),0===u._oldValueMax&&(u._oldValueMax=u.value),u._ignore=!0;let t=u.value;t+=u.step,t>u.max&&(t=u.max),u.value=t,u._updateDisplay(),u._timeout=setTimeout((t=>{t._ignore=!1,t._setTemperature(),t.requestUpdate("value",t._oldValueMax),t._oldValueMax=0}),600,u)})),ic.create(c,{type:"x,y",edgeResistance:1,liveSnap:{points:t=>this._liveSnapPont(u,t)},onRelease:()=>{u._updateDragger(!1),c.blur(),c.classList.remove("active");let t=new CustomEvent("value-changed",{detail:{value:this.value},bubbles:!0,composed:!0});this.dispatchEvent(t),this._setTemperature()},onPress:()=>{u._vibrate(30),c.classList.add("active"),c.focus()},onDragStart:function(){u._updateDragger(!0)}}),ua.to(c,{duration:0,repeat:0,repeatDelay:0,yoyo:!1,ease:"power1.inOut",motionPath:{path:null===(a=null==this?void 0:this.shadowRoot)||void 0===a?void 0:a.querySelector("#shadowpath"),autoRotate:!1,fromCurrent:!0,useRadians:!0,curviness:2,start:this._value2percent(this.value)/100||0,end:this._value2percent(this.value)/100||0}}),ua.to(h,{duration:0,repeat:0,repeatDelay:0,yoyo:!1,ease:"power1.inOut",motionPath:{path:null===(l=null==this?void 0:this.shadowRoot)||void 0===l?void 0:l.querySelector("#shadowpath"),autoRotate:!1,fromCurrent:!0,useRadians:!0,curviness:2,start:this._value2percent(this.current)/100||0,end:this._value2percent(this.current)/100||0}}),this._init=!1}shouldUpdate(t){return void 0!==t.has("_config")&&void 0!==t.get("_config")&&(this._hasSummer=!1,this._hasWindow=!1,this.humidity=0),void 0!==t.get("hass")&&(this._init=!1),!0}updated(t){var e,n,i,r;if(super.updated(t),this._ignore||this._init||this.dragging)return;const o=null===(e=null==this?void 0:this.shadowRoot)||void 0===e?void 0:e.querySelector(".value-handler"),s=null===(n=null==this?void 0:this.shadowRoot)||void 0===n?void 0:n.querySelector(".current-handler");t.has("value")&&ua.to(o,{duration:this._firstRender?0:5,repeat:0,repeatDelay:0,yoyo:!1,ease:"power1.inOut",motionPath:{path:null===(i=null==this?void 0:this.shadowRoot)||void 0===i?void 0:i.querySelector("#shadowpath"),autoRotate:!1,fromCurrent:!0,useRadians:!0,curviness:2,immediateRender:!0,start:this._value2percent(t.get("value"))/100||0,end:this._value2percent(this.value)/100||0}}),t.has("current")&&ua.to(s,{duration:this._firstRender?0:25,repeat:0,repeatDelay:0,yoyo:!1,ease:"power1.inOut",motionPath:{path:null===(r=null==this?void 0:this.shadowRoot)||void 0===r?void 0:r.querySelector("#shadowpath"),autoRotate:!1,fromCurrent:!0,useRadians:!0,curviness:2,start:this._value2percent(t.get("current"))/100||0,end:this._value2percent(this.current)/100||0}}),this._firstRender=!1}willUpdate(t){if(!this.hass||!this._config||!t.has("hass")&&!t.has("_config"))return;const e=this._config.entity,n=this.hass.states[e];if(!n)return;const i=t.get("hass");if(!i||i.states[e]!==n){if(!this._config||!this.hass||!this._config.entity)return;this.stateObj=n;const t=this.stateObj.attributes,e=this.stateObj.state;this.mode=e||"off",t.hvac_modes&&(this.modes=Object.values(t.hvac_modes)),t.temperature&&(this.value=t.temperature),t.target_temp_step&&(this.step=t.target_temp_step),t.min_temp&&(this.min=t.min_temp),t.max_temp&&(this.max=t.max_temp),t.current_temperature&&(this.current=t.current_temperature),void 0!==(null==t?void 0:t.humidity)&&(this.humidity=parseFloat(t.humidity)),void 0!==(null==t?void 0:t.window_open)&&(this._hasWindow=!0,this.window=t.window_open),void 0!==(null==t?void 0:t.call_for_heat)&&(this._hasSummer=!0,this.summer=!t.call_for_heat),this._updateDisplay()}}_updateDisplay(){var t;(null===(t=null==this?void 0:this._config)||void 0===t?void 0:t.set_current_as_main)?(this._display_bottom=this.value,this._display_top=this.current):(this._display_bottom=this.current,this._display_top=this.value)}_handleAction(t){var e,n,i;if("eco"===t.currentTarget.mode){null===((null===(n=null===(e=null==this?void 0:this.stateObj)||void 0===e?void 0:e.attributes)||void 0===n?void 0:n.saved_temperature)||null)?this.hass.callService("better_thermostat","set_temp_target_temperature",{entity_id:this._config.entity,temperature:(null===(i=this._config)||void 0===i?void 0:i.eco_temperature)||18}):this.hass.callService("better_thermostat","restore_saved_target_temperature",{entity_id:this._config.entity})}else this.hass.callService("climate","set_hvac_mode",{entity_id:this._config.entity,hvac_mode:t.currentTarget.mode})}_setTemperature(){this.hass.callService("climate","set_temperature",{entity_id:this._config.entity,temperature:this.value})}_renderIcon(t,e){if(!rc[t])return N``;const n=this.hass.localize(`component.climate.state._.${t}`)||Xe({hass:this.hass,string:`extra_states.${t}`});return N` + + + `}_handleMoreInfo(){Ke(this,"hass-more-info",{entityId:this._config.entity})}};sc.styles=s` + :host { + display: block; + overflow: hidden; + box-sizing: border-box; + } + + ha-card { + height: 100%; + width: 100%; + vertical-align: middle; + justify-content: center; + justify-items: center; + padding-left: 1em; + padding-right: 1em; + box-sizing: border-box; + } + + .unavailable { + opacity: 0.3; + } + + .unavailable #bar, .unavailable .main-value, .unavailable #value,.unavailable #current, .unavailable .current-info, + .unknown #bar, .unknown .main-value, .unknown #value,.unknown #current, .unknown .current-info { + display: none; + } + + .more-info { + position: absolute; + cursor: pointer; + top: 0px; + right: 0px; + inset-inline-end: 0px; + inset-inline-start: initial; + border-radius: 100%; + color: var(--secondary-text-color); + z-index: 1; + direction: var(--direction); + } + .container { + position: relative; + width: 100%; + height: 100%; + } + .content { + margin: -0.5em auto; + position: relative; + width: 100%; + box-sizing: border-box; + } + .name { + display: block; + width: 100%; + text-align: center; + font-size: 20px; + padding-top: 1em; + } + svg { + height: auto; + margin: auto; + display: block; + width: 100%; + + transform: scale(1.5); + -webkit-backface-visibility: hidden; + max-width: 255px; + } + + path { + stroke-linecap: round; + stroke-width: 1; + } + + text { + fill: var(--primary-text-color); + } + + .window_open { + --mode-color: var(--energy-grid-consumption-color) + } + + .summer { + --mode-color: var(--state-not_home-color) + } + + .auto, + .heat_cool { + --mode-color: var(--state-climate-auto-color); + } + .cool { + --mode-color: var(--state-climate-cool-color); + } + .heat { + --mode-color: var(--label-badge-red); + } + .manual { + --mode-color: var(--state-climate-manual-color); + } + .off { + --mode-color: var(--state-climate-off-color); + } + .fan_only { + --mode-color: var(--state-climate-fan_only-color); + } + .eco { + --mode-color: var(--state-climate-eco-color); + } + .dry { + --mode-color: var(--state-climate-dry-color); + } + .idle { + --mode-color: var(--state-climate-idle-color); + } + .unknown-mode { + --mode-color: var(--state-unknown-color); + } + + #modes { + z-index: 1; + position: relative; + display: flex; + width: auto; + justify-content: center; + margin-top: 1em; + margin-bottom: 1em; + } + + #modes > * { + color: var(--disabled-text-color); + cursor: pointer; + display: inline-block; + } + #modes .selected-icon { + color: var(--mode-color); + } + + #shadowpath { + stroke: #e7e7e8; + } + + #value { + fill: var(--mode-color); + r: 5; + z-index: 9999 !important; + transition: r 0.3s ease-in-out, fill 0.6s ease-in-out; + } + + #value,#current { + filter: drop-shadow(0px 0px 1px #000); + } + + #value:hover, #value:active, #value:focus, #value.active { + r: 8 !important; + } + + #current { + pointer-events: none; + fill: var(--label-badge-grey); + } + + .status { + transition: fill 0.6s ease-in-out, filter 0.6s ease-in-out; + filter: none; + } + .status.active { + fill: var(--error-color); + filter: drop-shadow(0px 0px 6px var(--error-color)); + } + + #bar { + stroke: var(--mode-color); + stroke-dasharray: 176; + stroke-dashoffset: 0; + transition: stroke-dashoffset 5.1s ease-in-out 0s, stroke 0.6s ease-in-out; + } + + #bar.drag { + transition: none !important; + } + #c-minus,#c-plus { + cursor: pointer; + } + .control { + cursor: pointer; + pointer-events: none; + } + ha-icon-button { + transition: color 0.6s ease-in-out; + } + .eco ha-icon-button[title="heat"], .window_open ha-icon-button[title="heat"], .summer ha-icon-button[title="heat"] { + --mode-color: var(--disabled-text-color); + } + .summer,.window { + transition: fill 0.3s ease; + fill: var(--disabled-text-color); + } + line { + stroke: var(--disabled-text-color); + } + .summer.active { + fill: var(--state-not_home-color); + } + .window.active { + fill: var(--energy-grid-consumption-color); + } + `,t([st({attribute:!1})],sc.prototype,"hass",void 0),t([st({type:Number})],sc.prototype,"value",void 0),t([st({type:Number})],sc.prototype,"current",void 0),t([st({type:Number})],sc.prototype,"humidity",void 0),t([st({type:Number})],sc.prototype,"min",void 0),t([st({type:Number})],sc.prototype,"max",void 0),t([st({type:Number})],sc.prototype,"step",void 0),t([st({type:Boolean})],sc.prototype,"window",void 0),t([st({type:Boolean})],sc.prototype,"summer",void 0),t([st({type:String})],sc.prototype,"status",void 0),t([st({type:String})],sc.prototype,"mode",void 0),t([st({type:Boolean,reflect:!0})],sc.prototype,"dragging",void 0),t([at()],sc.prototype,"_config",void 0),sc=t([rt("better-thermostat-ui-card")],sc);const ac=function(){for(var t=arguments.length,e=new Array(t),n=0;nt.schema)),o=Object.assign({},...r);return i?vn(o):mn(o)}(mn({index:gn(fn()),view_index:gn(fn()),view_layout:cn("any",(()=>!0)),type:_n()}),mn({entity:gn(_n()),name:gn(_n()),icon:gn(_n())}),mn({disable_window:gn(dn()),disable_summer:gn(dn()),disable_eco:gn(dn()),disable_heat:gn(dn()),disable_off:gn(dn()),set_current_as_main:gn(dn()),eco_temperature:gn(fn())})),lc=["icon_color","layout","fill_container","primary_info","secondary_info","icon_type","content_info","use_entity_picture","collapsible_controls","icon_animation"],uc=t=>{var e,n;customElements.get("ha-form")&&(customElements.get("hui-action-editor")||((t,e,n,i)=>{const[r,o,s]=t.split(".",3);return Number(r)>e||Number(r)===e&&(void 0===i?Number(o)>=n:Number(o)>n)||void 0!==i&&Number(r)===e&&Number(o)===n&&Number(s)>=i})(t,2022,11))||null===(e=customElements.get("hui-button-card"))||void 0===e||e.getConfigElement(),customElements.get("ha-entity-picker")||null===(n=customElements.get("hui-entities-card"))||void 0===n||n.getConfigElement()},cc=["eco_temperature","disable_window","disable_summer","disable_eco","disable_heat","disable_off","set_current_as_main"],hc=Ze((()=>[{name:"entity",selector:{entity:{domain:["climate"]}}},{name:"name",selector:{text:{}}},{name:"eco_temperature",selector:{number:{placeholder:20,min:5,max:45}}},{type:"grid",name:"",schema:[{name:"disable_window",selector:{boolean:{}}},{name:"disable_summer",selector:{boolean:{}}},{name:"disable_eco",selector:{boolean:{}}},{name:"disable_heat",selector:{boolean:{}}},{name:"disable_off",selector:{boolean:{}}},{name:"set_current_as_main",selector:{boolean:{}}}]}]));let dc=class extends nt{constructor(){super(...arguments),this._computeLabel=t=>{const e=(n=this.hass,function(t){var e;let i=Ye(t,null!==(e=null==n?void 0:n.locale.language)&&void 0!==e?e:"en");return i||(i=Ye(t,"en")),null!=i?i:t});var n;return lc.includes(t.name)?e(`editor.card.generic.${t.name}`):cc.includes(t.name)?e(`editor.card.climate.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),uc(this.hass.connection.haVersion)}setConfig(t){ln(t,ac),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=hc();return N` + + `}_valueChanged(t){Ke(this,"config-changed",{config:t.detail.value}),Ke(this,"hass",{config:t.detail.value})}};t([at()],dc.prototype,"_config",void 0),t([st({attribute:!1})],dc.prototype,"hass",void 0),dc=t([rt("better-thermostat-ui-card-editor")],dc);var pc=Object.freeze({__proto__:null,get ClimateCardEditor(){return dc}});export{sc as BetterThermostatUi,oc as registerCustomCard}; diff --git a/www/frigate-card/_commonjsHelpers-1789f0cf.js b/www/frigate-card/_commonjsHelpers-1789f0cf.js new file mode 100644 index 00000000..d6f8d42a --- /dev/null +++ b/www/frigate-card/_commonjsHelpers-1789f0cf.js @@ -0,0 +1 @@ +var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function o(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}export{e as c,o as g}; diff --git a/www/frigate-card/audio-557099cb.js b/www/frigate-card/audio-557099cb.js new file mode 100644 index 00000000..268044d1 --- /dev/null +++ b/www/frigate-card/audio-557099cb.js @@ -0,0 +1 @@ +const o=o=>void 0!==o.mozHasAudio?o.mozHasAudio:void 0===o.audioTracks||Boolean(o.audioTracks?.length);export{o as m}; diff --git a/www/frigate-card/card-555679fd.js b/www/frigate-card/card-555679fd.js new file mode 100644 index 00000000..6f71740c --- /dev/null +++ b/www/frigate-card/card-555679fd.js @@ -0,0 +1,565 @@ +function e(e,t,n,i){var a,r=arguments.length,o=r<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,n):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,n,i);else for(var s=e.length-1;s>=0;s--)(a=e[s])&&(o=(r<3?a(o):r>3?a(t,n,o):a(t,n))||o);return r>3&&o&&Object.defineProperty(t,n,o),o}var t=window&&window.__assign||function(){return t=Object.assign||function(e){for(var t,n=1,i=arguments.length;nnew y("string"==typeof e?e:e+"",void 0,v),w=(e,...t)=>{const n=1===e.length?e[0]:t.reduce(((t,n,i)=>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.")})(n)+e[i+1]),e[0]);return new y(n,e,v)},x=(e,t)=>{g?e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):t.forEach((t=>{const n=document.createElement("style"),i=f.litNonce;void 0!==i&&n.setAttribute("nonce",i),n.textContent=t.cssText,e.appendChild(n)}))},C=g?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const n of e.cssRules)t+=n.cssText;return b(t)})(e):e +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var $;const k=window,E=k.trustedTypes,M=E?E.emptyScript:"",S=k.reactiveElementPolyfillSupport,T={toAttribute(e,t){switch(t){case Boolean:e=e?M:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let 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}},A=(e,t)=>t!==e&&(t==t||e==e),z={attribute:!0,type:String,converter:T,reflect:!1,hasChanged:A};class j 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,n)=>{const i=this._$Ep(n,t);void 0!==i&&(this._$Ev.set(i,n),e.push(i))})),e}static createProperty(e,t=z){if(t.state&&(t.attribute=!1),this.finalize(),this.elementProperties.set(e,t),!t.noAccessor&&!this.prototype.hasOwnProperty(e)){const n="symbol"==typeof e?Symbol():"__"+e,i=this.getPropertyDescriptor(e,n,t);void 0!==i&&Object.defineProperty(this.prototype,e,i)}}static getPropertyDescriptor(e,t,n){return{get(){return this[t]},set(i){const a=this[e];this[t]=i,this.requestUpdate(e,a,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)||z}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 n of t)this.createProperty(n,e[n])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const n=new Set(e.flat(1/0).reverse());for(const e of n)t.unshift(C(e))}else void 0!==e&&t.push(C(e));return t}static _$Ep(e,t){const n=t.attribute;return!1===n?void 0:"string"==typeof n?n:"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,n;(null!==(t=this._$ES)&&void 0!==t?t:this._$ES=[]).push(e),void 0!==this.renderRoot&&this.isConnected&&(null===(n=e.hostConnected)||void 0===n||n.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 x(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,n){this._$AK(e,n)}_$EO(e,t,n=z){var i;const a=this.constructor._$Ep(e,n);if(void 0!==a&&!0===n.reflect){const r=(void 0!==(null===(i=n.converter)||void 0===i?void 0:i.toAttribute)?n.converter:T).toAttribute(t,n.type);this._$El=e,null==r?this.removeAttribute(a):this.setAttribute(a,r),this._$El=null}}_$AK(e,t){var n;const i=this.constructor,a=i._$Ev.get(e);if(void 0!==a&&this._$El!==a){const e=i.getPropertyOptions(a),r="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==(null===(n=e.converter)||void 0===n?void 0:n.fromAttribute)?e.converter:T;this._$El=a,this[a]=r.fromAttribute(t,e.type),this._$El=null}}requestUpdate(e,t,n){let i=!0;void 0!==e&&(((n=n||this.constructor.getPropertyOptions(e)).hasChanged||A)(this[e],t)?(this._$AL.has(e)||this._$AL.set(e,t),!0===n.reflect&&this._$El!==e&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(e,n))):i=!1),!this.isUpdatePending&&i&&(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 n=this._$AL;try{t=this.shouldUpdate(n),t?(this.willUpdate(n),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(n)):this._$Ek()}catch(e){throw t=!1,this._$Ek(),e}t&&this._$AE(n)}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 O;j.finalized=!0,j.elementProperties=new Map,j.elementStyles=[],j.shadowRootOptions={mode:"open"},null==S||S({ReactiveElement:j}),(null!==($=k.reactiveElementVersions)&&void 0!==$?$:k.reactiveElementVersions=[]).push("1.6.1");const I=window,R=I.trustedTypes,D=R?R.createPolicy("lit-html",{createHTML:e=>e}):void 0,P=`lit$${(Math.random()+"").slice(9)}$`,L="?"+P,N=`<${L}>`,U=document,F=(e="")=>U.createComment(e),H=e=>null===e||"object"!=typeof e&&"function"!=typeof e,Z=Array.isArray,q=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,V=/-->/g,W=/>/g,B=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),Y=/'/g,Q=/"/g,G=/^(?:script|style|textarea|title)$/i,K=(e=>(t,...n)=>({_$litType$:e,strings:t,values:n}))(1),X=Symbol.for("lit-noChange"),J=Symbol.for("lit-nothing"),ee=new WeakMap,te=U.createTreeWalker(U,129,null,!1),ne=(e,t)=>{const n=e.length-1,i=[];let a,r=2===t?"":"",o=q;for(let t=0;t"===c[0]?(o=null!=a?a:q,l=-1):void 0===c[1]?l=-2:(l=o.lastIndex-c[2].length,s=c[1],o=void 0===c[3]?B:'"'===c[3]?Q:Y):o===Q||o===Y?o=B:o===V||o===W?o=q:(o=B,a=void 0);const u=o===B&&e[t+1].startsWith("/>")?" ":"";r+=o===q?n+N:l>=0?(i.push(s),n.slice(0,l)+"$lit$"+n.slice(l)+P+u):n+P+(-2===l?(i.push(void 0),t):u)}const s=r+(e[n]||"")+(2===t?"":"");if(!Array.isArray(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==D?D.createHTML(s):s,i]};class ie{constructor({strings:e,_$litType$:t},n){let i;this.parts=[];let a=0,r=0;const o=e.length-1,s=this.parts,[c,l]=ne(e,t);if(this.el=ie.createElement(c,n),te.currentNode=this.el.content,2===t){const e=this.el.content,t=e.firstChild;t.remove(),e.append(...t.childNodes)}for(;null!==(i=te.nextNode())&&s.length0){i.textContent=R?R.emptyScript:"";for(let n=0;nZ(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!==J&&H(this._$AH)?this._$AA.nextSibling.data=e:this.T(U.createTextNode(e)),this._$AH=e}$(e){var t;const{values:n,_$litType$:i}=e,a="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=ie.createElement(i.h,this.options)),i);if((null===(t=this._$AH)||void 0===t?void 0:t._$AD)===a)this._$AH.p(n);else{const e=new re(a,this),t=e.v(this.options);e.p(n),this.T(t),this._$AH=e}}_$AC(e){let t=ee.get(e.strings);return void 0===t&&ee.set(e.strings,t=new ie(e)),t}k(e){Z(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let n,i=0;for(const a of e)i===t.length?t.push(n=new oe(this.O(F()),this.O(F()),this,this.options)):n=t[i],n._$AI(a),i++;i2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=J}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(e,t=this,n,i){const a=this.strings;let r=!1;if(void 0===a)e=ae(this,e,t,0),r=!H(e)||e!==this._$AH&&e!==X,r&&(this._$AH=e);else{const i=e;let o,s;for(e=a[0],o=0;o{var i,a;const r=null!==(i=null==n?void 0:n.renderBefore)&&void 0!==i?i:t;let o=r._$litPart$;if(void 0===o){const e=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;r._$litPart$=o=new oe(t.insertBefore(F(),e),e,void 0,null!=n?n:{})}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 X}}ge.finalized=!0,ge._$litElement$=!0,null===(pe=globalThis.litElementHydrateSupport)||void 0===pe||pe.call(globalThis,{LitElement:ge});const ve=globalThis.litElementPolyfillSupport;null==ve||ve({LitElement:ge}),(null!==(fe=globalThis.litElementVersions)&&void 0!==fe?fe:globalThis.litElementVersions=[]).push("3.2.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const _e=e=>t=>"function"==typeof t?((e,t)=>(customElements.define(e,t),t))(e,t):((e,t)=>{const{kind:n,elements:i}=t;return{kind:n,elements:i,finisher(t){customElements.define(e,t)}}})(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,ye=(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 be(e){return(t,n)=>void 0!==n?((e,t,n)=>{t.constructor.createProperty(n,e)})(e,t,n):ye(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function we(e){return be({...e,state:!0})} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var xe;null===(xe=window.HTMLSlotElement)||void 0===xe||xe.prototype.assignedElements; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const Ce={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},$e=e=>(...t)=>({_$litDirective$:e,values:t});class ke{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,n){this._$Ct=e,this._$AM=t,this._$Ci=n}_$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 Ee=$e(class extends ke{constructor(e){var t;if(super(e),e.type!==Ce.ATTRIBUTE||"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 n,i;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===(n=this.st)||void 0===n?void 0:n.has(e))&&this.nt.add(e);return this.render(t)}const a=e.element.classList;this.nt.forEach((e=>{e in t||(a.remove(e),this.nt.delete(e))}));for(const e in t){const n=!!t[e];n===this.nt.has(e)||(null===(i=this.st)||void 0===i?void 0:i.has(e))||(n?(a.add(e),this.nt.add(e)):(a.remove(e),this.nt.delete(e)))}return X}}),Me=e=>null===e||"object"!=typeof e&&"function"!=typeof e,Se=e=>void 0===e.strings,Te={},Ae=(e,t=Te)=>e._$AH=t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,ze=(e,t)=>{var n,i;const a=e._$AN;if(void 0===a)return!1;for(const e of a)null===(i=(n=e)._$AO)||void 0===i||i.call(n,t,!1),ze(e,t);return!0},je=e=>{let t,n;do{if(void 0===(t=e._$AM))break;n=t._$AN,n.delete(e),e=t}while(0===(null==n?void 0:n.size))},Oe=e=>{for(let t;t=e._$AM;e=t){let n=t._$AN;if(void 0===n)t._$AN=n=new Set;else if(n.has(e))break;n.add(e),De(t)}}; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function Ie(e){void 0!==this._$AN?(je(this),this._$AM=e,Oe(this)):this._$AM=e}function Re(e,t=!1,n=0){const i=this._$AH,a=this._$AN;if(void 0!==a&&0!==a.size)if(t)if(Array.isArray(i))for(let e=n;e{var t,n,i,a;e.type==Ce.CHILD&&(null!==(t=(i=e)._$AP)&&void 0!==t||(i._$AP=Re),null!==(n=(a=e)._$AQ)&&void 0!==n||(a._$AQ=Ie))};class Pe extends ke{constructor(){super(...arguments),this._$AN=void 0}_$AT(e,t,n){super._$AT(e,t,n),Oe(this),this.isConnected=e._$AU}_$AO(e,t=!0){var n,i;e!==this.isConnected&&(this.isConnected=e,e?null===(n=this.reconnected)||void 0===n||n.call(this):null===(i=this.disconnected)||void 0===i||i.call(this)),t&&(ze(this,e),je(this))}setValue(e){if(Se(this._$Ct))this._$Ct._$AI(e,this);else{const t=[...this._$Ct._$AH];t[this._$Ci]=e,this._$Ct._$AI(t,this,0)}}disconnected(){}reconnected(){}} +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const Le=()=>new Ne;class Ne{}const Ue=new WeakMap,Fe=$e(class extends Pe{render(e){return J}update(e,[t]){var n;const i=t!==this.Y;return i&&void 0!==this.Y&&this.rt(void 0),(i||this.lt!==this.ct)&&(this.Y=t,this.dt=null===(n=e.options)||void 0===n?void 0:n.host,this.rt(this.ct=e.element)),J}rt(e){var t;if("function"==typeof this.Y){const n=null!==(t=this.dt)&&void 0!==t?t:globalThis;let i=Ue.get(n);void 0===i&&(i=new WeakMap,Ue.set(n,i)),void 0!==i.get(this.Y)&&this.Y.call(this.dt,void 0),i.set(this.Y,e),void 0!==e&&this.Y.call(this.dt,e)}else this.Y.value=e}get lt(){var e,t,n;return"function"==typeof this.Y?null===(t=Ue.get(null!==(e=this.dt)&&void 0!==e?e:globalThis))||void 0===t?void 0:t.get(this.Y):null===(n=this.Y)||void 0===n?void 0:n.value}disconnected(){this.lt===this.ct&&this.rt(void 0)}reconnected(){this.rt(this.ct)}}),He=$e(class extends ke{constructor(e){var t;if(super(e),e.type!==Ce.ATTRIBUTE||"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,n)=>{const i=e[n];return null==i?t:t+`${n=n.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${i};`}),"")}update(e,[t]){const{style:n}=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("-")?n.removeProperty(e):n[e]="")}));for(const e in t){const i=t[e];null!=i&&(this.vt.add(e),e.includes("-")?n.setProperty(e,i):n[e]=i)}return X}}); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function Ze(e,t){return e===t||e!=e&&t!=t}function qe(e,t){for(var n=e.length;n--;)if(Ze(e[n][0],t))return n;return-1}var Ve=Array.prototype.splice;function We(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1},We.prototype.set=function(e,t){var n=this.__data__,i=qe(n,e);return i<0?(++this.size,n.push([e,t])):n[i][1]=t,this};var Be="object"==typeof global&&global&&global.Object===Object&&global,Ye="object"==typeof self&&self&&self.Object===Object&&self,Qe=Be||Ye||Function("return this")(),Ge=Qe.Symbol,Ke=Object.prototype,Xe=Ke.hasOwnProperty,Je=Ke.toString,et=Ge?Ge.toStringTag:void 0;var tt=Object.prototype.toString;var nt="[object Null]",it="[object Undefined]",at=Ge?Ge.toStringTag:void 0;function rt(e){return null==e?void 0===e?it:nt:at&&at in Object(e)?function(e){var t=Xe.call(e,et),n=e[et];try{e[et]=void 0;var i=!0}catch(e){}var a=Je.call(e);return i&&(t?e[et]=n:delete e[et]),a}(e):function(e){return tt.call(e)}(e)}function ot(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}var st="[object AsyncFunction]",ct="[object Function]",lt="[object GeneratorFunction]",dt="[object Proxy]";function ut(e){if(!ot(e))return!1;var t=rt(e);return t==ct||t==lt||t==st||t==dt}var ht,mt=Qe["__core-js_shared__"],pt=(ht=/[^.]+$/.exec(mt&&mt.keys&&mt.keys.IE_PROTO||""))?"Symbol(src)_1."+ht:"";var ft=Function.prototype.toString;function gt(e){if(null!=e){try{return ft.call(e)}catch(e){}try{return e+""}catch(e){}}return""}var vt=/^\[object .+?Constructor\]$/,_t=Function.prototype,yt=Object.prototype,bt=_t.toString,wt=yt.hasOwnProperty,xt=RegExp("^"+bt.call(wt).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function Ct(e){return!(!ot(e)||(t=e,pt&&pt in t))&&(ut(e)?xt:vt).test(gt(e));var t}function $t(e,t){var n=function(e,t){return null==e?void 0:e[t]}(e,t);return Ct(n)?n:void 0}var kt=$t(Qe,"Map"),Et=$t(Object,"create");var Mt="__lodash_hash_undefined__",St=Object.prototype.hasOwnProperty;var Tt=Object.prototype.hasOwnProperty;var At="__lodash_hash_undefined__";function zt(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1&&e%1==0&&e-1&&e%1==0&&e<=nn}var rn={};function on(e){return function(t){return e(t)}}rn["[object Float32Array]"]=rn["[object Float64Array]"]=rn["[object Int8Array]"]=rn["[object Int16Array]"]=rn["[object Int32Array]"]=rn["[object Uint8Array]"]=rn["[object Uint8ClampedArray]"]=rn["[object Uint16Array]"]=rn["[object Uint32Array]"]=!0,rn["[object Arguments]"]=rn["[object Array]"]=rn["[object ArrayBuffer]"]=rn["[object Boolean]"]=rn["[object DataView]"]=rn["[object Date]"]=rn["[object Error]"]=rn["[object Function]"]=rn["[object Map]"]=rn["[object Number]"]=rn["[object Object]"]=rn["[object RegExp]"]=rn["[object Set]"]=rn["[object String]"]=rn["[object WeakMap]"]=!1;var sn="object"==typeof exports&&exports&&!exports.nodeType&&exports,cn=sn&&"object"==typeof module&&module&&!module.nodeType&&module,ln=cn&&cn.exports===sn&&Be.process,dn=function(){try{var e=cn&&cn.require&&cn.require("util").types;return e||ln&&ln.binding&&ln.binding("util")}catch(e){}}(),un=dn&&dn.isTypedArray,hn=un?on(un):function(e){return Ft(e)&&an(e.length)&&!!rn[rt(e)]},mn=Object.prototype.hasOwnProperty;function pn(e,t){var n=Yt(e),i=!n&&Bt(e),a=!n&&!i&&Xt(e),r=!n&&!i&&!a&&hn(e),o=n||i||a||r,s=o?function(e,t){for(var n=-1,i=Array(e);++ns))return!1;var l=r.get(e),d=r.get(t);if(l&&d)return l==t&&d==e;var u=-1,h=!0,m=n&na?new Xi:void 0;for(r.set(e,t),r.set(t,e);++u0){if(++Ga>=Wa)return arguments[0]}else Ga=0;return Qa.apply(void 0,arguments)});function Ja(e,t){return Xa(function(e,t,n){return t=qa(void 0===t?e.length-1:t,0),function(){for(var i=arguments,a=-1,r=qa(i.length-t,0),o=Array(r);++a1?t[i-1]:void 0,r=i>2?t[2]:void 0;for(a=er.length>3&&"function"==typeof a?(i--,a):void 0,r&&function(e,t,n){if(!ot(n))return!1;var i=typeof t;return!!("number"==i?bn(n)&&tn(t,n.length):"string"==i&&t in n)&&Ze(n[t],e)}(t[0],t[1],r)&&(a=i<3?void 0:a,i=1),e=Object(e);++n=t||n<0||u&&e-l>=r}function f(){var e=nr();if(p(e))return g(e);s=setTimeout(f,function(e){var n=t-(e-c);return u?gr(n,r-(e-l)):n}(e))}function g(e){return s=void 0,h&&i?m(e):(i=a=void 0,o)}function v(){var e=nr(),n=p(e);if(i=arguments,a=this,c=e,n){if(void 0===s)return function(e){return l=e,s=setTimeout(f,t),d?m(e):o}(c);if(u)return clearTimeout(s),s=setTimeout(f,t),m(c)}return void 0===s&&(s=setTimeout(f,t)),o}return t=mr(t)||0,ot(n)&&(d=!!n.leading,r=(u="maxWait"in n)?fr(mr(n.maxWait)||0,t):r,h="trailing"in n?!!n.trailing:h),v.cancel=function(){void 0!==s&&clearTimeout(s),l=0,i=c=a=s=void 0},v.flush=function(){return void 0===s?o:g(nr())},v}var _r="Expected a function";function yr(e,t,n){var i=!0,a=!0;if("function"!=typeof e)throw new TypeError(_r);return ot(n)&&(i="leading"in n?!!n.leading:i,a="trailing"in n?!!n.trailing:a),vr(e,t,{leading:i,maxWait:t,trailing:a})}const br=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"],["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError"]],wr=(()=>{if("undefined"==typeof document)return!1;const e=br[0],t={};for(const n of br){const i=n?.[1];if(i in document){for(const[i,a]of n.entries())t[e[i]]=a;return t}}return!1})(),xr={change:wr.fullscreenchange,error:wr.fullscreenerror};let Cr={request:(e=document.documentElement,t)=>new Promise(((n,i)=>{const a=()=>{Cr.off("change",a),n()};Cr.on("change",a);const r=e[wr.requestFullscreen](t);r instanceof Promise&&r.then(a).catch(i)})),exit:()=>new Promise(((e,t)=>{if(!Cr.isFullscreen)return void e();const n=()=>{Cr.off("change",n),e()};Cr.on("change",n);const i=document[wr.exitFullscreen]();i instanceof Promise&&i.then(n).catch(t)})),toggle:(e,t)=>Cr.isFullscreen?Cr.exit():Cr.request(e,t),onchange(e){Cr.on("change",e)},onerror(e){Cr.on("error",e)},on(e,t){const n=xr[e];n&&document.addEventListener(n,t,!1)},off(e,t){const n=xr[e];n&&document.removeEventListener(n,t,!1)},raw:wr};Object.defineProperties(Cr,{isFullscreen:{get:()=>Boolean(document[wr.fullscreenElement])},element:{enumerable:!0,get:()=>document[wr.fullscreenElement]??void 0},isEnabled:{enumerable:!0,get:()=>Boolean(document[wr.fullscreenEnabled])}}),wr||(Cr={isEnabled:!1});var $r=Cr;function kr(e,t,n,i=20,a=0){let r=[];if(a>=i)return r;const o=e=>{const r=e.assignedNodes().filter((e=>1===e.nodeType));return r.length>0?kr(r[0].parentElement,t,n,i,a+1):[]},s=Array.from(e.children||[]);for(const e of s)t(e)||(n(e)&&r.push(e),null!=e.shadowRoot?r.push(...kr(e.shadowRoot,t,n,i,a+1)):"SLOT"===e.tagName?r.push(...o(e)):r.push(...kr(e,t,n,i,a+1)));return r}function Er(e){return e.hasAttribute("hidden")||e.hasAttribute("aria-hidden")&&"false"!==e.getAttribute("aria-hidden")||"none"===e.style.display||"0"===e.style.opacity||"hidden"===e.style.visibility||"collapse"===e.style.visibility}function Mr(e){return"-1"!==e.getAttribute("tabindex")&&!Er(e)&&!function(e){return e.hasAttribute("disabled")||e.hasAttribute("aria-disabled")&&"false"!==e.getAttribute("aria-disabled")}(e)&&(e.hasAttribute("tabindex")||(e instanceof HTMLAnchorElement||e instanceof HTMLAreaElement)&&e.hasAttribute("href")||e instanceof HTMLButtonElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement||e instanceof HTMLSelectElement||e instanceof HTMLIFrameElement)}const Sr=new Map;const Tr=document.createElement("template");Tr.innerHTML='\n\t
\n\t
\n\t\n\t
\n';class Ar extends HTMLElement{constructor(){super(),this.debounceId=Math.random().toString(),this._focused=!1;const e=this.attachShadow({mode:"open"});e.appendChild(Tr.content.cloneNode(!0)),this.$backup=e.querySelector("#backup"),this.$start=e.querySelector("#start"),this.$end=e.querySelector("#end"),this.focusLastElement=this.focusLastElement.bind(this),this.focusFirstElement=this.focusFirstElement.bind(this),this.onFocusIn=this.onFocusIn.bind(this),this.onFocusOut=this.onFocusOut.bind(this)}static get observedAttributes(){return["inactive"]}get inactive(){return this.hasAttribute("inactive")}set inactive(e){e?this.setAttribute("inactive",""):this.removeAttribute("inactive")}get focused(){return this._focused}connectedCallback(){this.$start.addEventListener("focus",this.focusLastElement),this.$end.addEventListener("focus",this.focusFirstElement),this.addEventListener("focusin",this.onFocusIn),this.addEventListener("focusout",this.onFocusOut),this.render()}disconnectedCallback(){this.$start.removeEventListener("focus",this.focusLastElement),this.$end.removeEventListener("focus",this.focusFirstElement),this.removeEventListener("focusin",this.onFocusIn),this.removeEventListener("focusout",this.onFocusOut)}attributeChangedCallback(){this.render()}focusFirstElement(){this.trapFocus()}focusLastElement(){this.trapFocus(!0)}getFocusableElements(){return kr(this,Er,Mr)}trapFocus(e){if(this.inactive)return;let t=this.getFocusableElements();t.length>0?(e?t[t.length-1].focus():t[0].focus(),this.$backup.setAttribute("tabindex","-1")):(this.$backup.setAttribute("tabindex","0"),this.$backup.focus())}onFocusIn(){this.updateFocused(!0)}onFocusOut(){this.updateFocused(!1)}updateFocused(e){!function(e,t,n){const i=Sr.get(n);null!=i&&window.clearTimeout(i),Sr.set(n,window.setTimeout((()=>{e(),Sr.delete(n)}),t))}((()=>{this.focused!==e&&(this._focused=e,this.render())}),0,this.debounceId)}render(){this.$start.setAttribute("tabindex",!this.focused||this.inactive?"-1":"0"),this.$end.setAttribute("tabindex",!this.focused||this.inactive?"-1":"0"),this.focused?this.setAttribute("focused",""):this.removeAttribute("focused")}}function zr(e){return Number(e.getAttribute("data-dialog-count"))||0}function jr(e,t){e.setAttribute("data-dialog-count",t.toString())}function Or(e=document.activeElement){return null!=e&&null!=e.shadowRoot&&null!=e.shadowRoot.activeElement?Or(e.shadowRoot.activeElement):e}window.customElements.define("focus-trap",Ar);const Ir=document.createElement("template");Ir.innerHTML='\n \n
\n \n \n \n';class Rr extends HTMLElement{constructor(){super(),this.$scrollContainer=document.documentElement,this.$previousActiveElement=null;const e=this.attachShadow({mode:"open"});e.appendChild(Ir.content.cloneNode(!0)),this.$dialog=e.querySelector("#dialog"),this.$backdrop=e.querySelector("#backdrop"),this.onBackdropClick=this.onBackdropClick.bind(this),this.onKeyDown=this.onKeyDown.bind(this),this.setAttribute("aria-modal","true"),this.$dialog.setAttribute("role","alertdialog")}static get observedAttributes(){return["open","center"]}get open(){return this.hasAttribute("open")}set open(e){e?this.setAttribute("open",""):this.removeAttribute("open")}get center(){return this.hasAttribute("center")}set center(e){e?this.setAttribute("center",""):this.removeAttribute("center")}connectedCallback(){this.$backdrop.addEventListener("click",this.onBackdropClick)}disconnectedCallback(){this.$backdrop.removeEventListener("click",this.onBackdropClick),this.open&&this.didClose()}show(){this.open=!0}close(e){this.result=e,this.open=!1}onBackdropClick(){this.assertClosing()&&this.close()}onKeyDown(e){if("Escape"===e.code)this.assertClosing()&&(this.close(),e.stopImmediatePropagation())}assertClosing(){return this.dispatchEvent(new CustomEvent("closing",{cancelable:!0}))}didOpen(){this.$previousActiveElement=Or(document.activeElement),requestAnimationFrame((()=>{this.$dialog.focusFirstElement()})),this.tabIndex=0,this.$scrollContainer.style.overflow="hidden",this.addEventListener("keydown",this.onKeyDown,{capture:!0,passive:!0}),jr(this.$scrollContainer,zr(this.$scrollContainer)+1),this.dispatchEvent(new CustomEvent("open"))}didClose(){this.removeEventListener("keydown",this.onKeyDown,{capture:!0}),jr(this.$scrollContainer,Math.max(0,zr(this.$scrollContainer)-1)),zr(this.$scrollContainer)<=0&&(this.$scrollContainer.style.overflow=""),this.tabIndex=-1,null!=this.$previousActiveElement&&(this.$previousActiveElement.focus(),this.$previousActiveElement=null),this.dispatchEvent(new CustomEvent("close",{detail:this.result}))}attributeChangedCallback(e,t,n){if("open"===e)this.open?this.didOpen():this.didClose()}}customElements.define("web-dialog",Rr);var Dr,Pr,Lr="5.2.0",Nr="Fri, 23 Jun 2023 15:26:26 GMT",Ur="Thu, 22 Jun 2023 09:21:26 -0600",Fr="5.2.0-HEAD+g69249b6";!function(e){e.assertEqual=e=>e,e.assertIs=function(e){},e.assertNever=function(e){throw new Error},e.arrayToEnum=e=>{const t={};for(const n of e)t[n]=n;return t},e.getValidEnumValues=t=>{const n=e.objectKeys(t).filter((e=>"number"!=typeof t[t[e]])),i={};for(const e of n)i[e]=t[e];return e.objectValues(i)},e.objectValues=t=>e.objectKeys(t).map((function(e){return t[e]})),e.objectKeys="function"==typeof Object.keys?e=>Object.keys(e):e=>{const t=[];for(const n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},e.find=(e,t)=>{for(const n of e)if(t(n))return n},e.isInteger="function"==typeof Number.isInteger?e=>Number.isInteger(e):e=>"number"==typeof e&&isFinite(e)&&Math.floor(e)===e,e.joinValues=function(e,t=" | "){return e.map((e=>"string"==typeof e?`'${e}'`:e)).join(t)},e.jsonStringifyReplacer=(e,t)=>"bigint"==typeof t?t.toString():t}(Dr||(Dr={})),function(e){e.mergeShapes=(e,t)=>({...e,...t})}(Pr||(Pr={}));const Hr=Dr.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),Zr=e=>{switch(typeof e){case"undefined":return Hr.undefined;case"string":return Hr.string;case"number":return isNaN(e)?Hr.nan:Hr.number;case"boolean":return Hr.boolean;case"function":return Hr.function;case"bigint":return Hr.bigint;case"symbol":return Hr.symbol;case"object":return Array.isArray(e)?Hr.array:null===e?Hr.null:e.then&&"function"==typeof e.then&&e.catch&&"function"==typeof e.catch?Hr.promise:"undefined"!=typeof Map&&e instanceof Map?Hr.map:"undefined"!=typeof Set&&e instanceof Set?Hr.set:"undefined"!=typeof Date&&e instanceof Date?Hr.date:Hr.object;default:return Hr.unknown}},qr=Dr.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]);class Vr extends Error{constructor(e){super(),this.issues=[],this.addIssue=e=>{this.issues=[...this.issues,e]},this.addIssues=(e=[])=>{this.issues=[...this.issues,...e]};const t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}get errors(){return this.issues}format(e){const t=e||function(e){return e.message},n={_errors:[]},i=e=>{for(const a of e.issues)if("invalid_union"===a.code)a.unionErrors.map(i);else if("invalid_return_type"===a.code)i(a.returnTypeError);else if("invalid_arguments"===a.code)i(a.argumentsError);else if(0===a.path.length)n._errors.push(t(a));else{let e=n,i=0;for(;ie.message)){const t={},n=[];for(const i of this.issues)i.path.length>0?(t[i.path[0]]=t[i.path[0]]||[],t[i.path[0]].push(e(i))):n.push(e(i));return{formErrors:n,fieldErrors:t}}get formErrors(){return this.flatten()}}Vr.create=e=>new Vr(e);const Wr=(e,t)=>{let n;switch(e.code){case qr.invalid_type:n=e.received===Hr.undefined?"Required":`Expected ${e.expected}, received ${e.received}`;break;case qr.invalid_literal:n=`Invalid literal value, expected ${JSON.stringify(e.expected,Dr.jsonStringifyReplacer)}`;break;case qr.unrecognized_keys:n=`Unrecognized key(s) in object: ${Dr.joinValues(e.keys,", ")}`;break;case qr.invalid_union:n="Invalid input";break;case qr.invalid_union_discriminator:n=`Invalid discriminator value. Expected ${Dr.joinValues(e.options)}`;break;case qr.invalid_enum_value:n=`Invalid enum value. Expected ${Dr.joinValues(e.options)}, received '${e.received}'`;break;case qr.invalid_arguments:n="Invalid function arguments";break;case qr.invalid_return_type:n="Invalid function return type";break;case qr.invalid_date:n="Invalid date";break;case qr.invalid_string:"object"==typeof e.validation?"includes"in e.validation?(n=`Invalid input: must include "${e.validation.includes}"`,"number"==typeof e.validation.position&&(n=`${n} at one or more positions greater than or equal to ${e.validation.position}`)):"startsWith"in e.validation?n=`Invalid input: must start with "${e.validation.startsWith}"`:"endsWith"in e.validation?n=`Invalid input: must end with "${e.validation.endsWith}"`:Dr.assertNever(e.validation):n="regex"!==e.validation?`Invalid ${e.validation}`:"Invalid";break;case qr.too_small:n="array"===e.type?`Array must contain ${e.exact?"exactly":e.inclusive?"at least":"more than"} ${e.minimum} element(s)`:"string"===e.type?`String must contain ${e.exact?"exactly":e.inclusive?"at least":"over"} ${e.minimum} character(s)`:"number"===e.type?`Number must be ${e.exact?"exactly equal to ":e.inclusive?"greater than or equal to ":"greater than "}${e.minimum}`:"date"===e.type?`Date must be ${e.exact?"exactly equal to ":e.inclusive?"greater than or equal to ":"greater than "}${new Date(Number(e.minimum))}`:"Invalid input";break;case qr.too_big:n="array"===e.type?`Array must contain ${e.exact?"exactly":e.inclusive?"at most":"less than"} ${e.maximum} element(s)`:"string"===e.type?`String must contain ${e.exact?"exactly":e.inclusive?"at most":"under"} ${e.maximum} character(s)`:"number"===e.type?`Number must be ${e.exact?"exactly":e.inclusive?"less than or equal to":"less than"} ${e.maximum}`:"bigint"===e.type?`BigInt must be ${e.exact?"exactly":e.inclusive?"less than or equal to":"less than"} ${e.maximum}`:"date"===e.type?`Date must be ${e.exact?"exactly":e.inclusive?"smaller than or equal to":"smaller than"} ${new Date(Number(e.maximum))}`:"Invalid input";break;case qr.custom:n="Invalid input";break;case qr.invalid_intersection_types:n="Intersection results could not be merged";break;case qr.not_multiple_of:n=`Number must be a multiple of ${e.multipleOf}`;break;case qr.not_finite:n="Number must be finite";break;default:n=t.defaultError,Dr.assertNever(e)}return{message:n}};let Br=Wr;function Yr(){return Br}const Qr=e=>{const{data:t,path:n,errorMaps:i,issueData:a}=e,r=[...n,...a.path||[]],o={...a,path:r};let s="";const c=i.filter((e=>!!e)).slice().reverse();for(const e of c)s=e(o,{data:t,defaultError:s}).message;return{...a,path:r,message:a.message||s}};function Gr(e,t){const n=Qr({issueData:t,data:e.data,path:e.path,errorMaps:[e.common.contextualErrorMap,e.schemaErrorMap,Yr(),Wr].filter((e=>!!e))});e.common.issues.push(n)}class Kr{constructor(){this.value="valid"}dirty(){"valid"===this.value&&(this.value="dirty")}abort(){"aborted"!==this.value&&(this.value="aborted")}static mergeArray(e,t){const n=[];for(const i of t){if("aborted"===i.status)return Xr;"dirty"===i.status&&e.dirty(),n.push(i.value)}return{status:e.value,value:n}}static async mergeObjectAsync(e,t){const n=[];for(const e of t)n.push({key:await e.key,value:await e.value});return Kr.mergeObjectSync(e,n)}static mergeObjectSync(e,t){const n={};for(const i of t){const{key:t,value:a}=i;if("aborted"===t.status)return Xr;if("aborted"===a.status)return Xr;"dirty"===t.status&&e.dirty(),"dirty"===a.status&&e.dirty(),(void 0!==a.value||i.alwaysSet)&&(n[t.value]=a.value)}return{status:e.value,value:n}}}const Xr=Object.freeze({status:"aborted"}),Jr=e=>({status:"dirty",value:e}),eo=e=>({status:"valid",value:e}),to=e=>"aborted"===e.status,no=e=>"dirty"===e.status,io=e=>"valid"===e.status,ao=e=>"undefined"!=typeof Promise&&e instanceof Promise;var ro;!function(e){e.errToObj=e=>"string"==typeof e?{message:e}:e||{},e.toString=e=>"string"==typeof e?e:null==e?void 0:e.message}(ro||(ro={}));class oo{constructor(e,t,n,i){this._cachedPath=[],this.parent=e,this.data=t,this._path=n,this._key=i}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}}const so=(e,t)=>{if(io(t))return{success:!0,data:t.value};if(!e.common.issues.length)throw new Error("Validation failed but no issues detected.");return{success:!1,get error(){if(this._error)return this._error;const t=new Vr(e.common.issues);return this._error=t,this._error}}};function co(e){if(!e)return{};const{errorMap:t,invalid_type_error:n,required_error:i,description:a}=e;if(t&&(n||i))throw new Error('Can\'t use "invalid_type_error" or "required_error" in conjunction with custom error map.');if(t)return{errorMap:t,description:a};return{errorMap:(e,t)=>"invalid_type"!==e.code?{message:t.defaultError}:void 0===t.data?{message:null!=i?i:t.defaultError}:{message:null!=n?n:t.defaultError},description:a}}class lo{constructor(e){this.spa=this.safeParseAsync,this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this)}get description(){return this._def.description}_getType(e){return Zr(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:Zr(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new Kr,ctx:{common:e.parent.common,data:e.data,parsedType:Zr(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){const t=this._parse(e);if(ao(t))throw new Error("Synchronous parse encountered promise.");return t}_parseAsync(e){const t=this._parse(e);return Promise.resolve(t)}parse(e,t){const n=this.safeParse(e,t);if(n.success)return n.data;throw n.error}safeParse(e,t){var n;const i={common:{issues:[],async:null!==(n=null==t?void 0:t.async)&&void 0!==n&&n,contextualErrorMap:null==t?void 0:t.errorMap},path:(null==t?void 0:t.path)||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Zr(e)},a=this._parseSync({data:e,path:i.path,parent:i});return so(i,a)}async parseAsync(e,t){const n=await this.safeParseAsync(e,t);if(n.success)return n.data;throw n.error}async safeParseAsync(e,t){const n={common:{issues:[],contextualErrorMap:null==t?void 0:t.errorMap,async:!0},path:(null==t?void 0:t.path)||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Zr(e)},i=this._parse({data:e,path:n.path,parent:n}),a=await(ao(i)?i:Promise.resolve(i));return so(n,a)}refine(e,t){const n=e=>"string"==typeof t||void 0===t?{message:t}:"function"==typeof t?t(e):t;return this._refinement(((t,i)=>{const a=e(t),r=()=>i.addIssue({code:qr.custom,...n(t)});return"undefined"!=typeof Promise&&a instanceof Promise?a.then((e=>!!e||(r(),!1))):!!a||(r(),!1)}))}refinement(e,t){return this._refinement(((n,i)=>!!e(n)||(i.addIssue("function"==typeof t?t(n,i):t),!1)))}_refinement(e){return new Xo({schema:this,typeName:ls.ZodEffects,effect:{type:"refinement",refinement:e}})}superRefine(e){return this._refinement(e)}optional(){return Jo.create(this,this._def)}nullable(){return es.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return Oo.create(this,this._def)}promise(){return Ko.create(this,this._def)}or(e){return Do.create([this,e],this._def)}and(e){return Uo.create(this,e,this._def)}transform(e){return new Xo({...co(this._def),schema:this,typeName:ls.ZodEffects,effect:{type:"transform",transform:e}})}default(e){const t="function"==typeof e?e:()=>e;return new ts({...co(this._def),innerType:this,defaultValue:t,typeName:ls.ZodDefault})}brand(){return new rs({typeName:ls.ZodBranded,type:this,...co(this._def)})}catch(e){const t="function"==typeof e?e:()=>e;return new ns({...co(this._def),innerType:this,catchValue:t,typeName:ls.ZodCatch})}describe(e){return new(0,this.constructor)({...this._def,description:e})}pipe(e){return os.create(this,e)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}}const uo=/^c[^\s-]{8,}$/i,ho=/^[a-z][a-z0-9]*$/,mo=/[0-9A-HJKMNP-TV-Z]{26}/,po=/^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i,fo=/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/,go=/^(\p{Extended_Pictographic}|\p{Emoji_Component})+$/u,vo=/^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/,_o=/^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/;function yo(e,t){return!("v4"!==t&&t||!vo.test(e))||!("v6"!==t&&t||!_o.test(e))}class bo extends lo{constructor(){super(...arguments),this._regex=(e,t,n)=>this.refinement((t=>e.test(t)),{validation:t,code:qr.invalid_string,...ro.errToObj(n)}),this.nonempty=e=>this.min(1,ro.errToObj(e)),this.trim=()=>new bo({...this._def,checks:[...this._def.checks,{kind:"trim"}]}),this.toLowerCase=()=>new bo({...this._def,checks:[...this._def.checks,{kind:"toLowerCase"}]}),this.toUpperCase=()=>new bo({...this._def,checks:[...this._def.checks,{kind:"toUpperCase"}]})}_parse(e){this._def.coerce&&(e.data=String(e.data));if(this._getType(e)!==Hr.string){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.string,received:t.parsedType}),Xr}const t=new Kr;let n;for(const a of this._def.checks)if("min"===a.kind)e.data.lengtha.value&&(n=this._getOrReturnCtx(e,n),Gr(n,{code:qr.too_big,maximum:a.value,type:"string",inclusive:!0,exact:!1,message:a.message}),t.dirty());else if("length"===a.kind){const i=e.data.length>a.value,r=e.data.length"datetime"===e.kind))}get isEmail(){return!!this._def.checks.find((e=>"email"===e.kind))}get isURL(){return!!this._def.checks.find((e=>"url"===e.kind))}get isEmoji(){return!!this._def.checks.find((e=>"emoji"===e.kind))}get isUUID(){return!!this._def.checks.find((e=>"uuid"===e.kind))}get isCUID(){return!!this._def.checks.find((e=>"cuid"===e.kind))}get isCUID2(){return!!this._def.checks.find((e=>"cuid2"===e.kind))}get isULID(){return!!this._def.checks.find((e=>"ulid"===e.kind))}get isIP(){return!!this._def.checks.find((e=>"ip"===e.kind))}get minLength(){let e=null;for(const t of this._def.checks)"min"===t.kind&&(null===e||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(const t of this._def.checks)"max"===t.kind&&(null===e||t.valuei?n:i;return parseInt(e.toFixed(a).replace(".",""))%parseInt(t.toFixed(a).replace(".",""))/Math.pow(10,a)}bo.create=e=>{var t;return new bo({checks:[],typeName:ls.ZodString,coerce:null!==(t=null==e?void 0:e.coerce)&&void 0!==t&&t,...co(e)})};class xo extends lo{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(e){this._def.coerce&&(e.data=Number(e.data));if(this._getType(e)!==Hr.number){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.number,received:t.parsedType}),Xr}let t;const n=new Kr;for(const i of this._def.checks)if("int"===i.kind)Dr.isInteger(e.data)||(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.invalid_type,expected:"integer",received:"float",message:i.message}),n.dirty());else if("min"===i.kind){(i.inclusive?e.datai.value:e.data>=i.value)&&(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.too_big,maximum:i.value,type:"number",inclusive:i.inclusive,exact:!1,message:i.message}),n.dirty())}else"multipleOf"===i.kind?0!==wo(e.data,i.value)&&(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.not_multiple_of,multipleOf:i.value,message:i.message}),n.dirty()):"finite"===i.kind?Number.isFinite(e.data)||(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.not_finite,message:i.message}),n.dirty()):Dr.assertNever(i);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit("min",e,!0,ro.toString(t))}gt(e,t){return this.setLimit("min",e,!1,ro.toString(t))}lte(e,t){return this.setLimit("max",e,!0,ro.toString(t))}lt(e,t){return this.setLimit("max",e,!1,ro.toString(t))}setLimit(e,t,n,i){return new xo({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:ro.toString(i)}]})}_addCheck(e){return new xo({...this._def,checks:[...this._def.checks,e]})}int(e){return this._addCheck({kind:"int",message:ro.toString(e)})}positive(e){return this._addCheck({kind:"min",value:0,inclusive:!1,message:ro.toString(e)})}negative(e){return this._addCheck({kind:"max",value:0,inclusive:!1,message:ro.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:0,inclusive:!0,message:ro.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:0,inclusive:!0,message:ro.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:ro.toString(t)})}finite(e){return this._addCheck({kind:"finite",message:ro.toString(e)})}safe(e){return this._addCheck({kind:"min",inclusive:!0,value:Number.MIN_SAFE_INTEGER,message:ro.toString(e)})._addCheck({kind:"max",inclusive:!0,value:Number.MAX_SAFE_INTEGER,message:ro.toString(e)})}get minValue(){let e=null;for(const t of this._def.checks)"min"===t.kind&&(null===e||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(const t of this._def.checks)"max"===t.kind&&(null===e||t.value"int"===e.kind||"multipleOf"===e.kind&&Dr.isInteger(e.value)))}get isFinite(){let e=null,t=null;for(const n of this._def.checks){if("finite"===n.kind||"int"===n.kind||"multipleOf"===n.kind)return!0;"min"===n.kind?(null===t||n.value>t)&&(t=n.value):"max"===n.kind&&(null===e||n.valuenew xo({checks:[],typeName:ls.ZodNumber,coerce:(null==e?void 0:e.coerce)||!1,...co(e)});class Co extends lo{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(e){this._def.coerce&&(e.data=BigInt(e.data));if(this._getType(e)!==Hr.bigint){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.bigint,received:t.parsedType}),Xr}let t;const n=new Kr;for(const i of this._def.checks)if("min"===i.kind){(i.inclusive?e.datai.value:e.data>=i.value)&&(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.too_big,type:"bigint",maximum:i.value,inclusive:i.inclusive,message:i.message}),n.dirty())}else"multipleOf"===i.kind?e.data%i.value!==BigInt(0)&&(t=this._getOrReturnCtx(e,t),Gr(t,{code:qr.not_multiple_of,multipleOf:i.value,message:i.message}),n.dirty()):Dr.assertNever(i);return{status:n.value,value:e.data}}gte(e,t){return this.setLimit("min",e,!0,ro.toString(t))}gt(e,t){return this.setLimit("min",e,!1,ro.toString(t))}lte(e,t){return this.setLimit("max",e,!0,ro.toString(t))}lt(e,t){return this.setLimit("max",e,!1,ro.toString(t))}setLimit(e,t,n,i){return new Co({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:ro.toString(i)}]})}_addCheck(e){return new Co({...this._def,checks:[...this._def.checks,e]})}positive(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!1,message:ro.toString(e)})}negative(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!1,message:ro.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!0,message:ro.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!0,message:ro.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:ro.toString(t)})}get minValue(){let e=null;for(const t of this._def.checks)"min"===t.kind&&(null===e||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(const t of this._def.checks)"max"===t.kind&&(null===e||t.value{var t;return new Co({checks:[],typeName:ls.ZodBigInt,coerce:null!==(t=null==e?void 0:e.coerce)&&void 0!==t&&t,...co(e)})};class $o extends lo{_parse(e){this._def.coerce&&(e.data=Boolean(e.data));if(this._getType(e)!==Hr.boolean){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.boolean,received:t.parsedType}),Xr}return eo(e.data)}}$o.create=e=>new $o({typeName:ls.ZodBoolean,coerce:(null==e?void 0:e.coerce)||!1,...co(e)});class ko extends lo{_parse(e){this._def.coerce&&(e.data=new Date(e.data));if(this._getType(e)!==Hr.date){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.date,received:t.parsedType}),Xr}if(isNaN(e.data.getTime())){return Gr(this._getOrReturnCtx(e),{code:qr.invalid_date}),Xr}const t=new Kr;let n;for(const i of this._def.checks)"min"===i.kind?e.data.getTime()i.value&&(n=this._getOrReturnCtx(e,n),Gr(n,{code:qr.too_big,message:i.message,inclusive:!0,exact:!1,maximum:i.value,type:"date"}),t.dirty()):Dr.assertNever(i);return{status:t.value,value:new Date(e.data.getTime())}}_addCheck(e){return new ko({...this._def,checks:[...this._def.checks,e]})}min(e,t){return this._addCheck({kind:"min",value:e.getTime(),message:ro.toString(t)})}max(e,t){return this._addCheck({kind:"max",value:e.getTime(),message:ro.toString(t)})}get minDate(){let e=null;for(const t of this._def.checks)"min"===t.kind&&(null===e||t.value>e)&&(e=t.value);return null!=e?new Date(e):null}get maxDate(){let e=null;for(const t of this._def.checks)"max"===t.kind&&(null===e||t.valuenew ko({checks:[],coerce:(null==e?void 0:e.coerce)||!1,typeName:ls.ZodDate,...co(e)});class Eo extends lo{_parse(e){if(this._getType(e)!==Hr.symbol){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.symbol,received:t.parsedType}),Xr}return eo(e.data)}}Eo.create=e=>new Eo({typeName:ls.ZodSymbol,...co(e)});class Mo extends lo{_parse(e){if(this._getType(e)!==Hr.undefined){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.undefined,received:t.parsedType}),Xr}return eo(e.data)}}Mo.create=e=>new Mo({typeName:ls.ZodUndefined,...co(e)});class So extends lo{_parse(e){if(this._getType(e)!==Hr.null){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.null,received:t.parsedType}),Xr}return eo(e.data)}}So.create=e=>new So({typeName:ls.ZodNull,...co(e)});class To extends lo{constructor(){super(...arguments),this._any=!0}_parse(e){return eo(e.data)}}To.create=e=>new To({typeName:ls.ZodAny,...co(e)});class Ao extends lo{constructor(){super(...arguments),this._unknown=!0}_parse(e){return eo(e.data)}}Ao.create=e=>new Ao({typeName:ls.ZodUnknown,...co(e)});class zo extends lo{_parse(e){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.never,received:t.parsedType}),Xr}}zo.create=e=>new zo({typeName:ls.ZodNever,...co(e)});class jo extends lo{_parse(e){if(this._getType(e)!==Hr.undefined){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.void,received:t.parsedType}),Xr}return eo(e.data)}}jo.create=e=>new jo({typeName:ls.ZodVoid,...co(e)});class Oo extends lo{_parse(e){const{ctx:t,status:n}=this._processInputParams(e),i=this._def;if(t.parsedType!==Hr.array)return Gr(t,{code:qr.invalid_type,expected:Hr.array,received:t.parsedType}),Xr;if(null!==i.exactLength){const e=t.data.length>i.exactLength.value,a=t.data.lengthi.maxLength.value&&(Gr(t,{code:qr.too_big,maximum:i.maxLength.value,type:"array",inclusive:!0,exact:!1,message:i.maxLength.message}),n.dirty()),t.common.async)return Promise.all([...t.data].map(((e,n)=>i.type._parseAsync(new oo(t,e,t.path,n))))).then((e=>Kr.mergeArray(n,e)));const a=[...t.data].map(((e,n)=>i.type._parseSync(new oo(t,e,t.path,n))));return Kr.mergeArray(n,a)}get element(){return this._def.type}min(e,t){return new Oo({...this._def,minLength:{value:e,message:ro.toString(t)}})}max(e,t){return new Oo({...this._def,maxLength:{value:e,message:ro.toString(t)}})}length(e,t){return new Oo({...this._def,exactLength:{value:e,message:ro.toString(t)}})}nonempty(e){return this.min(1,e)}}function Io(e){if(e instanceof Ro){const t={};for(const n in e.shape){const i=e.shape[n];t[n]=Jo.create(Io(i))}return new Ro({...e._def,shape:()=>t})}return e instanceof Oo?new Oo({...e._def,type:Io(e.element)}):e instanceof Jo?Jo.create(Io(e.unwrap())):e instanceof es?es.create(Io(e.unwrap())):e instanceof Fo?Fo.create(e.items.map((e=>Io(e)))):e}Oo.create=(e,t)=>new Oo({type:e,minLength:null,maxLength:null,exactLength:null,typeName:ls.ZodArray,...co(t)});class Ro extends lo{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(null!==this._cached)return this._cached;const e=this._def.shape(),t=Dr.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==Hr.object){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.object,received:t.parsedType}),Xr}const{status:t,ctx:n}=this._processInputParams(e),{shape:i,keys:a}=this._getCached(),r=[];if(!(this._def.catchall instanceof zo&&"strip"===this._def.unknownKeys))for(const e in n.data)a.includes(e)||r.push(e);const o=[];for(const e of a){const t=i[e],a=n.data[e];o.push({key:{status:"valid",value:e},value:t._parse(new oo(n,a,n.path,e)),alwaysSet:e in n.data})}if(this._def.catchall instanceof zo){const e=this._def.unknownKeys;if("passthrough"===e)for(const e of r)o.push({key:{status:"valid",value:e},value:{status:"valid",value:n.data[e]}});else if("strict"===e)r.length>0&&(Gr(n,{code:qr.unrecognized_keys,keys:r}),t.dirty());else if("strip"!==e)throw new Error("Internal ZodObject error: invalid unknownKeys value.")}else{const e=this._def.catchall;for(const t of r){const i=n.data[t];o.push({key:{status:"valid",value:t},value:e._parse(new oo(n,i,n.path,t)),alwaysSet:t in n.data})}}return n.common.async?Promise.resolve().then((async()=>{const e=[];for(const t of o){const n=await t.key;e.push({key:n,value:await t.value,alwaysSet:t.alwaysSet})}return e})).then((e=>Kr.mergeObjectSync(t,e))):Kr.mergeObjectSync(t,o)}get shape(){return this._def.shape()}strict(e){return ro.errToObj,new Ro({...this._def,unknownKeys:"strict",...void 0!==e?{errorMap:(t,n)=>{var i,a,r,o;const s=null!==(r=null===(a=(i=this._def).errorMap)||void 0===a?void 0:a.call(i,t,n).message)&&void 0!==r?r:n.defaultError;return"unrecognized_keys"===t.code?{message:null!==(o=ro.errToObj(e).message)&&void 0!==o?o:s}:{message:s}}}:{}})}strip(){return new Ro({...this._def,unknownKeys:"strip"})}passthrough(){return new Ro({...this._def,unknownKeys:"passthrough"})}extend(e){return new Ro({...this._def,shape:()=>({...this._def.shape(),...e})})}merge(e){return new Ro({unknownKeys:e._def.unknownKeys,catchall:e._def.catchall,shape:()=>({...this._def.shape(),...e._def.shape()}),typeName:ls.ZodObject})}setKey(e,t){return this.augment({[e]:t})}catchall(e){return new Ro({...this._def,catchall:e})}pick(e){const t={};return Dr.objectKeys(e).forEach((n=>{e[n]&&this.shape[n]&&(t[n]=this.shape[n])})),new Ro({...this._def,shape:()=>t})}omit(e){const t={};return Dr.objectKeys(this.shape).forEach((n=>{e[n]||(t[n]=this.shape[n])})),new Ro({...this._def,shape:()=>t})}deepPartial(){return Io(this)}partial(e){const t={};return Dr.objectKeys(this.shape).forEach((n=>{const i=this.shape[n];e&&!e[n]?t[n]=i:t[n]=i.optional()})),new Ro({...this._def,shape:()=>t})}required(e){const t={};return Dr.objectKeys(this.shape).forEach((n=>{if(e&&!e[n])t[n]=this.shape[n];else{let e=this.shape[n];for(;e instanceof Jo;)e=e._def.innerType;t[n]=e}})),new Ro({...this._def,shape:()=>t})}keyof(){return Yo(Dr.objectKeys(this.shape))}}Ro.create=(e,t)=>new Ro({shape:()=>e,unknownKeys:"strip",catchall:zo.create(),typeName:ls.ZodObject,...co(t)}),Ro.strictCreate=(e,t)=>new Ro({shape:()=>e,unknownKeys:"strict",catchall:zo.create(),typeName:ls.ZodObject,...co(t)}),Ro.lazycreate=(e,t)=>new Ro({shape:e,unknownKeys:"strip",catchall:zo.create(),typeName:ls.ZodObject,...co(t)});class Do extends lo{_parse(e){const{ctx:t}=this._processInputParams(e),n=this._def.options;if(t.common.async)return Promise.all(n.map((async e=>{const n={...t,common:{...t.common,issues:[]},parent:null};return{result:await e._parseAsync({data:t.data,path:t.path,parent:n}),ctx:n}}))).then((function(e){for(const t of e)if("valid"===t.result.status)return t.result;for(const n of e)if("dirty"===n.result.status)return t.common.issues.push(...n.ctx.common.issues),n.result;const n=e.map((e=>new Vr(e.ctx.common.issues)));return Gr(t,{code:qr.invalid_union,unionErrors:n}),Xr}));{let e;const i=[];for(const a of n){const n={...t,common:{...t.common,issues:[]},parent:null},r=a._parseSync({data:t.data,path:t.path,parent:n});if("valid"===r.status)return r;"dirty"!==r.status||e||(e={result:r,ctx:n}),n.common.issues.length&&i.push(n.common.issues)}if(e)return t.common.issues.push(...e.ctx.common.issues),e.result;const a=i.map((e=>new Vr(e)));return Gr(t,{code:qr.invalid_union,unionErrors:a}),Xr}}get options(){return this._def.options}}Do.create=(e,t)=>new Do({options:e,typeName:ls.ZodUnion,...co(t)});const Po=e=>e instanceof Wo?Po(e.schema):e instanceof Xo?Po(e.innerType()):e instanceof Bo?[e.value]:e instanceof Qo?e.options:e instanceof Go?Object.keys(e.enum):e instanceof ts?Po(e._def.innerType):e instanceof Mo?[void 0]:e instanceof So?[null]:null;class Lo extends lo{_parse(e){const{ctx:t}=this._processInputParams(e);if(t.parsedType!==Hr.object)return Gr(t,{code:qr.invalid_type,expected:Hr.object,received:t.parsedType}),Xr;const n=this.discriminator,i=t.data[n],a=this.optionsMap.get(i);return a?t.common.async?a._parseAsync({data:t.data,path:t.path,parent:t}):a._parseSync({data:t.data,path:t.path,parent:t}):(Gr(t,{code:qr.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[n]}),Xr)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(e,t,n){const i=new Map;for(const n of t){const t=Po(n.shape[e]);if(!t)throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`);for(const a of t){if(i.has(a))throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(a)}`);i.set(a,n)}}return new Lo({typeName:ls.ZodDiscriminatedUnion,discriminator:e,options:t,optionsMap:i,...co(n)})}}function No(e,t){const n=Zr(e),i=Zr(t);if(e===t)return{valid:!0,data:e};if(n===Hr.object&&i===Hr.object){const n=Dr.objectKeys(t),i=Dr.objectKeys(e).filter((e=>-1!==n.indexOf(e))),a={...e,...t};for(const n of i){const i=No(e[n],t[n]);if(!i.valid)return{valid:!1};a[n]=i.data}return{valid:!0,data:a}}if(n===Hr.array&&i===Hr.array){if(e.length!==t.length)return{valid:!1};const n=[];for(let i=0;i{if(to(e)||to(i))return Xr;const a=No(e.value,i.value);return a.valid?((no(e)||no(i))&&t.dirty(),{status:t.value,value:a.data}):(Gr(n,{code:qr.invalid_intersection_types}),Xr)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then((([e,t])=>i(e,t))):i(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}}Uo.create=(e,t,n)=>new Uo({left:e,right:t,typeName:ls.ZodIntersection,...co(n)});class Fo extends lo{_parse(e){const{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==Hr.array)return Gr(n,{code:qr.invalid_type,expected:Hr.array,received:n.parsedType}),Xr;if(n.data.lengththis._def.items.length&&(Gr(n,{code:qr.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:"array"}),t.dirty());const i=[...n.data].map(((e,t)=>{const i=this._def.items[t]||this._def.rest;return i?i._parse(new oo(n,e,n.path,t)):null})).filter((e=>!!e));return n.common.async?Promise.all(i).then((e=>Kr.mergeArray(t,e))):Kr.mergeArray(t,i)}get items(){return this._def.items}rest(e){return new Fo({...this._def,rest:e})}}Fo.create=(e,t)=>{if(!Array.isArray(e))throw new Error("You must pass an array of schemas to z.tuple([ ... ])");return new Fo({items:e,typeName:ls.ZodTuple,rest:null,...co(t)})};class Ho extends lo{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){const{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==Hr.object)return Gr(n,{code:qr.invalid_type,expected:Hr.object,received:n.parsedType}),Xr;const i=[],a=this._def.keyType,r=this._def.valueType;for(const e in n.data)i.push({key:a._parse(new oo(n,e,n.path,e)),value:r._parse(new oo(n,n.data[e],n.path,e))});return n.common.async?Kr.mergeObjectAsync(t,i):Kr.mergeObjectSync(t,i)}get element(){return this._def.valueType}static create(e,t,n){return new Ho(t instanceof lo?{keyType:e,valueType:t,typeName:ls.ZodRecord,...co(n)}:{keyType:bo.create(),valueType:e,typeName:ls.ZodRecord,...co(t)})}}class Zo extends lo{_parse(e){const{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==Hr.map)return Gr(n,{code:qr.invalid_type,expected:Hr.map,received:n.parsedType}),Xr;const i=this._def.keyType,a=this._def.valueType,r=[...n.data.entries()].map((([e,t],r)=>({key:i._parse(new oo(n,e,n.path,[r,"key"])),value:a._parse(new oo(n,t,n.path,[r,"value"]))})));if(n.common.async){const e=new Map;return Promise.resolve().then((async()=>{for(const n of r){const i=await n.key,a=await n.value;if("aborted"===i.status||"aborted"===a.status)return Xr;"dirty"!==i.status&&"dirty"!==a.status||t.dirty(),e.set(i.value,a.value)}return{status:t.value,value:e}}))}{const e=new Map;for(const n of r){const i=n.key,a=n.value;if("aborted"===i.status||"aborted"===a.status)return Xr;"dirty"!==i.status&&"dirty"!==a.status||t.dirty(),e.set(i.value,a.value)}return{status:t.value,value:e}}}}Zo.create=(e,t,n)=>new Zo({valueType:t,keyType:e,typeName:ls.ZodMap,...co(n)});class qo extends lo{_parse(e){const{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==Hr.set)return Gr(n,{code:qr.invalid_type,expected:Hr.set,received:n.parsedType}),Xr;const i=this._def;null!==i.minSize&&n.data.sizei.maxSize.value&&(Gr(n,{code:qr.too_big,maximum:i.maxSize.value,type:"set",inclusive:!0,exact:!1,message:i.maxSize.message}),t.dirty());const a=this._def.valueType;function r(e){const n=new Set;for(const i of e){if("aborted"===i.status)return Xr;"dirty"===i.status&&t.dirty(),n.add(i.value)}return{status:t.value,value:n}}const o=[...n.data.values()].map(((e,t)=>a._parse(new oo(n,e,n.path,t))));return n.common.async?Promise.all(o).then((e=>r(e))):r(o)}min(e,t){return new qo({...this._def,minSize:{value:e,message:ro.toString(t)}})}max(e,t){return new qo({...this._def,maxSize:{value:e,message:ro.toString(t)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}}qo.create=(e,t)=>new qo({valueType:e,minSize:null,maxSize:null,typeName:ls.ZodSet,...co(t)});class Vo extends lo{constructor(){super(...arguments),this.validate=this.implement}_parse(e){const{ctx:t}=this._processInputParams(e);if(t.parsedType!==Hr.function)return Gr(t,{code:qr.invalid_type,expected:Hr.function,received:t.parsedType}),Xr;function n(e,n){return Qr({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,Yr(),Wr].filter((e=>!!e)),issueData:{code:qr.invalid_arguments,argumentsError:n}})}function i(e,n){return Qr({data:e,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,Yr(),Wr].filter((e=>!!e)),issueData:{code:qr.invalid_return_type,returnTypeError:n}})}const a={errorMap:t.common.contextualErrorMap},r=t.data;return this._def.returns instanceof Ko?eo((async(...e)=>{const t=new Vr([]),o=await this._def.args.parseAsync(e,a).catch((i=>{throw t.addIssue(n(e,i)),t})),s=await r(...o),c=await this._def.returns._def.type.parseAsync(s,a).catch((e=>{throw t.addIssue(i(s,e)),t}));return c})):eo(((...e)=>{const t=this._def.args.safeParse(e,a);if(!t.success)throw new Vr([n(e,t.error)]);const o=r(...t.data),s=this._def.returns.safeParse(o,a);if(!s.success)throw new Vr([i(o,s.error)]);return s.data}))}parameters(){return this._def.args}returnType(){return this._def.returns}args(...e){return new Vo({...this._def,args:Fo.create(e).rest(Ao.create())})}returns(e){return new Vo({...this._def,returns:e})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}static create(e,t,n){return new Vo({args:e||Fo.create([]).rest(Ao.create()),returns:t||Ao.create(),typeName:ls.ZodFunction,...co(n)})}}class Wo extends lo{get schema(){return this._def.getter()}_parse(e){const{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}}Wo.create=(e,t)=>new Wo({getter:e,typeName:ls.ZodLazy,...co(t)});class Bo extends lo{_parse(e){if(e.data!==this._def.value){const t=this._getOrReturnCtx(e);return Gr(t,{received:t.data,code:qr.invalid_literal,expected:this._def.value}),Xr}return{status:"valid",value:e.data}}get value(){return this._def.value}}function Yo(e,t){return new Qo({values:e,typeName:ls.ZodEnum,...co(t)})}Bo.create=(e,t)=>new Bo({value:e,typeName:ls.ZodLiteral,...co(t)});class Qo extends lo{_parse(e){if("string"!=typeof e.data){const t=this._getOrReturnCtx(e),n=this._def.values;return Gr(t,{expected:Dr.joinValues(n),received:t.parsedType,code:qr.invalid_type}),Xr}if(-1===this._def.values.indexOf(e.data)){const t=this._getOrReturnCtx(e),n=this._def.values;return Gr(t,{received:t.data,code:qr.invalid_enum_value,options:n}),Xr}return eo(e.data)}get options(){return this._def.values}get enum(){const e={};for(const t of this._def.values)e[t]=t;return e}get Values(){const e={};for(const t of this._def.values)e[t]=t;return e}get Enum(){const e={};for(const t of this._def.values)e[t]=t;return e}extract(e){return Qo.create(e)}exclude(e){return Qo.create(this.options.filter((t=>!e.includes(t))))}}Qo.create=Yo;class Go extends lo{_parse(e){const t=Dr.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(e);if(n.parsedType!==Hr.string&&n.parsedType!==Hr.number){const e=Dr.objectValues(t);return Gr(n,{expected:Dr.joinValues(e),received:n.parsedType,code:qr.invalid_type}),Xr}if(-1===t.indexOf(e.data)){const e=Dr.objectValues(t);return Gr(n,{received:n.data,code:qr.invalid_enum_value,options:e}),Xr}return eo(e.data)}get enum(){return this._def.values}}Go.create=(e,t)=>new Go({values:e,typeName:ls.ZodNativeEnum,...co(t)});class Ko extends lo{unwrap(){return this._def.type}_parse(e){const{ctx:t}=this._processInputParams(e);if(t.parsedType!==Hr.promise&&!1===t.common.async)return Gr(t,{code:qr.invalid_type,expected:Hr.promise,received:t.parsedType}),Xr;const n=t.parsedType===Hr.promise?t.data:Promise.resolve(t.data);return eo(n.then((e=>this._def.type.parseAsync(e,{path:t.path,errorMap:t.common.contextualErrorMap}))))}}Ko.create=(e,t)=>new Ko({type:e,typeName:ls.ZodPromise,...co(t)});class Xo extends lo{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===ls.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(e){const{status:t,ctx:n}=this._processInputParams(e),i=this._def.effect||null;if("preprocess"===i.type){const e=i.transform(n.data);return n.common.async?Promise.resolve(e).then((e=>this._def.schema._parseAsync({data:e,path:n.path,parent:n}))):this._def.schema._parseSync({data:e,path:n.path,parent:n})}const a={addIssue:e=>{Gr(n,e),e.fatal?t.abort():t.dirty()},get path(){return n.path}};if(a.addIssue=a.addIssue.bind(a),"refinement"===i.type){const e=e=>{const t=i.refinement(e,a);if(n.common.async)return Promise.resolve(t);if(t instanceof Promise)throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");return e};if(!1===n.common.async){const i=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return"aborted"===i.status?Xr:("dirty"===i.status&&t.dirty(),e(i.value),{status:t.value,value:i.value})}return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then((n=>"aborted"===n.status?Xr:("dirty"===n.status&&t.dirty(),e(n.value).then((()=>({status:t.value,value:n.value}))))))}if("transform"===i.type){if(!1===n.common.async){const e=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!io(e))return e;const r=i.transform(e.value,a);if(r instanceof Promise)throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.");return{status:t.value,value:r}}return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then((e=>io(e)?Promise.resolve(i.transform(e.value,a)).then((e=>({status:t.value,value:e}))):e))}Dr.assertNever(i)}}Xo.create=(e,t,n)=>new Xo({schema:e,typeName:ls.ZodEffects,effect:t,...co(n)}),Xo.createWithPreprocess=(e,t,n)=>new Xo({schema:t,effect:{type:"preprocess",transform:e},typeName:ls.ZodEffects,...co(n)});class Jo extends lo{_parse(e){return this._getType(e)===Hr.undefined?eo(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}}Jo.create=(e,t)=>new Jo({innerType:e,typeName:ls.ZodOptional,...co(t)});class es extends lo{_parse(e){return this._getType(e)===Hr.null?eo(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}}es.create=(e,t)=>new es({innerType:e,typeName:ls.ZodNullable,...co(t)});class ts extends lo{_parse(e){const{ctx:t}=this._processInputParams(e);let n=t.data;return t.parsedType===Hr.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:t.path,parent:t})}removeDefault(){return this._def.innerType}}ts.create=(e,t)=>new ts({innerType:e,typeName:ls.ZodDefault,defaultValue:"function"==typeof t.default?t.default:()=>t.default,...co(t)});class ns extends lo{_parse(e){const{ctx:t}=this._processInputParams(e),n={...t,common:{...t.common,issues:[]}},i=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return ao(i)?i.then((e=>({status:"valid",value:"valid"===e.status?e.value:this._def.catchValue({get error(){return new Vr(n.common.issues)},input:n.data})}))):{status:"valid",value:"valid"===i.status?i.value:this._def.catchValue({get error(){return new Vr(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}}ns.create=(e,t)=>new ns({innerType:e,typeName:ls.ZodCatch,catchValue:"function"==typeof t.catch?t.catch:()=>t.catch,...co(t)});class is extends lo{_parse(e){if(this._getType(e)!==Hr.nan){const t=this._getOrReturnCtx(e);return Gr(t,{code:qr.invalid_type,expected:Hr.nan,received:t.parsedType}),Xr}return{status:"valid",value:e.data}}}is.create=e=>new is({typeName:ls.ZodNaN,...co(e)});const as=Symbol("zod_brand");class rs extends lo{_parse(e){const{ctx:t}=this._processInputParams(e),n=t.data;return this._def.type._parse({data:n,path:t.path,parent:t})}unwrap(){return this._def.type}}class os extends lo{_parse(e){const{status:t,ctx:n}=this._processInputParams(e);if(n.common.async){return(async()=>{const e=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return"aborted"===e.status?Xr:"dirty"===e.status?(t.dirty(),Jr(e.value)):this._def.out._parseAsync({data:e.value,path:n.path,parent:n})})()}{const e=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return"aborted"===e.status?Xr:"dirty"===e.status?(t.dirty(),{status:"dirty",value:e.value}):this._def.out._parseSync({data:e.value,path:n.path,parent:n})}}static create(e,t){return new os({in:e,out:t,typeName:ls.ZodPipeline})}}const ss=(e,t={},n)=>e?To.create().superRefine(((i,a)=>{var r,o;if(!e(i)){const e="function"==typeof t?t(i):"string"==typeof t?{message:t}:t,s=null===(o=null!==(r=e.fatal)&&void 0!==r?r:n)||void 0===o||o,c="string"==typeof e?{message:e}:e;a.addIssue({code:"custom",...c,fatal:s})}})):To.create(),cs={object:Ro.lazycreate};var ls;!function(e){e.ZodString="ZodString",e.ZodNumber="ZodNumber",e.ZodNaN="ZodNaN",e.ZodBigInt="ZodBigInt",e.ZodBoolean="ZodBoolean",e.ZodDate="ZodDate",e.ZodSymbol="ZodSymbol",e.ZodUndefined="ZodUndefined",e.ZodNull="ZodNull",e.ZodAny="ZodAny",e.ZodUnknown="ZodUnknown",e.ZodNever="ZodNever",e.ZodVoid="ZodVoid",e.ZodArray="ZodArray",e.ZodObject="ZodObject",e.ZodUnion="ZodUnion",e.ZodDiscriminatedUnion="ZodDiscriminatedUnion",e.ZodIntersection="ZodIntersection",e.ZodTuple="ZodTuple",e.ZodRecord="ZodRecord",e.ZodMap="ZodMap",e.ZodSet="ZodSet",e.ZodFunction="ZodFunction",e.ZodLazy="ZodLazy",e.ZodLiteral="ZodLiteral",e.ZodEnum="ZodEnum",e.ZodEffects="ZodEffects",e.ZodNativeEnum="ZodNativeEnum",e.ZodOptional="ZodOptional",e.ZodNullable="ZodNullable",e.ZodDefault="ZodDefault",e.ZodCatch="ZodCatch",e.ZodPromise="ZodPromise",e.ZodBranded="ZodBranded",e.ZodPipeline="ZodPipeline"}(ls||(ls={}));const ds=bo.create,us=xo.create,hs=is.create,ms=Co.create,ps=$o.create,fs=ko.create,gs=Eo.create,vs=Mo.create,_s=So.create,ys=To.create,bs=Ao.create,ws=zo.create,xs=jo.create,Cs=Oo.create,$s=Ro.create,ks=Ro.strictCreate,Es=Do.create,Ms=Lo.create,Ss=Uo.create,Ts=Fo.create,As=Ho.create,zs=Zo.create,js=qo.create,Os=Vo.create,Is=Wo.create,Rs=Bo.create,Ds=Qo.create,Ps=Go.create,Ls=Ko.create,Ns=Xo.create,Us=Jo.create,Fs=es.create,Hs=Xo.createWithPreprocess,Zs=os.create,qs={string:e=>bo.create({...e,coerce:!0}),number:e=>xo.create({...e,coerce:!0}),boolean:e=>$o.create({...e,coerce:!0}),bigint:e=>Co.create({...e,coerce:!0}),date:e=>ko.create({...e,coerce:!0})},Vs=Xr;var Ws=Object.freeze({__proto__:null,defaultErrorMap:Wr,setErrorMap:function(e){Br=e},getErrorMap:Yr,makeIssue:Qr,EMPTY_PATH:[],addIssueToContext:Gr,ParseStatus:Kr,INVALID:Xr,DIRTY:Jr,OK:eo,isAborted:to,isDirty:no,isValid:io,isAsync:ao,get util(){return Dr},get objectUtil(){return Pr},ZodParsedType:Hr,getParsedType:Zr,ZodType:lo,ZodString:bo,ZodNumber:xo,ZodBigInt:Co,ZodBoolean:$o,ZodDate:ko,ZodSymbol:Eo,ZodUndefined:Mo,ZodNull:So,ZodAny:To,ZodUnknown:Ao,ZodNever:zo,ZodVoid:jo,ZodArray:Oo,ZodObject:Ro,ZodUnion:Do,ZodDiscriminatedUnion:Lo,ZodIntersection:Uo,ZodTuple:Fo,ZodRecord:Ho,ZodMap:Zo,ZodSet:qo,ZodFunction:Vo,ZodLazy:Wo,ZodLiteral:Bo,ZodEnum:Qo,ZodNativeEnum:Go,ZodPromise:Ko,ZodEffects:Xo,ZodTransformer:Xo,ZodOptional:Jo,ZodNullable:es,ZodDefault:ts,ZodCatch:ns,ZodNaN:is,BRAND:as,ZodBranded:rs,ZodPipeline:os,custom:ss,Schema:lo,ZodSchema:lo,late:cs,get ZodFirstPartyTypeKind(){return ls},coerce:qs,any:ys,array:Cs,bigint:ms,boolean:ps,date:fs,discriminatedUnion:Ms,effect:Ns,enum:Ds,function:Os,instanceof:(e,t={message:`Input not instance of ${e.name}`})=>ss((t=>t instanceof e),t),intersection:Ss,lazy:Is,literal:Rs,map:zs,nan:hs,nativeEnum:Ps,never:ws,null:_s,nullable:Fs,number:us,object:$s,oboolean:()=>ps().optional(),onumber:()=>us().optional(),optional:Us,ostring:()=>ds().optional(),pipeline:Zs,preprocess:Hs,promise:Ls,record:As,set:js,strictObject:ks,string:ds,symbol:gs,transformer:Ns,tuple:Ts,undefined:vs,union:Es,unknown:bs,void:xs,NEVER:Vs,ZodIssueCode:qr,quotelessJson:e=>JSON.stringify(e,null,2).replace(/"([^"]+)":/g,"$1:"),ZodError:Vr});const Bs="https://github.com/dermotduffy/frigate-hass-card",Ys=`${Bs}#troubleshooting`,Qs="cameras",Gs=`${Qs}.#.camera_entity`,Ks=`${Qs}.#.frigate.camera_name`,Xs=`${Qs}.#.frigate.client_id`,Js=`${Qs}.#.frigate.labels`,ec=`${Qs}.#.frigate.url`,tc=`${Qs}.#.frigate.zones`,nc=`${Qs}.#.go2rtc.modes`,ic=`${Qs}.#.go2rtc.stream`,ac=`${Qs}.#.hide`,rc=`${Qs}.#.icon`,oc=`${Qs}.#.id`,sc=`${Qs}.#.image.refresh_seconds`,cc=`${Qs}.#.image.url`,lc=`${Qs}.#.motioneye.images.directory_pattern`,dc=`${Qs}.#.motioneye.images.file_pattern`,uc=`${Qs}.#.motioneye.movies.directory_pattern`,hc=`${Qs}.#.motioneye.movies.file_pattern`,mc=`${Qs}.#.motioneye.url`,pc=`${Qs}.#.title`,fc=`${Qs}.#.webrtc_card.entity`,gc=`${Qs}.#.webrtc_card.url`,vc=`${Qs}.#.live_provider`,_c=`${Qs}.#.dependencies.cameras`,yc=`${Qs}.#.dependencies.all_cameras`,bc=`${Qs}.#.triggers.motion`,wc=`${Qs}.#.triggers.occupancy`,xc=`${Qs}.#.triggers.entities`,Cc="cameras_global",$c=`${Cc}.image`,kc=`${Cc}.jsmpeg`,Ec=`${Cc}.webrtc_card`,Mc=`${Cc}.triggers.occupancy`,Sc=`${Cc}.image.refresh_seconds`,Tc="view",Ac=`${Tc}.camera_select`,zc=`${Tc}.dark_mode`,jc=`${Tc}.default`,Oc=`${Tc}.timeout_seconds`,Ic=`${Tc}.update_cycle_camera`,Rc=`${Tc}.update_force`,Dc=`${Tc}.update_seconds`,Pc=`${Tc}.scan`,Lc=`${Pc}.enabled`,Nc=`${Pc}.show_trigger_status`,Uc=`${Pc}.untrigger_reset`,Fc=`${Pc}.untrigger_seconds`,Hc="media_gallery",Zc=`${Hc}.controls.filter.mode`,qc=`${Hc}.controls.thumbnails.show_details`,Vc=`${Hc}.controls.thumbnails.show_download_control`,Wc=`${Hc}.controls.thumbnails.show_favorite_control`,Bc=`${Hc}.controls.thumbnails.show_timeline_control`,Yc=`${Hc}.controls.thumbnails.size`,Qc="media_viewer",Gc=`${Qc}.auto_play`,Kc=`${Qc}.auto_pause`,Xc=`${Qc}.auto_mute`,Jc=`${Qc}.auto_unmute`,el=`${Qc}.draggable`,tl=`${Qc}.lazy_load`,nl=`${Qc}.snapshot_click_plays_clip`,il=`${Qc}.transition_effect`,al=`${Qc}.controls.builtin`,rl=`${Qc}.controls.next_previous.style`,ol=`${Qc}.controls.next_previous.size`,sl=`${Qc}.controls.thumbnails.mode`,cl=`${Qc}.controls.thumbnails.show_details`,ll=`${Qc}.controls.thumbnails.show_download_control`,dl=`${Qc}.controls.thumbnails.show_favorite_control`,ul=`${Qc}.controls.thumbnails.show_timeline_control`,hl=`${Qc}.controls.thumbnails.size`,ml=`${Qc}.controls.timeline.clustering_threshold`,pl=`${Qc}.controls.timeline.media`,fl=`${Qc}.controls.timeline.mode`,gl=`${Qc}.controls.timeline.show_recordings`,vl=`${Qc}.controls.timeline.style`,_l=`${Qc}.controls.timeline.window_seconds`,yl=`${Qc}.zoomable`,bl=`${Qc}.controls.title.mode`,wl=`${Qc}.controls.title.duration_seconds`,xl=`${Qc}.layout.fit`,Cl=`${Qc}.layout.position.x`,$l=`${Qc}.layout.position.y`,kl="live",El=`${kl}.auto_play`,Ml=`${kl}.auto_pause`,Sl=`${kl}.auto_mute`,Tl=`${kl}.auto_unmute`,Al=`${kl}.controls.builtin`,zl=`${kl}.controls.next_previous.style`,jl=`${kl}.controls.next_previous.size`,Ol=`${kl}.controls.thumbnails.media`,Il=`${kl}.controls.thumbnails.mode`,Rl=`${kl}.controls.thumbnails.size`,Dl=`${kl}.controls.thumbnails.show_details`,Pl=`${kl}.controls.thumbnails.show_download_control`,Ll=`${kl}.controls.thumbnails.show_favorite_control`,Nl=`${kl}.controls.thumbnails.show_timeline_control`,Ul=`${kl}.controls.timeline.clustering_threshold`,Fl=`${kl}.controls.timeline.media`,Hl=`${kl}.controls.timeline.mode`,Zl=`${kl}.controls.timeline.show_recordings`,ql=`${kl}.controls.timeline.style`,Vl=`${kl}.controls.timeline.window_seconds`,Wl=`${kl}.controls.title.mode`,Bl=`${kl}.controls.title.duration_seconds`,Yl=`${kl}.layout.fit`,Ql=`${kl}.layout.position.x`,Gl=`${kl}.layout.position.y`,Kl=`${kl}.draggable`,Xl=`${kl}.lazy_load`,Jl=`${kl}.lazy_unload`,ed=`${kl}.preload`,td=`${kl}.transition_effect`,nd=`${kl}.show_image_during_load`,id=`${kl}.microphone.disconnect_seconds`,ad=`${kl}.microphone.always_connected`,rd=`${kl}.zoomable`,od="image",sd=`${od}.layout.fit`,cd=`${od}.layout.position.x`,ld=`${od}.layout.position.y`,dd=`${od}.mode`,ud=`${od}.refresh_seconds`,hd=`${od}.url`,md=`${od}.zoomable`,pd="timeline",fd=`${pd}.window_seconds`,gd=`${pd}.clustering_threshold`,vd=`${pd}.media`,_d=`${pd}.show_recordings`,yd=`${pd}.style`,bd=`${pd}.controls.thumbnails.mode`,wd=`${pd}.controls.thumbnails.size`,xd=`${pd}.controls.thumbnails.show_details`,Cd=`${pd}.controls.thumbnails.show_download_control`,$d=`${pd}.controls.thumbnails.show_favorite_control`,kd=`${pd}.controls.thumbnails.show_timeline_control`,Ed="menu",Md=`${Ed}.alignment`,Sd=`${Ed}.position`,Td=`${Ed}.style`,Ad=`${Ed}.button_size`,zd=`${Ed}.buttons`,jd=`${Ed}.buttons.cameras`,Od=`${Ed}.buttons.clips`,Id=`${Ed}.buttons.download`,Rd=`${Ed}.buttons.frigate`,Dd=`${Ed}.buttons.camera_ui`,Pd=`${Ed}.buttons.fullscreen`,Ld=`${Ed}.buttons.image`,Nd=`${Ed}.buttons.live`,Ud=`${Ed}.buttons.media_player`,Fd=`${Ed}.buttons.snapshots`,Hd=`${Ed}.buttons.timeline`,Zd="dimensions",qd=`${Zd}.aspect_ratio`,Vd=`${Zd}.aspect_ratio_mode`,Wd=`${Zd}.max_height`,Bd=`${Zd}.min_height`,Yd="overrides",Qd="performance",Gd=`${Qd}.features.animated_progress_indicator`,Kd=`${Qd}.features.media_chunk_size`,Xd=`${Qd}.profile`,Jd=`${Qd}.style.box_shadow`,eu=`${Qd}.style.border_radius`,tu=1e3,nu="frigate";function iu(e){if(e instanceof Ws.ZodDefault)return iu(e.removeDefault());if(e instanceof Ws.ZodObject){const t={};for(const n in e.shape){const i=e.shape[n];t[n]=Ws.ZodOptional.create(iu(i))}return new Ws.ZodObject({...e._def,shape:()=>t})}return e instanceof Ws.ZodArray?Ws.ZodArray.create(iu(e.element)):e instanceof Ws.ZodOptional?Ws.ZodOptional.create(iu(e.unwrap())):e instanceof Ws.ZodNullable?Ws.ZodNullable.create(iu(e.unwrap())):e instanceof Ws.ZodTuple?Ws.ZodTuple.create(e.items.map((e=>iu(e)))):e}function au(e){const t=e.format();return Object.keys(t).filter((e=>!e.startsWith("_")))}const ru=e=>{const t=new Set;if(e&&e.issues)for(let n=0;n{let t="";for(let n=0;n"fire-dom-event")).or(Ws.literal("fire-dom-event")),card_id:Ws.string().optional()}),Mu=["camera_ui","default","diagnostics","expand","download","fullscreen","menu_toggle","mute","live_substream_on","live_substream_off","microphone_mute","microphone_unmute","play","pause","screenshot","unmute"],Su=Eu.extend({frigate_card_action:Ws.enum(du)}),Tu=Eu.extend({frigate_card_action:Ws.enum(Mu)}),Au=Eu.extend({frigate_card_action:Ws.literal("camera_select"),camera:Ws.string()}),zu=Eu.extend({frigate_card_action:Ws.literal("live_substream_select"),camera:Ws.string()}),ju=Eu.extend({frigate_card_action:Ws.literal("media_player"),media_player:Ws.string(),media_player_action:Ws.enum(["play","stop"])}),Ou=Ws.union([Su,Tu,Au,zu,ju]),Iu=Ws.union([yu,bu,wu,xu,Cu,ku,$u,Ou]),Ru=Ws.object({tap_action:Iu.or(Iu.array()).optional(),hold_action:Iu.or(Iu.array()).optional(),double_tap_action:Iu.or(Iu.array()).optional(),start_tap_action:Iu.or(Iu.array()).optional(),end_tap_action:Iu.or(Iu.array()).optional()}).passthrough(),Du=Ws.object({actions:Ru.optional()}),Pu=Ru.extend({style:Ws.object({}).passthrough().optional(),title:Ws.string().nullable().optional()}),Lu=Pu.extend({type:Ws.literal("state-badge"),entity:Ws.string()}),Nu=Pu.extend({type:Ws.literal("state-icon"),entity:Ws.string(),icon:Ws.string().optional(),state_color:Ws.boolean().default(!0)}),Uu=Pu.extend({type:Ws.literal("state-label"),entity:Ws.string(),attribute:Ws.string().optional(),prefix:Ws.string().optional(),suffix:Ws.string().optional()}),Fu=Pu.extend({type:Ws.literal("service-button"),title:Ws.string(),service:Ws.string(),service_data:Ws.object({}).passthrough().optional()}),Hu=Pu.extend({type:Ws.literal("icon"),icon:Ws.string(),entity:Ws.string().optional()}),Zu=Pu.extend({type:Ws.literal("image"),entity:Ws.string().optional(),image:Ws.string().optional(),camera_image:Ws.string().optional(),camera_view:Ws.string().optional(),state_image:Ws.object({}).passthrough().optional(),filter:Ws.string().optional(),state_filter:Ws.object({}).passthrough().optional(),aspect_ratio:Ws.string().optional()}),qu=Ws.object({entity:Ws.string(),state:Ws.string().optional(),state_not:Ws.string().optional()}).array(),Vu=Ws.object({type:Ws.literal("conditional"),conditions:qu,elements:Ws.lazy((()=>mh))}),Wu=Ws.object({type:Ws.string().superRefine(((e,t)=>{e.match(/^custom:(?!frigate-card).+/)||t.addIssue({code:Ws.ZodIssueCode.custom,message:"Frigate-card custom elements must match specific schemas",fatal:!0})}))}).passthrough(),Bu={refresh_seconds:1},Yu=Ws.object({url:Ws.string().optional(),refresh_seconds:Ws.number().min(0).default(Bu.refresh_seconds)}),Qu={always_connected:!1,disconnect_seconds:60},Gu=Ws.object({always_connected:Ws.boolean().default(Qu.always_connected),disconnect_seconds:Ws.number().min(0).default(Qu.disconnect_seconds)}).default(Qu),Ku=Ws.object({modes:Ws.enum(["webrtc","mse","mp4","mjpeg"]).array().optional(),stream:Ws.string().optional()}),Xu=Yu,Ju=Ws.object({entity:Ws.string().optional(),url:Ws.string().optional()}).passthrough(),eh=Ws.object({options:Ws.object({audio:Ws.boolean().optional(),video:Ws.boolean().optional(),pauseWhenHidden:Ws.boolean().optional(),disableGl:Ws.boolean().optional(),disableWebAssembly:Ws.boolean().optional(),preserveDrawingBuffer:Ws.boolean().optional(),progressive:Ws.boolean().optional(),throttled:Ws.boolean().optional(),chunkSize:Ws.number().optional(),maxAudioLag:Ws.number().optional(),videoBufferSize:Ws.number().optional(),audioBufferSize:Ws.number().optional()}).optional()}),th={dependencies:{all_cameras:!1,cameras:[]},engine:"auto",frigate:{client_id:"frigate"},hide:!1,image:{refresh_seconds:1},live_provider:"auto",motioneye:{images:{directory_pattern:"%Y-%m-%d",file_pattern:"%H-%M-%S"},movies:{directory_pattern:"%Y-%m-%d",file_pattern:"%H-%M-%S"}},triggers:{motion:!1,occupancy:!0,entities:[]}},nh=Ws.object({camera_entity:Ws.string().optional(),icon:Ws.string().optional(),title:Ws.string().optional(),hide:Ws.boolean().optional(),id:Ws.string().optional(),dependencies:Ws.object({all_cameras:Ws.boolean().default(th.dependencies.all_cameras),cameras:Ws.string().array().default(th.dependencies.cameras)}).default(th.dependencies),triggers:Ws.object({motion:Ws.boolean().default(th.triggers.motion),occupancy:Ws.boolean().default(th.triggers.occupancy),entities:Ws.string().array().default(th.triggers.entities)}).default(th.triggers),engine:Ws.enum(["auto","frigate","generic","motioneye"]).default("auto"),frigate:Ws.object({url:Ws.string().optional(),client_id:Ws.string().default(th.frigate.client_id),camera_name:Ws.string().optional(),labels:Ws.string().array().optional(),zones:Ws.string().array().optional()}).default(th.frigate),motioneye:Ws.object({url:Ws.string().optional(),images:Ws.object({directory_pattern:Ws.string().includes("%").default(th.motioneye.images.directory_pattern),file_pattern:Ws.string().includes("%").default(th.motioneye.images.file_pattern)}).default(th.motioneye.images),movies:Ws.object({directory_pattern:Ws.string().includes("%").default(th.motioneye.movies.directory_pattern),file_pattern:Ws.string().includes("%").default(th.motioneye.movies.file_pattern)}).default(th.motioneye.movies)}).default(th.motioneye),live_provider:Ws.enum(["auto","image","ha","jsmpeg","go2rtc","webrtc-card"]).default(th.live_provider),go2rtc:Ku.optional(),image:Xu.default(th.image),jsmpeg:eh.optional(),webrtc_card:Ju.optional()}).default(th),ih=nh.array().min(1),ah=Ws.object({enabled:Ws.boolean().default(!0).optional(),priority:Ws.number().min(0).max(100).default(50).optional(),alignment:Ws.enum(["matching","opposing"]).default("matching").optional(),icon:Ws.string().optional()}),rh=ah.merge(Hu).extend({type:Ws.literal("custom:frigate-card-menu-icon")}),oh=ah.merge(Nu).extend({type:Ws.literal("custom:frigate-card-menu-state-icon")}).merge(ah),sh=Pu.extend({entity:Ws.string().optional(),icon:Ws.string().optional(),state_color:Ws.boolean().default(!0),selected:Ws.boolean().default(!1),subtitle:Ws.string().optional(),enabled:Ws.boolean().default(!0)}),ch=ah.merge(Hu).extend({type:Ws.literal("custom:frigate-card-menu-submenu"),items:sh.array()}),lh=ah.merge(Nu).extend({type:Ws.literal("custom:frigate-card-menu-submenu-select"),options:Ws.record(sh.deepPartial()).optional()}),dh=Ws.object({view:Ws.string().array().optional(),fullscreen:Ws.boolean().optional(),expand:Ws.boolean().optional(),camera:Ws.string().array().optional(),media_loaded:Ws.boolean().optional(),state:qu.optional(),media_query:Ws.string().optional()}),uh=Ws.object({type:Ws.literal("custom:frigate-card-conditional"),conditions:dh,elements:Ws.lazy((()=>mh))}),hh=Ws.preprocess((e=>{if(!e||"object"!=typeof e||!e.service)return e;const t={...e};return["left","right","up","down","zoom_in","zoom_out","home"].forEach((n=>{`data_${n}`in e&&!(`actions_${n}`in e)&&(t[`actions_${n}`]={tap_action:{action:"call-service",service:e.service,service_data:e[`data_${n}`]}},delete t[`data_${n}`])})),t}),Ws.object({type:Ws.literal("custom:frigate-card-ptz"),style:Ws.object({}).passthrough().optional(),orientation:Ws.enum(["vertical","horizontal"]).default("vertical").optional(),service:Ws.string().optional(),actions_left:Ru.optional(),actions_right:Ru.optional(),actions_up:Ru.optional(),actions_down:Ru.optional(),actions_zoom_in:Ru.optional(),actions_zoom_out:Ru.optional(),actions_home:Ru.optional()})),mh=Ws.union([oh,rh,ch,lh,uh,hh,Lu,Nu,Uu,Fu,Hu,Zu,Vu,Wu]).array().optional(),ph=Ws.object({fit:Ws.enum(["contain","cover","fill"]).optional(),position:Ws.object({x:Ws.number().min(0).max(100).optional(),y:Ws.number().min(0).max(100).optional()}).optional()}),fh={default:uu,camera_select:"current",timeout_seconds:300,update_seconds:0,update_force:!1,update_cycle_camera:!1,dark_mode:"off",scan:{enabled:!1,show_trigger_status:!0,untrigger_seconds:0,untrigger_reset:!0}},gh=Ws.object({default:Ws.enum(du).default(fh.default),camera_select:Ws.enum([...du,"current"]).default(fh.camera_select),timeout_seconds:Ws.number().default(fh.timeout_seconds),update_seconds:Ws.number().default(fh.update_seconds),update_force:Ws.boolean().default(fh.update_force),update_cycle_camera:Ws.boolean().default(fh.update_cycle_camera),update_entities:Ws.string().array().optional(),render_entities:Ws.string().array().optional(),dark_mode:Ws.enum(["on","off","auto"]).optional(),scan:Ws.object({enabled:Ws.boolean().default(fh.scan.enabled),show_trigger_status:Ws.boolean().default(fh.scan.show_trigger_status),untrigger_seconds:Ws.number().default(fh.scan.untrigger_seconds),untrigger_reset:Ws.boolean().default(fh.scan.untrigger_reset)}).default(fh.scan)}).merge(Du).default(fh),vh={mode:"url",zoomable:!0,...Bu},_h=Yu.extend({mode:Ws.enum(["screensaver","camera","url"]).default(vh.mode),layout:ph.optional(),zoomable:Ws.boolean().default(vh.zoomable)}).merge(Du).default(vh),yh={size:100,show_details:!0,show_favorite_control:!0,show_timeline_control:!0,show_download_control:!0},bh=Ws.object({size:Ws.number().min(75).max(cu).default(yh.size),show_details:Ws.boolean().default(yh.show_details),show_favorite_control:Ws.boolean().default(yh.show_favorite_control),show_timeline_control:Ws.boolean().default(yh.show_timeline_control),show_download_control:Ws.boolean().default(yh.show_download_control)}),wh={...yh,mode:"right"},xh=bh.extend({mode:Ws.enum(["none","above","below","left","right"]).default(wh.mode)}),Ch={clustering_threshold:3,media:"all",window_seconds:3600,show_recordings:!0,style:"stack"},$h=Ws.enum(["all","clips","snapshots"]),kh=Ws.object({clustering_threshold:Ws.number().optional().default(Ch.clustering_threshold),media:$h.optional().default(Ch.media),window_seconds:Ws.number().min(60).max(86400).optional().default(Ch.window_seconds),show_recordings:Ws.boolean().optional().default(Ch.show_recordings),style:Ws.enum(["stack","ribbon"]).optional().default(Ch.style)}),Eh={...Ch,mode:"none",style:"ribbon"},Mh=kh.extend({mode:Ws.enum(["none","above","below"]).default(Eh.mode),style:kh.shape.style.default(Eh.style)}),Sh=Ws.object({style:Ws.enum(["none","chevrons","icons","thumbnails"]),size:Ws.number().min(su)}),Th=Ws.enum(["none","slide"]),Ah=Ws.object({mode:Ws.enum(["none","popup-top-right","popup-top-left","popup-bottom-right","popup-bottom-left"]),duration_seconds:Ws.number().min(0).max(60)}),zh={auto_play:"all",auto_pause:"never",auto_mute:"all",auto_unmute:"never",preload:!1,lazy_load:!0,lazy_unload:"never",draggable:!0,zoomable:!0,transition_effect:"slide",show_image_during_load:!0,controls:{builtin:!0,next_previous:{size:48,style:"chevrons"},thumbnails:{...wh,media:"all"},timeline:Eh,title:{mode:"popup-bottom-right",duration_seconds:2}},microphone:{...Qu}},jh=xh.extend({media:Ws.enum(["all","clips","snapshots"]).default(zh.controls.thumbnails.media)}),Oh=Ws.object({controls:Ws.object({builtin:Ws.boolean().default(zh.controls.builtin),next_previous:Sh.extend({style:Ws.enum(["none","chevrons","icons"]).default(zh.controls.next_previous.style),size:Sh.shape.size.default(zh.controls.next_previous.size)}).default(zh.controls.next_previous),thumbnails:jh.default(zh.controls.thumbnails),timeline:Mh.default(zh.controls.timeline),title:Ah.extend({mode:Ah.shape.mode.default(zh.controls.title.mode),duration_seconds:Ah.shape.duration_seconds.default(zh.controls.title.duration_seconds)}).default(zh.controls.title)}).default(zh.controls),show_image_during_load:Ws.boolean().default(zh.show_image_during_load),layout:ph.optional(),microphone:Gu.default(zh.microphone),zoomable:Ws.boolean().default(zh.zoomable)}).merge(Du),Ih=Oh.extend({auto_play:Ws.enum(gu).default(zh.auto_play),auto_pause:Ws.enum(fu).default(zh.auto_pause),auto_mute:Ws.enum(fu).default(zh.auto_mute),auto_unmute:Ws.enum(gu).default(zh.auto_unmute),preload:Ws.boolean().default(zh.preload),lazy_load:Ws.boolean().default(zh.lazy_load),lazy_unload:Ws.enum(fu).default(zh.lazy_unload),draggable:Ws.boolean().default(zh.draggable),transition_effect:Th.default(zh.transition_effect)}).default(zh),Rh={priority:50,enabled:!0},Dh={priority:50,enabled:!1},Ph={style:"hidden",position:"top",alignment:"left",buttons:{frigate:Rh,cameras:Rh,substreams:Rh,live:Rh,clips:Rh,snapshots:Rh,image:Dh,timeline:Rh,download:Rh,camera_ui:Rh,fullscreen:Rh,expand:Dh,media_player:Rh,microphone:{...Dh,type:"momentary"},mute:Dh,play:Dh,recordings:Dh,screenshot:Dh},button_size:40},Lh=ah.extend({enabled:ah.shape.enabled.default(Rh.enabled),priority:ah.shape.priority.default(Rh.priority)}),Nh=ah.extend({enabled:ah.shape.enabled.default(Dh.enabled),priority:ah.shape.priority.default(Dh.priority)}),Uh=Ws.object({style:Ws.enum(["none","hidden","overlay","hover","hover-card","outside"]).default(Ph.style),position:Ws.enum(hu).default(Ph.position),alignment:Ws.enum(mu).default(Ph.alignment),buttons:Ws.object({frigate:Lh.default(Ph.buttons.frigate),cameras:Lh.default(Ph.buttons.cameras),substreams:Lh.default(Ph.buttons.substreams),live:Lh.default(Ph.buttons.live),clips:Lh.default(Ph.buttons.clips),snapshots:Lh.default(Ph.buttons.snapshots),image:Nh.default(Ph.buttons.image),timeline:Lh.default(Ph.buttons.timeline),download:Lh.default(Ph.buttons.download),camera_ui:Lh.default(Ph.buttons.camera_ui),fullscreen:Lh.default(Ph.buttons.fullscreen),expand:Nh.default(Ph.buttons.expand),media_player:Lh.default(Ph.buttons.media_player),microphone:Nh.extend({type:Ws.enum(["momentary","toggle"]).default(Ph.buttons.microphone.type)}).default(Ph.buttons.microphone),recordings:Nh.default(Ph.buttons.recordings),mute:Nh.default(Ph.buttons.mute),play:Nh.default(Ph.buttons.play),screenshot:Nh.default(Ph.buttons.screenshot)}).default(Ph.buttons),button_size:Ws.number().min(su).default(Ph.button_size)}).default(Ph),Fh={auto_play:"all",auto_pause:"all",auto_mute:"all",auto_unmute:"never",lazy_load:!0,draggable:!0,zoomable:!0,transition_effect:"slide",snapshot_click_plays_clip:!0,controls:{builtin:!0,next_previous:{size:48,style:"thumbnails"},thumbnails:wh,timeline:Eh,title:{mode:"popup-bottom-right",duration_seconds:2}}},Hh=Sh.extend({style:Ws.enum(["none","thumbnails","chevrons"]).default(Fh.controls.next_previous.style),size:Sh.shape.size.default(Fh.controls.next_previous.size)}),Zh=Ws.object({auto_play:Ws.enum(gu).default(Fh.auto_play),auto_pause:Ws.enum(fu).default(Fh.auto_pause),auto_mute:Ws.enum(fu).default(Fh.auto_mute),auto_unmute:Ws.enum(gu).default(Fh.auto_unmute),lazy_load:Ws.boolean().default(Fh.lazy_load),draggable:Ws.boolean().default(Fh.draggable),zoomable:Ws.boolean().default(Fh.zoomable),transition_effect:Th.default(Fh.transition_effect),snapshot_click_plays_clip:Ws.boolean().default(Fh.snapshot_click_plays_clip),controls:Ws.object({builtin:Ws.boolean().default(Fh.controls.builtin),next_previous:Hh.default(Fh.controls.next_previous),thumbnails:xh.default(Fh.controls.thumbnails),timeline:Mh.default(Fh.controls.timeline),title:Ah.extend({mode:Ah.shape.mode.default(Fh.controls.title.mode),duration_seconds:Ah.shape.duration_seconds.default(Fh.controls.title.duration_seconds)}).default(Fh.controls.title)}).default(Fh.controls),layout:ph.optional()}).merge(Du).default(Fh),qh={...wh,show_details:!1},Vh={controls:{thumbnails:qh,filter:{mode:"right"}}},Wh=xh.extend({show_details:Ws.boolean().default(qh.show_details)}),Bh=Ws.object({controls:Ws.object({thumbnails:Wh.default(Vh.controls.thumbnails),filter:Ws.object({mode:Ws.enum(["none","left","right"]).default(Vh.controls.filter.mode)}).default(Vh.controls.filter)}).default(Vh.controls)}).merge(Du).default(Vh),Yh={aspect_ratio_mode:"dynamic",aspect_ratio:[16,9],max_height:"100vh",min_height:"100px"},Qh=Ws.object({aspect_ratio_mode:Ws.enum(["dynamic","static","unconstrained"]).default(Yh.aspect_ratio_mode),aspect_ratio:Ws.number().array().length(2).or(Ws.string().regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/).transform((e=>e.split(/[:\/]/).map((e=>Number(e)))))).default(Yh.aspect_ratio),max_height:Ws.string().default(Yh.max_height),min_height:Ws.string().default(Yh.min_height)}).default(Yh),Gh={...Ch,controls:{thumbnails:wh}},Kh=kh.extend({controls:Ws.object({thumbnails:xh.default(Gh.controls.thumbnails)}).default(Gh.controls)}).default(Gh),Xh=Ws.object({cameras:iu(ih).optional(),cameras_global:iu(nh).optional(),live:iu(Oh).optional(),menu:iu(Uh).optional(),image:iu(_h).optional(),view:iu(gh).optional(),dimensions:iu(Qh).optional()}),Jh=Ws.object({conditions:dh,overrides:Xh}).array().optional();Ws.object({conditions:dh,overrides:Oh}).array().optional();const em=Iu.array().optional(),tm=Ws.object({conditions:dh,actions:em,actions_not:em}).array().optional(),nm={profile:"high",features:{animated_progress_indicator:!0,media_chunk_size:50},style:{border_radius:!0,box_shadow:!0}},im=Ws.object({profile:Ws.enum(["low","high"]).default(nm.profile),features:Ws.object({animated_progress_indicator:Ws.boolean().default(nm.features.animated_progress_indicator),media_chunk_size:Ws.number().min(0).max(1e3).default(nm.features.media_chunk_size)}).default(nm.features),style:Ws.object({border_radius:Ws.boolean().default(nm.style.border_radius),box_shadow:Ws.boolean().default(nm.style.box_shadow)}).default(nm.style)}).default(nm),am={logging:!1},rm=Ws.object({logging:Ws.boolean().default(am.logging)}).default(am),om=Ws.object({cameras:iu(ih),cameras_global:nh,view:gh,menu:Uh,live:Ih,media_gallery:Bh,media_viewer:Zh,image:_h,elements:mh,dimensions:Qh,timeline:Kh,performance:im,debug:rm,automations:tm,overrides:Jh,card_mod:Ws.unknown(),card_id:Ws.string().regex(/^\w+$/).optional(),type:Ws.string(),test_gui:Ws.boolean().optional()}),sm={cameras:th,view:fh,menu:Ph,live:zh,media_gallery:Vh,media_viewer:Fh,image:vh,timeline:Gh,performance:nm,debug:am};Ws.discriminatedUnion("type",[rh,oh,ch,lh]);const cm={info:10,error:20,connection:30,diagnostics:40},lm=Ws.object({url:Ws.string(),mime_type:Ws.string()}),dm=Ws.object({path:Ws.string()});function um(e){if(!e)return null;const t=Ou.safeParse(e);return t.success?t.data:null}function hm(e,t){return"camera_select"===e||"live_substream_select"===e?t?.camera?{action:"fire-dom-event",frigate_card_action:e,camera:t.camera,...t.cardID&&{card_id:t.cardID}}:null:"media_player"===e?t?.media_player&&t.media_player_action?{action:"fire-dom-event",frigate_card_action:e,media_player:t.media_player,media_player_action:t.media_player_action,...t.cardID&&{card_id:t.cardID}}:null:{action:"fire-dom-event",frigate_card_action:e,...t?.cardID&&{card_id:t.cardID}}}function mm(e,t){if(e&&t)return"tap"==e&&t.tap_action?t.tap_action:"hold"==e&&t.hold_action?t.hold_action:"double_tap"==e&&t.double_tap_action?t.double_tap_action:"end_tap"==e&&t.end_tap_action?t.end_tap_action:"start_tap"==e&&t.start_tap_action?t.start_tap_action:void 0}const pm=(e,t,n,i,a)=>!(!a&&"tap"!=i)&&(fm(e,t,n,a),!0),fm=(e,t,n,i)=>{Array.isArray(i)?i.forEach((i=>h(e,t,n,i))):h(e,t,n,i)},gm=e=>Array.isArray(e)?!!e.find((e=>m(e))):m(e),vm=e=>{e.stopPropagation()};class _m{constructor(){this._timer=null}stop(){this._timer&&(window.clearTimeout(this._timer),this._timer=null)}isRunning(){return null!==this._timer}start(e,t){this.stop(),this._timer=window.setTimeout((()=>{this._timer=null,t()}),1e3*e)}startRepeated(e,t){this.stop(),this._timer=window.setInterval((()=>{t()}),1e3*e)}}class ym extends HTMLElement{constructor(){super(...arguments),this.holdTime=.4,this.holdTimer=new _m,this.doubleClickTimer=new _m,this.held=!1}connectedCallback(){["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach((e=>{document.addEventListener(e,(()=>{this.holdTimer.stop()}),{passive:!0})}))}bind(e,t){if(e.actionHandlerOptions)return void(e.actionHandlerOptions=t);e.actionHandlerOptions=t,e.addEventListener("contextmenu",(e=>{const t=e||window.event;return t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.cancelBubble=!0,t.returnValue=!1,!1}));const n=()=>{this.held=!1,this.holdTimer.start(this.holdTime,(()=>{this.held=!0})),l(e,"action",{action:"start_tap"})},i=t=>{const n=e.actionHandlerOptions;n?.allowPropagation||vm(t),["touchend","touchcancel"].includes(t.type)&&!this.held||(this.holdTimer.stop(),l(e,"action",{action:"end_tap"}),n?.hasHold&&this.held?l(e,"action",{action:"hold"}):n?.hasDoubleClick?"click"===t.type&&t.detail<2||!this.doubleClickTimer.isRunning()?this.doubleClickTimer.start(.25,(()=>l(e,"action",{action:"tap"}))):(this.doubleClickTimer.stop(),l(e,"action",{action:"double_tap"})):l(e,"action",{action:"tap"}))};e.addEventListener("touchstart",n,{passive:!0}),e.addEventListener("touchend",i),e.addEventListener("touchcancel",i),e.addEventListener("mousedown",n,{passive:!0}),e.addEventListener("click",i),e.addEventListener("keyup",(e=>{"Enter"===e.key&&i(e)}))}}customElements.define("action-handler-frigate-card",ym);const bm=(e,t)=>{const n=(()=>{const e=document.body;if(e.querySelector("action-handler-frigate-card"))return e.querySelector("action-handler-frigate-card");const t=document.createElement("action-handler-frigate-card");return e.appendChild(t),t})();n&&n.bind(e,t)},wm=$e(class extends ke{update(e,[t]){return bm(e.element,t),X}render(e){}});var xm={frigate_card:"Frigate card",frigate_card_description:"A Lovelace card for use with Frigate",live:"Live",no_media:"No media to display",recordings:"Recordings",version:"Version"},Cm={cameras:{camera_entity:"Camera Entity",dependencies:{all_cameras:"Show events for all cameras with this camera",cameras:"Show events for specific cameras with this camera",editor_label:"Dependency Options"},engines:{editor_label:"Camera engine options"},frigate:{camera_name:"Frigate camera name (Autodetected from entity)",client_id:"Frigate client id (For >1 Frigate server)",editor_label:"Frigate Options",labels:"Frigate labels/object filters",url:"Frigate server URL",zones:"Frigate zones"},go2rtc:{editor_label:"go2rtc Options",modes:{editor_label:"go2rtc Modes",mjpeg:"Motion JPEG (MJPEG)",mp4:"MPEG-4 (MP4)",mse:"Media Source Extensions (MSE)",webrtc:"Web Real-Time Communication (WebRTC)"},stream:"go2rtc stream name"},hide:"Hide camera from UI",icon:"Icon for this camera (Autodetected from entity)",id:"Unique id for this camera in this card",image:{editor_label:"Image Options",refresh_seconds:"Number of seconds after which to refresh live image (0=never)",url:"Image URL to use instead of camera entity snapshot"},live_provider:"Live view provider for this camera",live_provider_options:{editor_label:"Live provider options"},live_providers:{auto:"Automatic",go2rtc:"go2rtc",ha:"Home Assistant video stream (i.e. HLS, LL-HLS, WebRTC via HA)",image:"Home Assistant images",jsmpeg:"JSMpeg","webrtc-card":"WebRTC Card (i.e. AlexxIT's WebRTC Card)"},motioneye:{editor_label:"MotionEye Options",images:{directory_pattern:"Images directory pattern",file_pattern:"Images file pattern"},movies:{directory_pattern:"Movies directory pattern",file_pattern:"Movies file pattern"},url:"MotionEye UI URL"},title:"Title for this camera (Autodetected from entity)",triggers:{editor_label:"Trigger Options",entities:"Trigger from other entities",motion:"Trigger by auto-detecting the motion sensor",occupancy:"Trigger by auto-detecting the occupancy sensor"},webrtc_card:{editor_label:"WebRTC Card Options",entity:"WebRTC Card Camera Entity (Not a Frigate camera)",url:"WebRTC Card Camera URL"}},common:{controls:{builtin:"Built-in video controls",filter:{editor_label:"Media Filter",mode:"Filter mode",modes:{left:"Media filter in a drawer to the left",none:"No media filter",right:"Media filter in a drawer to the right"}},next_previous:{editor_label:"Next & Previous",size:"Next & previous control size in pixels",style:"Next & previous control style",styles:{chevrons:"Chevrons",icons:"Icons",none:"None",thumbnails:"Thumbnails"}},thumbnails:{editor_label:"Thumbnails",media:"Whether to show thumbnails of clips or snapshots",medias:{clips:"Clip thumbnails",snapshots:"Snapshot thumbnails"},mode:"Thumbnails mode",modes:{above:"Thumbnails above",below:"Thumbnails below",left:"Thumbnails in a drawer to the left",none:"No thumbnails",right:"Thumbnails in a drawer to the right"},show_details:"Show details with thumbnails",show_download_control:"Show download control on thumbnails",show_favorite_control:"Show favorite control on thumbnails",show_timeline_control:"Show timeline control on thumbnails",size:"Thumbnails size in pixels"},timeline:{editor_label:"Mini Timeline",mode:"Mode",modes:{above:"Above",below:"Below",none:"None"}},title:{duration_seconds:"Seconds to display popup title (0=forever)",editor_label:"Popup Title Controls",mode:"Popup title display mode",modes:{none:"No title display","popup-bottom-left":"Popup on the bottom left","popup-bottom-right":"Popup on the bottom right","popup-top-left":"Popup on the top left","popup-top-right":"Popup on the top right"}}},layout:{fit:"Layout fit",fits:{contain:"Media is contained/letterboxed",cover:"Media expands proportionally to cover the card",fill:"Media is stretched to fill the card"},position:{x:"Horizontal placement percentage",y:"Vertical placement percentage"}},media_action_conditions:{all:"All opportunities",hidden:"On browser/tab hiding",never:"Never",selected:"On selection",unselected:"On unselection",visible:"On browser/tab visibility"},timeline:{clustering_threshold:"The count of events at which they are clustered (0=no clustering)",media:"The media the timeline displays",medias:{all:"All media types",clips:"Clips",snapshots:"Snapshots"},show_recordings:"Show recordings",style:"Timeline style",styles:{ribbon:"Events on a single ribbon",stack:"Stacked & clustered events"},window_seconds:"The default length of the timeline view in seconds"}},dimensions:{aspect_ratio:"Default aspect ratio (e.g. '16:9')",aspect_ratio_mode:"Aspect ratio mode",aspect_ratio_modes:{dynamic:"Aspect ratio adjusts to media",static:"Static aspect ratio",unconstrained:"Unconstrained aspect ratio"},max_height:"Maximum card height in CSS units (e.g. '100vh')",min_height:"Minimum card height in CSS units (e.g. '100px')"},image:{layout:"Image Layout",mode:"Image view mode",modes:{camera:"Home Assistant camera snapshot of camera entity",screensaver:"Embedded Frigate logo",url:"Arbitrary image specified by URL"},refresh_seconds:"Number of seconds after which to refresh (0=never)",url:"Static image URL for image view",zoomable:"Image can be zoomed/panned"},live:{auto_mute:"Automatically mute live cameras",auto_pause:"Automatically pause live cameras",auto_play:"Automatically play live cameras",auto_unmute:"Automatically unmute live cameras",controls:{editor_label:"Live Controls"},draggable:"Live cameras view can be dragged/swiped",layout:"Live Layout",lazy_load:"Live cameras are lazily loaded",lazy_unload:"Live cameras are lazily unloaded",microphone:{always_connected:"Always keep the microphone connected",disconnect_seconds:"Seconds after which to disconnect microphone (0=never)",editor_label:"Microphone",enabled:"Microphone enabled"},preload:"Preload live view in the background",show_image_during_load:"Show still image while the live stream is loading",transition_effect:"Live camera transition effect",zoomable:"Live cameras can be zoomed/panned"},media_viewer:{auto_mute:"Automatically mute media",auto_pause:"Automatically pause media",auto_play:"Automatically play media",auto_unmute:"Automatically unmute media",controls:{editor_label:"Media Viewer Controls"},draggable:"Media Viewer can be dragged/swiped",layout:"Media Viewer Layout",lazy_load:"Media Viewer media is lazily loaded in carousel",snapshot_click_plays_clip:"Clicking on a snapshot plays a related clip",transition_effect:"Media Viewer transition effect",transition_effects:{none:"No transition",slide:"Slide transition"},zoomable:"Media Viewer can be zoomed/panned"},menu:{alignment:"Menu alignment",alignments:{bottom:"Aligned to the bottom",left:"Aligned to the left",right:"Aligned to the right",top:"Aligned to the top"},button_size:"Menu button size in pixels",buttons:{alignment:"Button alignment",alignments:{matching:"Matching the menu alignment",opposing:"Opposing the menu alignment"},camera_ui:"Camera user interface",cameras:"Cameras",clips:"Clips",download:"Download",enabled:"Button enabled",expand:"Expand",frigate:"Frigate menu / Default view",fullscreen:"Fullscreen",icon:"Icon",image:"Image",live:"Live",media_player:"Send to media player",microphone:"Microphone",mute:"Mute / Unmute",play:"Play / Pause",priority:"Priority",recordings:"Recordings",screenshot:"Screenshot",snapshots:"Snapshots",substreams:"Substream(s)",timeline:"Timeline",type:"Button type",types:{momentary:"Momentary",toggle:"Toggle"}},position:"Menu position",positions:{bottom:"Positioned on the bottom",left:"Positioned on the left",right:"Positioned on the right",top:"Positioned on the top"},style:"Menu style",styles:{hidden:"Hidden menu",hover:"Hover menu","hover-card":"Hover menu (card-wide)",none:"No menu",outside:"Outside menu",overlay:"Overlay menu"}},overrides:{info:"This card configuration has manually specified overrides configured which may override values shown in the visual editor, please consult the code editor to view/modify these overrides"},performance:{features:{animated_progress_indicator:"Animated Progress Indicator",editor_label:"Feature Options",media_chunk_size:"Media chunk size"},profile:"Performance profile",profiles:{high:"High/full performance",low:"Low performance"},style:{border_radius:"Curves",box_shadow:"Shadows",editor_label:"Style Options"},warning:"This card is in low profile mode so defaults have changed to optimize performance"},view:{camera_select:"View for newly selected cameras",dark_mode:"Dark mode",dark_modes:{auto:"Auto",off:"Off",on:"On"},default:"Default view",scan:{enabled:"Scan mode enabled",scan_mode:"Scan mode",show_trigger_status:"Show pulsing border when triggered",untrigger_reset:"Reset the view to default after untrigger",untrigger_seconds:"Seconds after inactive state change to untrigger"},timeout_seconds:"Reset to default view X seconds after user action (0=never)",update_cycle_camera:"Cycle through cameras when default view updates",update_force:"Force card updates (ignore user interaction)",update_seconds:"Refresh default view every X seconds (0=never)",views:{clip:"Most recent clip",clips:"Clips gallery",current:"Current view",image:"Static image",live:"Live view",recording:"Most recent recording",recordings:"Recordings gallery",snapshot:"Most recent snapshot",snapshots:"Snapshots gallery",timeline:"Timeline view"}}},$m={add_new_camera:"Add new camera",button:"Button",camera:"Camera",cameras:"Cameras",cameras_secondary:"What cameras to render on this card",delete:"Delete",dimensions:"Dimensions",dimensions_secondary:"Dimensions & shape options",image:"Image",image_secondary:"Static image view options",live:"Live",live_secondary:"Live camera view options",media_gallery:"Media gallery",media_gallery_secondary:"Media gallery options",media_viewer:"Media viewer",media_viewer_secondary:"Viewer for static media (clips, snapshots or recordings)",menu:"Menu",menu_secondary:"Menu look & feel options",move_down:"Move down",move_up:"Move up",overrides:"Overrides are active",overrides_secondary:"Dynamic configuration overrides detected",performance:"Performance",performance_secondary:"Card performance options",timeline:"Timeline",timeline_secondary:"Event timeline options",upgrade:"Upgrade",upgrade_available:"An automatic card configuration upgrade is available",view:"View",view_secondary:"What the card should show and how to show it"},km={ptz:{down:"Down",home:"Home",left:"Left",right:"Right",up:"Up",zoom_in:"Zoom In",zoom_out:"Zoom Out"}},Em={could_not_render_elements:"Could not render picture elements",could_not_resolve:"Could not resolve media URL",diagnostics:"Card diagnostics. Please review for confidential information prior to sharing",download_no_media:"No media to download",download_sign_failed:"Could not sign media URL for download",duplicate_camera_id:"Duplicate Frigate camera id for the following camera, use the 'id' parameter to uniquely identify cameras",empty_response:"Received empty response from Home Assistant for request",failed_response:"Failed to receive response from Home Assistant for request",failed_retain:"Could not retain event",failed_sign:"Could not sign Home Assistant URL",image_load_error:"The image could not be loaded",invalid_configuration:"Invalid configuration",invalid_configuration_no_hint:"No location hint available (bad or missing type?)",invalid_elements_config:"Invalid picture elements configuration",invalid_response:"Received invalid response from Home Assistant for request",jsmpeg_no_player:"Could not start JSMPEG player",live_camera_no_endpoint:"Could not get camera endpoint for this live provider (incomplete configuration?)",live_camera_not_found:"The configured camera_entity was not found",live_camera_unavailable:"Camera unavailable",no_camera_engine:"Could not determine suitable engine for camera",no_camera_entity:"Could not find camera entity",no_camera_entity_for_triggers:"A camera entity is required in order to autodetect triggers",no_camera_id:"Could not determine camera id for the following camera, may need to set 'id' parameter manually",no_camera_name:"Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",no_live_camera:"The camera_entity parameter must be set and valid for this live provider",no_visible_cameras:"No visible cameras found, you must configure at least one non-hidden camera",reconnecting:"Reconnecting",timeline_no_cameras:"No Frigate cameras to show in timeline",too_many_automations:"Too many nested automation calls, please check your configuration for loops",troubleshooting:"Check troubleshooting",unknown:"Unknown error",upgrade_available:"An automated card configuration upgrade is available, please visit the visual card editor",webrtc_card_reported_error:"WebRTC Card reported an error",webrtc_card_waiting:"Waiting for WebRTC Card to load ..."},Mm={camera:"Camera",duration:"Duration",in_progress:"In Progress",score:"Score",seek:"Seek",start:"Start",tag:"Tag",what:"What",where:"Where"},Sm={all:"All",camera:"Camera",favorite:"Favorite",media_type:"Media Type",media_types:{clips:"Clips",recordings:"Recordings",snapshots:"Snapshots"},not_favorite:"Not Favorite",select_camera:"Select camera...",select_favorite:"Select favorite...",select_media_type:"Select media type...",select_tag:"Select tag...",select_what:"Select what...",select_when:"Select when...",select_where:"Select where...",tag:"Tag",what:"What",when:"When",whens:{past_month:"Past Month",past_week:"Past Week",today:"Today",yesterday:"Yesterday"},where:"Where"},Tm={camera:"Camera",duration:"Duration",events:"Events",in_progress:"In Progress",seek:"Seek",start:"Start"},Am={download:"Download media",no_thumbnail:"No thumbnail available",retain_indefinitely:"Media will be indefinitely retained",timeline:"See media in timeline"},zm={pan_behavior:{pan:"Pan",seek:"Pan seeks across all media","seek-in-media":"Pan seeks within selected media item only"},select_date:"Choose date"},jm={common:xm,config:Cm,editor:$m,elements:km,error:Em,event:Mm,media_filter:Sm,recording:Tm,thumbnail:Am,timeline:zm};const Om="en",Im={[Om]:Object.freeze({__proto__:null,common:xm,config:Cm,editor:$m,elements:km,error:Em,event:Mm,media_filter:Sm,recording:Tm,thumbnail:Am,timeline:zm,default:jm})};let Rm;function Dm(e){const t=e=>e.replace("-","_"),n=e?.language??e?.selectedLanguage;if(n)return t(n);const i=localStorage.getItem("selectedLanguage");if(i){const e=JSON.parse(i);if(e)return t(e)}for(const e of navigator.languages){const n=t(e);if(n&&n in Im)return n}return Om}function Pm(e,t="",n=""){let i="";try{i=e.split(".").reduce(((e,t)=>e[t]),Im[Rm??Om])}catch(e){}return i||(i=e.split(".").reduce(((e,t)=>e[t]),Im[Om])),""!==t&&""!==n&&(i=i.replace(t,n)),i}class Lm extends vu{}class Nm{constructor(e){this._priorEvaluations=new Map,this._nestedAutomationExecutions=0,this._automations=e}execute(e,t,n){const i=[];for(const e of this._automations??[]){const t=n.evaluateCondition(e.conditions),a=t?e.actions:e.actions_not,r=this._priorEvaluations.get(e);this._priorEvaluations.set(e,t),t!==r&&a&&i.push(a)}if(++this._nestedAutomationExecutions,this._nestedAutomationExecutions>10)throw new Lm(Pm("error.too_many_automations"));i.forEach((n=>{fm(e,t,{},n)})),--this._nestedAutomationExecutions}}function Um(e){if(null===e||!0===e||!1===e)return NaN;var t=Number(e);return isNaN(t)?t:t<0?Math.ceil(t):Math.floor(t)}function Fm(e,t){if(t.length1?"s":"")+" required, but only "+t.length+" present")}function Hm(e){return Hm="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Hm(e)}function Zm(e){Fm(1,arguments);var t=Object.prototype.toString.call(e);return e instanceof Date||"object"===Hm(e)&&"[object Date]"===t?new Date(e.getTime()):"number"==typeof e||"[object Number]"===t?new Date(e):("string"!=typeof e&&"[object String]"!==t||"undefined"==typeof console||(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments"),console.warn((new Error).stack)),new Date(NaN))}function qm(e,t){Fm(2,arguments);var n=Zm(e),i=Um(t);return isNaN(i)?new Date(NaN):i?(n.setDate(n.getDate()+i),n):n}function Vm(e,t){Fm(2,arguments);var n=Zm(e),i=Um(t);if(isNaN(i))return new Date(NaN);if(!i)return n;var a=n.getDate(),r=new Date(n.getTime());return r.setMonth(n.getMonth()+i+1,0),a>=r.getDate()?r:(n.setFullYear(r.getFullYear(),r.getMonth(),a),n)}function Wm(e){return Wm="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Wm(e)}function Bm(e,t){if(Fm(2,arguments),!t||"object"!==Wm(t))return new Date(NaN);var n=t.years?Um(t.years):0,i=t.months?Um(t.months):0,a=t.weeks?Um(t.weeks):0,r=t.days?Um(t.days):0,o=t.hours?Um(t.hours):0,s=t.minutes?Um(t.minutes):0,c=t.seconds?Um(t.seconds):0,l=Zm(e),d=i||n?Vm(l,i+12*n):l,u=r||a?qm(d,r+7*a):d,h=1e3*(c+60*(s+60*o));return new Date(u.getTime()+h)}const Ym=(e,t)=>0!=((e.attributes.supported_features??0)&t),Qm=e=>(e=>Ym(e,4)&&"number"==typeof e.attributes.in_progress)(e)||!!e.attributes.in_progress,Gm=e=>{switch(e){case"armed_away":return"mdi:shield-lock";case"armed_vacation":return"mdi:shield-airplane";case"armed_home":return"mdi:shield-home";case"armed_night":return"mdi:shield-moon";case"armed_custom_bypass":return"mdi:security";case"pending":case"arming":return"mdi:shield-sync";case"triggered":return"mdi:bell-ring";case"disarmed":return"mdi:shield-off";default:return"mdi:shield"}},Km=(e,t)=>{const n="off"===e;switch(t?.attributes.device_class){case"battery":return n?"mdi:battery":"mdi:battery-outline";case"battery_charging":return n?"mdi:battery":"mdi:battery-charging";case"cold":return n?"mdi:thermometer":"mdi:snowflake";case"connectivity":return n?"mdi:close-network-outline":"mdi:check-network-outline";case"door":return n?"mdi:door-closed":"mdi:door-open";case"garage_door":return n?"mdi:garage":"mdi:garage-open";case"power":case"plug":return n?"mdi:power-plug-off":"mdi:power-plug";case"gas":case"problem":case"safety":case"tamper":return n?"mdi:check-circle":"mdi:alert-circle";case"smoke":return n?"mdi:check-circle":"mdi:smoke";case"heat":return n?"mdi:thermometer":"mdi:fire";case"light":return n?"mdi:brightness5":"mdi:brightness-7";case"lock":return n?"mdi:lock":"mdi:lock-open";case"moisture":return n?"mdi:water-off":"mdi:water";case"motion":return n?"mdi:motion-sensor-off":"mdi:motion-sensor";case"occupancy":case"presence":return n?"mdi:home-outline":"mdi:home";case"opening":return n?"mdi:square":"mdi:square-outline";case"running":return n?"mdi:stop":"mdi:play";case"sound":return n?"mdi:music-note-off":"mdi:music-note";case"update":return n?"mdi:package":"mdi:package-up";case"vibration":return n?"mdi:crop-portrait":"mdi:vibrate";case"window":return n?"mdi:window-closed":"mdi:window-open";default:return n?"mdi:radiobox-blank":"mdi:checkbox-marked-circle"}},Xm=(e,t)=>{const n="closed"!==e;switch(t?.attributes.device_class){case"garage":switch(e){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:garage";default:return"mdi:garage-open"}case"gate":switch(e){case"opening":case"closing":return"mdi:gate-arrow-right";case"closed":return"mdi:gate";default:return"mdi:gate-open"}case"door":return n?"mdi:door-open":"mdi:door-closed";case"damper":return n?"md:circle":"mdi:circle-slice-8";case"shutter":switch(e){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:window-shutter";default:return"mdi:window-shutter-open"}case"curtain":switch(e){case"opening":return"mdi:arrow-split-vertical";case"closing":return"mdi:arrow-collapse-horizontal";case"closed":return"mdi:curtains-closed";default:return"mdi:curtains"}case"blind":case"shade":switch(e){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:blinds";default:return"mdi:blinds-open"}case"window":switch(e){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:window-closed";default:return"mdi:window-open"}}switch(e){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:window-closed";default:return"mdi:window-open"}},Jm={apparent_power:"mdi:flash",aqi:"mdi:air-filter",carbon_dioxide:"mdi:molecule-co2",carbon_monoxide:"mdi:molecule-co",current:"mdi:current-ac",date:"mdi:calendar",energy:"mdi:lightning-bolt",frequency:"mdi:sine-wave",gas:"mdi:gas-cylinder",humidity:"mdi:water-percent",illuminance:"mdi:brightness-5",monetary:"mdi:cash",nitrogen_dioxide:"mdi:molecule",nitrogen_monoxide:"mdi:molecule",nitrous_oxide:"mdi:molecule",ozone:"mdi:molecule",pm1:"mdi:molecule",pm10:"mdi:molecule",pm25:"mdi:molecule",power:"mdi:flash",power_factor:"mdi:angle-acute",pressure:"mdi:gauge",reactive_power:"mdi:flash",signal_strength:"mdi:wifi",sulphur_dioxide:"mdi:molecule",temperature:"mdi:thermometer",timestamp:"mdi:clock",volatile_organic_compounds:"mdi:molecule",voltage:"mdi:sine-wave"},ep={10:"mdi:battery-10",20:"mdi:battery-20",30:"mdi:battery-30",40:"mdi:battery-40",50:"mdi:battery-50",60:"mdi:battery-60",70:"mdi:battery-70",80:"mdi:battery-80",90:"mdi:battery-90",100:"mdi:battery"},tp={10:"mdi:battery-charging-10",20:"mdi:battery-charging-20",30:"mdi:battery-charging-30",40:"mdi:battery-charging-40",50:"mdi:battery-charging-50",60:"mdi:battery-charging-60",70:"mdi:battery-charging-70",80:"mdi:battery-charging-80",90:"mdi:battery-charging-90",100:"mdi:battery-charging"},np=(e,t)=>{const n=Number(e);if(isNaN(n))return"off"===e?"mdi:battery":"on"===e?"mdi:battery-alert":"mdi:battery-unknown";const i=10*Math.round(n/10);return t&&n>=10?tp[i]:t?"mdi:battery-charging-outline":n<=5?"mdi:battery-alert-variant-outline":ep[i]},ip=e=>{const t=e?.attributes.device_class;if(t&&t in Jm)return Jm[t];if("battery"===t)return e?((e,t)=>{const n=e.state;return np(n,"on"===t?.state)})(e):"mdi:battery";const n=e?.attributes.unit_of_measurement;return"°C"===n||"°F"===n?"mdi:thermometer":void 0},ap="mdi:bookmark",rp={alert:"mdi:alert",air_quality:"mdi:air-filter",automation:"mdi:robot",calendar:"mdi:calendar",camera:"mdi:video",climate:"mdi:thermostat",configurator:"mdi:cog",conversation:"mdi:text-to-speech",counter:"mdi:counter",fan:"mdi:fan",google_assistant:"mdi:google-assistant",group:"mdi:google-circles-communities",homeassistant:"mdi:home-assistant",homekit:"mdi:home-automation",image_processing:"mdi:image-filter-frames",input_button:"mdi:gesture-tap-button",input_datetime:"mdi:calendar-clock",input_number:"mdi:ray-vertex",input_select:"mdi:format-list-bulleted",input_text:"mdi:form-textbox",light:"mdi:lightbulb",mailbox:"mdi:mailbox",notify:"mdi:comment-alert",number:"mdi:ray-vertex",persistent_notification:"mdi:bell",person:"mdi:account",plant:"mdi:flower",proximity:"mdi:apple-safari",remote:"mdi:remote",scene:"mdi:palette",script:"mdi:script-text",select:"mdi:format-list-bulleted",sensor:"mdi:eye",siren:"mdi:bullhorn",simple_alarm:"mdi:bell",sun:"mdi:white-balance-sunny",timer:"mdi:timer-outline",updater:"mdi:cloud-upload",vacuum:"mdi:robot-vacuum",water_heater:"mdi:thermometer",weather:"mdi:weather-cloudy",zone:"mdi:map-marker-radius"};function op(e,t,n){switch(e){case"alarm_control_panel":return Gm(n);case"binary_sensor":return Km(n,t);case"button":switch(t?.attributes.device_class){case"restart":return"mdi:restart";case"update":return"mdi:package-up";default:return"mdi:gesture-tap-button"}case"cover":return Xm(n,t);case"device_tracker":return"router"===t?.attributes.source_type?"home"===n?"mdi:lan-connect":"mdi:lan-disconnect":["bluetooth","bluetooth_le"].includes(t?.attributes.source_type)?"home"===n?"mdi:bluetooth-connect":"mdi:bluetooth":"not_home"===n?"mdi:account-arrow-right":"mdi:account";case"humidifier":return n&&"off"===n?"mdi:air-humidifier-off":"mdi:air-humidifier";case"input_boolean":return"on"===n?"mdi:check-circle-outline":"mdi:close-circle-outline";case"lock":switch(n){case"unlocked":return"mdi:lock-open";case"jammed":return"mdi:lock-alert";case"locking":case"unlocking":return"mdi:lock-clock";default:return"mdi:lock"}case"media_player":switch(t?.attributes.device_class){case"speaker":switch(n){case"playing":return"mdi:speaker-play";case"paused":return"mdi:speaker-pause";case"off":return"mdi:speaker-off";default:return"mdi:speaker"}case"tv":switch(n){case"playing":return"mdi:television-play";case"paused":return"mdi:television-pause";case"off":return"mdi:television-off";default:return"mdi:television"}default:switch(n){case"playing":case"paused":return"mdi:cast-connected";case"off":return"mdi:cast-off";default:return"mdi:cast"}}case"switch":switch(t?.attributes.device_class){case"outlet":return"on"===n?"mdi:power-plug":"mdi:power-plug-off";case"switch":return"on"===n?"mdi:toggle-switch":"mdi:toggle-switch-off";default:return"mdi:flash"}case"zwave":switch(n){case"dead":return"mdi:emoticon-dead";case"sleeping":return"mdi:sleep";case"initializing":return"mdi:timer-sand";default:return"mdi:z-wave"}case"sensor":{const e=ip(t);if(e)return e;break}case"input_datetime":if(!t?.attributes.has_date)return"mdi:clock";if(!t.attributes.has_time)return"mdi:calendar";break;case"sun":return"above_horizon"===t?.state?rp[e]:"mdi:weather-night";case"update":return"on"===t?.state?Qm(t)?"mdi:package-down":"mdi:package-up":"mdi:package"}return e in rp?rp[e]:(console.warn(`Unable to find icon for domain: ${e}`),ap)}function sp(e){if(!e)return ap;if(e.attributes.icon)return e.attributes.icon;return op(s(e.entity_id),e,e.state)}async function cp(e,t,n,i=!1){let a;try{a=await e.callWS(n)}catch(e){if(!(e instanceof Error))throw new vu(Pm("error.failed_response"),{request:n,response:e});throw e}if(!a)throw new vu(Pm("error.empty_response"),{request:n});const r=i?t.safeParse(JSON.parse(a)):t.safeParse(a);if(!r.success)throw new vu(Pm("error.invalid_response"),{request:n,response:a,invalid_keys:au(r.error)});return r.data}async function lp(e,t,n){const i={type:"auth/sign_path",path:t,expires:n},a=await cp(e,dm,i);return a?e.hassUrl(a.path):null}function dp(e,t,n,i){if(!e||!n||!n.length)return[];const a=[];for(const r of n){const n=t?.states[r],o=e.states[r];if((i?.stateOnly&&n?.state!==o?.state||!i?.stateOnly&&n!==o)&&(a.push({entity:r,oldState:n,newState:o}),i?.firstOnly))break}return a}function up(e,t,n,i){return!!dp(e,t,n,{...i,firstOnly:!0}).length}function hp(e){if("off"===e.state||!e.attributes.brightness)return"";return`brightness(${(e.attributes.brightness+245)/5}%)`}function mp(e){return"off"===e.state?"":e.attributes.rgb_color?`rgb(${e.attributes.rgb_color.join(",")})`:""}function pp(e){return{color:mp(e),filter:hp(e)}}const fp=e=>{const t=e.entity_id.split(".")[0];let n=e.state;return"climate"===t&&(n=e.attributes.hvac_action),n};function gp(e,t){if(!t.entity)return t;const n=e.states[t.entity];n&&t.state_color&&(t.style={...pp(n),...t.style}),t.title=t.title??(n?.attributes?.friendly_name||t.entity),t.icon=t.icon??sp(n);const i=n?function(e){return s(e.entity_id)}(n):void 0;return t.data_domain=t.state_color||"light"===i&&!1!==t.state_color?i:void 0,n&&(t.data_state=fp(n)),t}function vp(e,t){return t?e?.states[t]?.attributes?.friendly_name:void 0}function _p(e,t){return sp(t?e?.states[t]:null)}const yp=async()=>{if(["ha-selector","ha-menu-button","ha-camera-stream","ha-hls-player","ha-web-rtc-player","ha-icon","ha-circular-progress","ha-icon-button","ha-card","ha-svg-icon","ha-button-menu"].every((e=>customElements.get(e))))return!0;const e=await window.loadCardHelpers(),t=await e.createCardElement({type:"picture-glance",entities:[],camera_image:"dummy-to-load-editor-components"});return!!t.constructor.getConfigElement&&(await t.constructor.getConfigElement(),!0)},bp=e=>!!e&&["on","open"].includes(e.state),wp=(e,t)=>{if(!e)return[];const n=Object.keys(e.states).filter((e=>!t||e.substr(0,e.indexOf("."))===t));return n.sort(),n};function xp(e,t){return e&&t&&t.startsWith("/")?e.hassUrl(t):t??null}var Cp=6e4,$p=36e5,kp=1e3;function Ep(e,t){return Fm(2,arguments),Zm(e).getTime()-Zm(t).getTime()}var Mp={ceil:Math.ceil,round:Math.round,floor:Math.floor,trunc:function(e){return e<0?Math.ceil(e):Math.floor(e)}},Sp="trunc";function Tp(e){return e?Mp[e]:Mp[Sp]}function Ap(e,t,n){Fm(2,arguments);var i=Ep(e,t)/1e3;return Tp(null==n?void 0:n.roundingMethod)(i)}function zp(e){return zp="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},zp(e)}function jp(e){if(Fm(1,arguments),!function(e){return Fm(1,arguments),e instanceof Date||"object"===zp(e)&&"[object Date]"===Object.prototype.toString.call(e)}(e)&&"number"!=typeof e)return!1;var t=Zm(e);return!isNaN(Number(t))}function Op(e,t){return Fm(2,arguments),function(e,t){Fm(2,arguments);var n=Zm(e).getTime(),i=Um(t);return new Date(n+i)}(e,-Um(t))}var Ip=864e5;function Rp(e){Fm(1,arguments);var t=Zm(e),n=t.getUTCDay(),i=(n<1?7:0)+n-1;return t.setUTCDate(t.getUTCDate()-i),t.setUTCHours(0,0,0,0),t}function Dp(e){Fm(1,arguments);var t=Zm(e),n=t.getUTCFullYear(),i=new Date(0);i.setUTCFullYear(n+1,0,4),i.setUTCHours(0,0,0,0);var a=Rp(i),r=new Date(0);r.setUTCFullYear(n,0,4),r.setUTCHours(0,0,0,0);var o=Rp(r);return t.getTime()>=a.getTime()?n+1:t.getTime()>=o.getTime()?n:n-1}var Pp=6048e5;function Lp(e){Fm(1,arguments);var t=Zm(e),n=Rp(t).getTime()-function(e){Fm(1,arguments);var t=Dp(e),n=new Date(0);return n.setUTCFullYear(t,0,4),n.setUTCHours(0,0,0,0),Rp(n)}(t).getTime();return Math.round(n/Pp)+1}var Np={};function Up(){return Np}function Fp(e,t){var n,i,a,r,o,s,c,l;Fm(1,arguments);var d=Up(),u=Um(null!==(n=null!==(i=null!==(a=null!==(r=null==t?void 0:t.weekStartsOn)&&void 0!==r?r:null==t||null===(o=t.locale)||void 0===o||null===(s=o.options)||void 0===s?void 0:s.weekStartsOn)&&void 0!==a?a:d.weekStartsOn)&&void 0!==i?i:null===(c=d.locale)||void 0===c||null===(l=c.options)||void 0===l?void 0:l.weekStartsOn)&&void 0!==n?n:0);if(!(u>=0&&u<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var h=Zm(e),m=h.getUTCDay(),p=(m=1&&m<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var p=new Date(0);p.setUTCFullYear(u+1,0,m),p.setUTCHours(0,0,0,0);var f=Fp(p,t),g=new Date(0);g.setUTCFullYear(u,0,m),g.setUTCHours(0,0,0,0);var v=Fp(g,t);return d.getTime()>=f.getTime()?u+1:d.getTime()>=v.getTime()?u:u-1}var Zp=6048e5;function qp(e,t){Fm(1,arguments);var n=Zm(e),i=Fp(n,t).getTime()-function(e,t){var n,i,a,r,o,s,c,l;Fm(1,arguments);var d=Up(),u=Um(null!==(n=null!==(i=null!==(a=null!==(r=null==t?void 0:t.firstWeekContainsDate)&&void 0!==r?r:null==t||null===(o=t.locale)||void 0===o||null===(s=o.options)||void 0===s?void 0:s.firstWeekContainsDate)&&void 0!==a?a:d.firstWeekContainsDate)&&void 0!==i?i:null===(c=d.locale)||void 0===c||null===(l=c.options)||void 0===l?void 0:l.firstWeekContainsDate)&&void 0!==n?n:1),h=Hp(e,t),m=new Date(0);return m.setUTCFullYear(h,0,u),m.setUTCHours(0,0,0,0),Fp(m,t)}(n,t).getTime();return Math.round(i/Zp)+1}function Vp(e,t){for(var n=e<0?"-":"",i=Math.abs(e).toString();i.length0?n:1-n;return Vp("yy"===t?i%100:i,t.length)},Bp=function(e,t){var n=e.getUTCMonth();return"M"===t?String(n+1):Vp(n+1,2)},Yp=function(e,t){return Vp(e.getUTCDate(),t.length)},Qp=function(e,t){return Vp(e.getUTCHours()%12||12,t.length)},Gp=function(e,t){return Vp(e.getUTCHours(),t.length)},Kp=function(e,t){return Vp(e.getUTCMinutes(),t.length)},Xp=function(e,t){return Vp(e.getUTCSeconds(),t.length)},Jp=function(e,t){var n=t.length,i=e.getUTCMilliseconds();return Vp(Math.floor(i*Math.pow(10,n-3)),t.length)},ef="midnight",tf="noon",nf="morning",af="afternoon",rf="evening",of="night",sf={G:function(e,t,n){var i=e.getUTCFullYear()>0?1:0;switch(t){case"G":case"GG":case"GGG":return n.era(i,{width:"abbreviated"});case"GGGGG":return n.era(i,{width:"narrow"});default:return n.era(i,{width:"wide"})}},y:function(e,t,n){if("yo"===t){var i=e.getUTCFullYear(),a=i>0?i:1-i;return n.ordinalNumber(a,{unit:"year"})}return Wp(e,t)},Y:function(e,t,n,i){var a=Hp(e,i),r=a>0?a:1-a;return"YY"===t?Vp(r%100,2):"Yo"===t?n.ordinalNumber(r,{unit:"year"}):Vp(r,t.length)},R:function(e,t){return Vp(Dp(e),t.length)},u:function(e,t){return Vp(e.getUTCFullYear(),t.length)},Q:function(e,t,n){var i=Math.ceil((e.getUTCMonth()+1)/3);switch(t){case"Q":return String(i);case"QQ":return Vp(i,2);case"Qo":return n.ordinalNumber(i,{unit:"quarter"});case"QQQ":return n.quarter(i,{width:"abbreviated",context:"formatting"});case"QQQQQ":return n.quarter(i,{width:"narrow",context:"formatting"});default:return n.quarter(i,{width:"wide",context:"formatting"})}},q:function(e,t,n){var i=Math.ceil((e.getUTCMonth()+1)/3);switch(t){case"q":return String(i);case"qq":return Vp(i,2);case"qo":return n.ordinalNumber(i,{unit:"quarter"});case"qqq":return n.quarter(i,{width:"abbreviated",context:"standalone"});case"qqqqq":return n.quarter(i,{width:"narrow",context:"standalone"});default:return n.quarter(i,{width:"wide",context:"standalone"})}},M:function(e,t,n){var i=e.getUTCMonth();switch(t){case"M":case"MM":return Bp(e,t);case"Mo":return n.ordinalNumber(i+1,{unit:"month"});case"MMM":return n.month(i,{width:"abbreviated",context:"formatting"});case"MMMMM":return n.month(i,{width:"narrow",context:"formatting"});default:return n.month(i,{width:"wide",context:"formatting"})}},L:function(e,t,n){var i=e.getUTCMonth();switch(t){case"L":return String(i+1);case"LL":return Vp(i+1,2);case"Lo":return n.ordinalNumber(i+1,{unit:"month"});case"LLL":return n.month(i,{width:"abbreviated",context:"standalone"});case"LLLLL":return n.month(i,{width:"narrow",context:"standalone"});default:return n.month(i,{width:"wide",context:"standalone"})}},w:function(e,t,n,i){var a=qp(e,i);return"wo"===t?n.ordinalNumber(a,{unit:"week"}):Vp(a,t.length)},I:function(e,t,n){var i=Lp(e);return"Io"===t?n.ordinalNumber(i,{unit:"week"}):Vp(i,t.length)},d:function(e,t,n){return"do"===t?n.ordinalNumber(e.getUTCDate(),{unit:"date"}):Yp(e,t)},D:function(e,t,n){var i=function(e){Fm(1,arguments);var t=Zm(e),n=t.getTime();t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0);var i=n-t.getTime();return Math.floor(i/Ip)+1}(e);return"Do"===t?n.ordinalNumber(i,{unit:"dayOfYear"}):Vp(i,t.length)},E:function(e,t,n){var i=e.getUTCDay();switch(t){case"E":case"EE":case"EEE":return n.day(i,{width:"abbreviated",context:"formatting"});case"EEEEE":return n.day(i,{width:"narrow",context:"formatting"});case"EEEEEE":return n.day(i,{width:"short",context:"formatting"});default:return n.day(i,{width:"wide",context:"formatting"})}},e:function(e,t,n,i){var a=e.getUTCDay(),r=(a-i.weekStartsOn+8)%7||7;switch(t){case"e":return String(r);case"ee":return Vp(r,2);case"eo":return n.ordinalNumber(r,{unit:"day"});case"eee":return n.day(a,{width:"abbreviated",context:"formatting"});case"eeeee":return n.day(a,{width:"narrow",context:"formatting"});case"eeeeee":return n.day(a,{width:"short",context:"formatting"});default:return n.day(a,{width:"wide",context:"formatting"})}},c:function(e,t,n,i){var a=e.getUTCDay(),r=(a-i.weekStartsOn+8)%7||7;switch(t){case"c":return String(r);case"cc":return Vp(r,t.length);case"co":return n.ordinalNumber(r,{unit:"day"});case"ccc":return n.day(a,{width:"abbreviated",context:"standalone"});case"ccccc":return n.day(a,{width:"narrow",context:"standalone"});case"cccccc":return n.day(a,{width:"short",context:"standalone"});default:return n.day(a,{width:"wide",context:"standalone"})}},i:function(e,t,n){var i=e.getUTCDay(),a=0===i?7:i;switch(t){case"i":return String(a);case"ii":return Vp(a,t.length);case"io":return n.ordinalNumber(a,{unit:"day"});case"iii":return n.day(i,{width:"abbreviated",context:"formatting"});case"iiiii":return n.day(i,{width:"narrow",context:"formatting"});case"iiiiii":return n.day(i,{width:"short",context:"formatting"});default:return n.day(i,{width:"wide",context:"formatting"})}},a:function(e,t,n){var i=e.getUTCHours()/12>=1?"pm":"am";switch(t){case"a":case"aa":return n.dayPeriod(i,{width:"abbreviated",context:"formatting"});case"aaa":return n.dayPeriod(i,{width:"abbreviated",context:"formatting"}).toLowerCase();case"aaaaa":return n.dayPeriod(i,{width:"narrow",context:"formatting"});default:return n.dayPeriod(i,{width:"wide",context:"formatting"})}},b:function(e,t,n){var i,a=e.getUTCHours();switch(i=12===a?tf:0===a?ef:a/12>=1?"pm":"am",t){case"b":case"bb":return n.dayPeriod(i,{width:"abbreviated",context:"formatting"});case"bbb":return n.dayPeriod(i,{width:"abbreviated",context:"formatting"}).toLowerCase();case"bbbbb":return n.dayPeriod(i,{width:"narrow",context:"formatting"});default:return n.dayPeriod(i,{width:"wide",context:"formatting"})}},B:function(e,t,n){var i,a=e.getUTCHours();switch(i=a>=17?rf:a>=12?af:a>=4?nf:of,t){case"B":case"BB":case"BBB":return n.dayPeriod(i,{width:"abbreviated",context:"formatting"});case"BBBBB":return n.dayPeriod(i,{width:"narrow",context:"formatting"});default:return n.dayPeriod(i,{width:"wide",context:"formatting"})}},h:function(e,t,n){if("ho"===t){var i=e.getUTCHours()%12;return 0===i&&(i=12),n.ordinalNumber(i,{unit:"hour"})}return Qp(e,t)},H:function(e,t,n){return"Ho"===t?n.ordinalNumber(e.getUTCHours(),{unit:"hour"}):Gp(e,t)},K:function(e,t,n){var i=e.getUTCHours()%12;return"Ko"===t?n.ordinalNumber(i,{unit:"hour"}):Vp(i,t.length)},k:function(e,t,n){var i=e.getUTCHours();return 0===i&&(i=24),"ko"===t?n.ordinalNumber(i,{unit:"hour"}):Vp(i,t.length)},m:function(e,t,n){return"mo"===t?n.ordinalNumber(e.getUTCMinutes(),{unit:"minute"}):Kp(e,t)},s:function(e,t,n){return"so"===t?n.ordinalNumber(e.getUTCSeconds(),{unit:"second"}):Xp(e,t)},S:function(e,t){return Jp(e,t)},X:function(e,t,n,i){var a=(i._originalDate||e).getTimezoneOffset();if(0===a)return"Z";switch(t){case"X":return lf(a);case"XXXX":case"XX":return df(a);default:return df(a,":")}},x:function(e,t,n,i){var a=(i._originalDate||e).getTimezoneOffset();switch(t){case"x":return lf(a);case"xxxx":case"xx":return df(a);default:return df(a,":")}},O:function(e,t,n,i){var a=(i._originalDate||e).getTimezoneOffset();switch(t){case"O":case"OO":case"OOO":return"GMT"+cf(a,":");default:return"GMT"+df(a,":")}},z:function(e,t,n,i){var a=(i._originalDate||e).getTimezoneOffset();switch(t){case"z":case"zz":case"zzz":return"GMT"+cf(a,":");default:return"GMT"+df(a,":")}},t:function(e,t,n,i){var a=i._originalDate||e;return Vp(Math.floor(a.getTime()/1e3),t.length)},T:function(e,t,n,i){return Vp((i._originalDate||e).getTime(),t.length)}};function cf(e,t){var n=e>0?"-":"+",i=Math.abs(e),a=Math.floor(i/60),r=i%60;if(0===r)return n+String(a);var o=t||"";return n+String(a)+o+Vp(r,2)}function lf(e,t){return e%60==0?(e>0?"-":"+")+Vp(Math.abs(e)/60,2):df(e,t)}function df(e,t){var n=t||"",i=e>0?"-":"+",a=Math.abs(e);return i+Vp(Math.floor(a/60),2)+n+Vp(a%60,2)}var uf=function(e,t){switch(e){case"P":return t.date({width:"short"});case"PP":return t.date({width:"medium"});case"PPP":return t.date({width:"long"});default:return t.date({width:"full"})}},hf=function(e,t){switch(e){case"p":return t.time({width:"short"});case"pp":return t.time({width:"medium"});case"ppp":return t.time({width:"long"});default:return t.time({width:"full"})}},mf={p:hf,P:function(e,t){var n,i=e.match(/(P+)(p+)?/)||[],a=i[1],r=i[2];if(!r)return uf(e,t);switch(a){case"P":n=t.dateTime({width:"short"});break;case"PP":n=t.dateTime({width:"medium"});break;case"PPP":n=t.dateTime({width:"long"});break;default:n=t.dateTime({width:"full"})}return n.replace("{{date}}",uf(a,t)).replace("{{time}}",hf(r,t))}};function pf(e){var t=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()));return t.setUTCFullYear(e.getFullYear()),e.getTime()-t.getTime()}var ff=["D","DD"],gf=["YY","YYYY"];function vf(e){return-1!==ff.indexOf(e)}function _f(e){return-1!==gf.indexOf(e)}function yf(e,t,n){if("YYYY"===e)throw new RangeError("Use `yyyy` instead of `YYYY` (in `".concat(t,"`) for formatting years to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("YY"===e)throw new RangeError("Use `yy` instead of `YY` (in `".concat(t,"`) for formatting years to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("D"===e)throw new RangeError("Use `d` instead of `D` (in `".concat(t,"`) for formatting days of the month to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("DD"===e)throw new RangeError("Use `dd` instead of `DD` (in `".concat(t,"`) for formatting days of the month to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"))}var bf={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function wf(e){return function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.width?String(t.width):e.defaultWidth;return e.formats[n]||e.formats[e.defaultWidth]}}var xf={date:wf({formats:{full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},defaultWidth:"full"}),time:wf({formats:{full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},defaultWidth:"full"}),dateTime:wf({formats:{full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},defaultWidth:"full"})},Cf={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"};function $f(e){return function(t,n){var i;if("formatting"===(null!=n&&n.context?String(n.context):"standalone")&&e.formattingValues){var a=e.defaultFormattingWidth||e.defaultWidth,r=null!=n&&n.width?String(n.width):a;i=e.formattingValues[r]||e.formattingValues[a]}else{var o=e.defaultWidth,s=null!=n&&n.width?String(n.width):e.defaultWidth;i=e.values[s]||e.values[o]}return i[e.argumentCallback?e.argumentCallback(t):t]}}function kf(e){return function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.width,a=i&&e.matchPatterns[i]||e.matchPatterns[e.defaultMatchWidth],r=t.match(a);if(!r)return null;var o,s=r[0],c=i&&e.parsePatterns[i]||e.parsePatterns[e.defaultParseWidth],l=Array.isArray(c)?function(e,t){for(var n=0;n0?"in "+i:i+" ago":i},formatLong:xf,formatRelative:function(e,t,n,i){return Cf[e]},localize:{ordinalNumber:function(e,t){var n=Number(e),i=n%100;if(i>20||i<10)switch(i%10){case 1:return n+"st";case 2:return n+"nd";case 3:return n+"rd"}return n+"th"},era:$f({values:{narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},defaultWidth:"wide"}),quarter:$f({values:{narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},defaultWidth:"wide",argumentCallback:function(e){return e-1}}),month:$f({values:{narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},defaultWidth:"wide"}),day:$f({values:{narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},defaultWidth:"wide"}),dayPeriod:$f({values:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},defaultWidth:"wide",formattingValues:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}},defaultFormattingWidth:"wide"})},match:{ordinalNumber:(Ef={matchPattern:/^(\d+)(th|st|nd|rd)?/i,parsePattern:/\d+/i,valueCallback:function(e){return parseInt(e,10)}},function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=e.match(Ef.matchPattern);if(!n)return null;var i=n[0],a=e.match(Ef.parsePattern);if(!a)return null;var r=Ef.valueCallback?Ef.valueCallback(a[0]):a[0];return{value:r=t.valueCallback?t.valueCallback(r):r,rest:e.slice(i.length)}}),era:kf({matchPatterns:{narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},defaultMatchWidth:"wide",parsePatterns:{any:[/^b/i,/^(a|c)/i]},defaultParseWidth:"any"}),quarter:kf({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},defaultMatchWidth:"wide",parsePatterns:{any:[/1/i,/2/i,/3/i,/4/i]},defaultParseWidth:"any",valueCallback:function(e){return e+1}}),month:kf({matchPatterns:{narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},defaultParseWidth:"any"}),day:kf({matchPatterns:{narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},defaultParseWidth:"any"}),dayPeriod:kf({matchPatterns:{narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},defaultMatchWidth:"any",parsePatterns:{any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},defaultParseWidth:"any"})},options:{weekStartsOn:0,firstWeekContainsDate:1}},Sf=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,Tf=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,Af=/^'([^]*?)'?$/,zf=/''/g,jf=/[a-zA-Z]/;function Of(e,t,n){var i,a,r,o,s,c,l,d,u,h,m,p,f,g,v,_,y,b;Fm(2,arguments);var w=String(t),x=Up(),C=null!==(i=null!==(a=null==n?void 0:n.locale)&&void 0!==a?a:x.locale)&&void 0!==i?i:Mf,$=Um(null!==(r=null!==(o=null!==(s=null!==(c=null==n?void 0:n.firstWeekContainsDate)&&void 0!==c?c:null==n||null===(l=n.locale)||void 0===l||null===(d=l.options)||void 0===d?void 0:d.firstWeekContainsDate)&&void 0!==s?s:x.firstWeekContainsDate)&&void 0!==o?o:null===(u=x.locale)||void 0===u||null===(h=u.options)||void 0===h?void 0:h.firstWeekContainsDate)&&void 0!==r?r:1);if(!($>=1&&$<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var k=Um(null!==(m=null!==(p=null!==(f=null!==(g=null==n?void 0:n.weekStartsOn)&&void 0!==g?g:null==n||null===(v=n.locale)||void 0===v||null===(_=v.options)||void 0===_?void 0:_.weekStartsOn)&&void 0!==f?f:x.weekStartsOn)&&void 0!==p?p:null===(y=x.locale)||void 0===y||null===(b=y.options)||void 0===b?void 0:b.weekStartsOn)&&void 0!==m?m:0);if(!(k>=0&&k<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(!C.localize)throw new RangeError("locale must contain localize property");if(!C.formatLong)throw new RangeError("locale must contain formatLong property");var E=Zm(e);if(!jp(E))throw new RangeError("Invalid time value");var M=Op(E,pf(E)),S={firstWeekContainsDate:$,weekStartsOn:k,locale:C,_originalDate:E};return w.match(Tf).map((function(e){var t=e[0];return"p"===t||"P"===t?(0,mf[t])(e,C.formatLong):e})).join("").match(Sf).map((function(i){if("''"===i)return"'";var a=i[0];if("'"===a)return function(e){var t=e.match(Af);if(!t)return e;return t[1].replace(zf,"'")}(i);var r=sf[a];if(r)return null!=n&&n.useAdditionalWeekYearTokens||!_f(i)||yf(i,t,String(e)),null!=n&&n.useAdditionalDayOfYearTokens||!vf(i)||yf(i,t,String(e)),r(M,i,C.localize,S);if(a.match(jf))throw new RangeError("Format string contains an unescaped latin alphabet character `"+a+"`");return i})).join("")}function If(e,t,n){e.dispatchEvent(new CustomEvent(`frigate-card:${t}`,{bubbles:!0,composed:!0,detail:n}))}function Rf(e){if(!e)return;return e.split(/[_\s]+/).map((e=>e[0].toUpperCase()+e.substring(1))).join(" ")}function Df(e,t,n){const i=e[t];return e.splice(t,1),e.splice(n,0,i),e}const Pf=e=>Array.isArray(e)?e:[e];function Lf(e,t){return!Aa(e,t)}function Nf(e,t=console.warn){e instanceof vu&&e.context?t(e,e.context):t(e)}const Uf=()=>window.matchMedia("(hover: hover) and (pointer: fine)").matches,Ff=(e,t)=>Of(e,"yyyy-MM-dd HH:mm"+(t?":ss":"")),Hf=e=>Of(e,"yyyy-MM-dd"),Zf=(e,t)=>{window.requestIdleCallback?window.requestIdleCallback(e,{...t&&{timeout:t}}):e()};function qf(e,t){const n=function(e,t,n){Fm(2,arguments);var i=Ep(e,t)/$p;return Tp(null==n?void 0:n.roundingMethod)(i)}(t,e),i=function(e,t,n){Fm(2,arguments);var i=Ep(e,t)/Cp;return Tp(null==n?void 0:n.roundingMethod)(i)}(t,e)-60*n;let a="";return n&&(a+=`${n}h `),i&&(a+=`${i}m `),a+=`${Ap(t,e)-60*n*60-60*i}s`,a}const Vf=async(e,t)=>await Promise.all(Array.from(e).map((e=>t(e)))),Wf=e=>new Date(`${e}T00:00:00`),Bf=async e=>{await new Promise((t=>setTimeout(t,1e3*e)))},Yf=e=>!isNaN(e.getTime()),Qf=(e,t,n,i)=>{t?e.setAttribute(n,i??""):e.removeAttribute(n)},Gf=Ws.lazy((()=>Ws.object({title:Ws.string(),media_class:Ws.string(),media_content_type:Ws.string(),media_content_id:Ws.string(),can_play:Ws.boolean(),can_expand:Ws.boolean(),children_media_class:Ws.string().nullable().optional(),thumbnail:Ws.string().nullable(),children:Ws.array(Gf).nullable().optional()}))),Kf="video",Xf="image",Jf=60;class eg{constructor(e){this._cache=e}async walkBrowseMedias(e,t,n){return t&&t.length?(await Vf(t,(async t=>await this._walkBrowseMedia(e,t,n)))).flat():[]}async _walkBrowseMedia(e,t,n){const i=await Vf(t.targets,(async i=>await this._browseMedia(e,i,{useCache:n?.useCache,metadataGenerator:t.metadataGenerator}))),a=[];for(const e of i)for(const n of e.children??[])t.matcher(n)&&a.push(n);const r=t.advance?t.advance(a):null;if(!r||!r.length)return a;const o=new Set(r.map((e=>e.targets)).flat()),s=[];for(const e of a)o.has(e)||s.push(e);const c=await this.walkBrowseMedias(e,r,n);return s.concat(c)}async _browseMedia(e,t,n){const i="object"==typeof t?t.media_content_id:t,a=n?.useCache??1?this._cache.get(i):null;if(a)return a;const r={type:"media_source/browse_media",media_content_id:i},o=await cp(e,Gf,r);if(n?.metadataGenerator)for(const e of o.children??[])e._metadata=n.metadataGenerator(e,"object"==typeof t?t:void 0)??void 0;return(n?.useCache??1)&&this._cache.set(i,o,Bm(new Date,{seconds:60})),o}}function tg(e,t){for(var n=-1,i=null==e?0:e.length,a=Array(i);++nt||r&&o&&c&&!s&&!l||i&&o&&c||!n&&c||!a)return 1;if(!i&&!r&&!l&&e=s?c:c*("desc"==n[i]?-1:1)}return e.index-t.index}(e,t,n)}))}function Rg(e,t,n,i){return null==e?[]:(Yt(t)||(t=null==t?[]:[t]),Yt(n=i?void 0:n)||(n=null==n?[]:[n]),Ig(e,t,n))}function Dg(e,t){return e&&e.length?function(e,t){for(var n=-1,i=e.length,a=0,r=[];++nNg(t,e)))}add(e){this._ranges.push(e),this._ranges=Fg(this._ranges)}clear(){this._ranges=[]}}class Lg{constructor(e){this._ranges=e??[]}hasCoverage(e){const t=new Date;return this._ranges.some((n=>tet.start>=e.start&&t.end<=e.end,Ug=(e,t)=>e.start>=t.start&&e.start<=t.end||e.end>=t.start&&e.end<=t.end||e.start<=t.start&&e.end>=t.end,Fg=(e,t=0)=>{const n=[];e=Rg(e,(e=>e.start),"asc");let i=null;for(let a=0;a=o?r.end>i.end&&(i.end=r.end):(n.push(i),i={...r})}return i&&n.push(i),n};class Hg{constructor(){this._data=[]}get(e){const t=new Date;for(const n of this._data)if((!n.expires||t<=n.expires)&&this._contains(e,n.request))return n.response;return null}clear(){this._data=[]}has(e){return!!this.get(e)}set(e,t,n){this._data.push({request:e,response:t,expires:n}),this._expireOldRequests()}_contains(e,t){return Aa(e,t)}_expireOldRequests(){const e=new Date;this._data=this._data.filter((t=>!t.expires||e=e.start.getTime()){if(i>e.end.getTime())break;t.push(n)}}return t}getSize(){return this._data.length}expireMatches(e){this._data=this._data.filter((t=>!e(t)))}}class Vg{constructor(){this._segments=new Map}add(e,t,n){let i=this._segments.get(e);i||(i=new qg((e=>1e3*e.start_time),(e=>e.id)),this._segments.set(e,i)),i.add(t,n)}clear(){this._segments.clear()}hasCoverage(e,t){return!!this._segments.get(e)?.hasCoverage(t)}get(e,t){return this._segments.get(e)?.get(t)??null}getSize(e){return this._segments.get(e)?.getSize()??null}getCameraIDs(){return[...this._segments.keys()]}expireMatches(e,t){this._segments.get(e)?.expireMatches(t)}}class Wg extends vu{}var Bg,Yg,Qg;function Gg(e){Fm(1,arguments);var t=Zm(e);return t.setMinutes(0,0,0),t}function Kg(e){Fm(1,arguments);var t=Zm(e);return t.setMinutes(59,59,999),t}function Xg(e){Fm(1,arguments);var t=Zm(e);return t.setHours(0,0,0,0),t}function Jg(e){Fm(1,arguments);var t=Zm(e);return t.setHours(23,59,59,999),t}function ev(e){return e!=e}function tv(e,t){return!!(null==e?0:e.length)&&function(e,t,n){return t==t?function(e,t,n){for(var i=n-1,a=e.length;++i-1}function nv(e,t,n){for(var i=-1,a=null==e?0:e.length;++i=av){var l=t?null:iv(e);if(l)return ra(l);o=!1,a=ea,c=new Xi}else c=t?[]:s;e:for(;++i{let n,i;return(e.end.getTime()-e.start.getTime())/1e3<=3600?(n=Gg(e.start),i=Kg(e.end)):(n=Xg(e.start),i=Jg(e.end)),t?.endCap&&(i=function(e){Fm(1,arguments);var t=Zm(e);return t.setSeconds(59,999),t}(sv(i))),{start:n,end:i}},sv=e=>{const t=new Date;return e>t?t:e},cv=e=>{return Rg((n=e=>e.getID()??e,(t=e)&&t.length?rv(t,zg(n)):[]),(e=>e.getStartTime()),"asc");var t,n},lv=e=>e.camera_entity??e.webrtc_card?.entity??null;class dv{constructor(e,t,n){this._entityRegistryManager=e,this._cardWideConfig=n,this._resolvedMediaCache=t}async createEngine(e){let t=null;switch(e){case Qg.Generic:const{GenericCameraManagerEngine:e}=await import("./engine-generic-395b8c68.js");t=new e;break;case Qg.Frigate:const{FrigateCameraManagerEngine:n}=await import("./engine-frigate-2c5e3aa9.js");t=new n(this._cardWideConfig,new Vg,new Zg);break;case Qg.MotionEye:const{MotionEyeCameraManagerEngine:i}=await import("./engine-motioneye-ae70fe08.js");t=new i(new eg(new Hg),this._resolvedMediaCache,new Zg)}return t}async getEngineForCamera(e,t){let n=null;if("frigate"===t.engine)n=Qg.Frigate;else if("motioneye"===t.engine)n=Qg.MotionEye;else if("generic"===t.engine)n=Qg.Generic;else if("auto"===t.engine){const i=lv(t);if(i){let a;try{a=await this._entityRegistryManager.getEntity(e,i)}catch(n){if(e.states[i])return Qg.Generic;throw new Wg(Pm("error.no_camera_entity"),t)}switch(a?.platform){case"frigate":n=Qg.Frigate;break;case"motioneye":n=Qg.MotionEye;break;default:n=Qg.Generic}}else t.frigate.camera_name&&(n=Qg.Frigate)}return n}}function uv(e){return e&&e.length?function(e,t){for(var n,i=-1,a=e.length;++i{e?.debug?.logging&&console.debug(...t)};function mv(e){return"string"==typeof e?.id&&e.id||"string"==typeof e?.camera_entity&&e.camera_entity||"object"==typeof e?.webrtc_card&&e.webrtc_card&&"string"==typeof e.webrtc_card.entity&&e.webrtc_card.entity||"object"==typeof e?.frigate&&e.frigate&&"string"==typeof e?.frigate.camera_name&&e.frigate.camera_name||""}function pv(e,t){if(!e||!t)return null;const n=e.getStore().getCameras(),i=new Set,a=e=>{const t=n.get(e);if(t){i.add(e);const r=new Set;t.dependencies.cameras.forEach((e=>r.add(e))),t.dependencies.all_cameras&&n.forEach(((e,t)=>r.add(t)));for(const e of r)i.has(e)||a(e)}};return t&&a(t),i}class fv{constructor(){this._allConfigs=new Map,this._visibleConfigs=new Map,this._enginesByCamera=new Map,this._enginesByType=new Map}addCamera(e,t,n){t.hide||this._visibleConfigs.set(e,t),this._allConfigs.set(e,t),this._enginesByCamera.set(e,n),this._enginesByType.set(n.getEngineType(),n)}getCameraConfig(e){return this._allConfigs.get(e)??null}hasCameraID(e){return this._allConfigs.has(e)}hasVisibleCameraID(e){return this._visibleConfigs.has(e)}getCameraCount(){return this._allConfigs.size}getVisibleCameraCount(){return this._visibleConfigs.size}getCameras(){return this._allConfigs}getVisibleCameras(){return this._visibleConfigs}getCameraIDs(){return new Set(this._allConfigs.keys())}getVisibleCameraIDs(){return new Set(this._visibleConfigs.keys())}getCameraConfigForMedia(e){const t=e.getCameraID();return t?this.getCameraConfig(t):null}getEngineOfType(e){return this._enginesByType.get(e)??null}getEngineForCameraID(e){return this._enginesByCamera.get(e)??null}getEnginesForCameraIDs(e){const t=new Map;for(const n of e){const e=this.getEngineForCameraID(n);e&&(t.has(e)||t.set(e,new Set),t.get(e)?.add(n))}return t.size?t:null}getEngineForMedia(e){const t=e.getCameraID();return t?this.getEngineForCameraID(t):null}getAllEngines(){return[...this._enginesByType.values()]}}class gv{static isEventQuery(e){return e.type===Bg.Event}static isRecordingQuery(e){return e.type===Bg.Recording}static isRecordingSegmentsQuery(e){return e.type===Bg.RecordingSegments}static isMediaMetadataQuery(e){return e.type===Bg.MediaMetadata}}class vv{static isEventQueryResult(e){return e.type===Yg.Event}static isRecordingQuery(e){return e.type===Yg.Recording}static isRecordingSegmentsQuery(e){return e.type===Yg.RecordingSegments}static isMediaMetadataQuery(e){return e.type===Yg.MediaMetadata}}class _v{constructor(e,t){this._engineFactory=e,this._cardWideConfig=t,this._store=new fv}async _getEnginesForCameras(e,t){const n=new Map,i=new Map,a=await(async t=>await Vf(t,(t=>this._engineFactory.getEngineForCamera(e,t))))(t);for(const[e,r]of t.entries()){const t=a[e],o=t?i.get(t)??await this._engineFactory.createEngine(t):null;if(!o||!t)throw new Wg(Pm("error.no_camera_engine"),r);i.set(t,o),n.set(r,o)}return n}async _initializeCamera(e,t,n,i){return{inputConfig:i,initializedConfig:await t.initializeCamera(e,n,Gi(i)),engine:t}}async initializeCameras(e,t,n){const i=new Date;n.some((e=>(e=>e.triggers.motion||e.triggers.occupancy)(e)))&&await t.fetchEntityList(e);const a=await this._getEnginesForCameras(e,n);if((await Vf(a.entries(),(async([n,i])=>await this._initializeCamera(e,i,t,n)))).forEach((e=>{const t=mv(e.initializedConfig);if(!t)throw new Wg(Pm("error.no_camera_id"),e.inputConfig);if(this._store.hasCameraID(t))throw new Wg(Pm("error.duplicate_camera_id"),e.inputConfig);this._store.addCamera(t,e.initializedConfig,e.engine)})),!this._store.getVisibleCameraCount())throw new Wg(Pm("error.no_visible_cameras"));hv(this._cardWideConfig,"Frigate Card CameraManager initialized (Cameras: ",this._store.getCameras(),`, Duration: ${((new Date).getTime()-i.getTime())/1e3}s,`,")")}isInitialized(){return this._store.getCameraCount()>0}getStore(){return this._store}generateDefaultEventQueries(e,t){return this._generateDefaultQueries(e,{type:Bg.Event,...t})}generateDefaultRecordingQueries(e,t){return this._generateDefaultQueries(e,{type:Bg.Recording,...t})}generateDefaultRecordingSegmentsQueries(e,t){return this._generateDefaultQueries(e,{type:Bg.RecordingSegments,...t})}async getMediaMetadata(e){const t=new Set,n=new Set,i=new Set,a=new Set,r={type:Bg.MediaMetadata,cameraIDs:this._store.getCameraIDs()},o=await this._handleQuery(e,r);for(const e of o?.values()??[])e.metadata.tags&&e.metadata.tags.forEach(t.add,t),e.metadata.what&&e.metadata.what.forEach(n.add,n),e.metadata.where&&e.metadata.where.forEach(i.add,i),e.metadata.days&&e.metadata.days.forEach(a.add,a);return n.size||i.size||a.size?{...t.size&&{tags:t},...n.size&&{what:n},...i.size&&{where:i},...a.size&&{days:a}}:null}_generateDefaultQueries(e,t){const n=[],i=(a=e)instanceof Set?a:new Set(Pf(a));var a;const r=this._store.getEnginesForCameraIDs(i);if(!r)return null;for(const[e,i]of r){let a=null;gv.isEventQuery(t)?a=e.generateDefaultEventQuery(this._store.getVisibleCameras(),i,t):gv.isRecordingQuery(t)?a=e.generateDefaultRecordingQuery(this._store.getVisibleCameras(),i,t):gv.isRecordingSegmentsQuery(t)&&(a=e.generateDefaultRecordingSegmentsQuery(this._store.getVisibleCameras(),i,t));for(const e of a??[])n.push(e)}return n.length?n:null}async getEvents(e,t,n){return await this._handleQuery(e,t,n)}async getRecordings(e,t,n){return await this._handleQuery(e,t,n)}async getRecordingSegments(e,t,n){return await this._handleQuery(e,t,n)}async executeMediaQueries(e,t,n){return this._convertQueryResultsToMedia(e,await this._handleQuery(e,t,n))}async extendMediaQueries(e,t,n,i,a){const r=e=>{let t=null;for(const i of n){const n=i.getStartTime();n&&(!t||"earliest"===e&&nt)&&(t=n)}return t},o=this._cardWideConfig?.performance?.features.media_chunk_size??50,s=[],c=[];for(const e of t){const t={...e};if("later"===i){const e=r("latest");e&&(t.start=e)}else if("earlier"===i){const e=r("earliest");e&&(t.end=e)}t.limit=o,c.push({...e,limit:(e.limit??0)+o}),s.push(t)}const l=this._convertQueryResultsToMedia(e,await this._handleQuery(e,s,a));if(!l.length)return null;const d=cv(n.concat(l));return d.length===n.length?null:{queries:c,results:d}}async getMediaDownloadPath(e,t){const n=this._store.getCameraConfigForMedia(t),i=this._store.getEngineForMedia(t);return n&&i?await i.getMediaDownloadPath(e,n,t):null}getMediaCapabilities(e){const t=this._store.getEngineForMedia(e);return t?t.getMediaCapabilities(e):null}async favoriteMedia(e,t,n){const i=this._store.getCameraConfigForMedia(t),a=this._store.getEngineForMedia(t);if(!i||!a)return;const r=new Date;await a.favoriteMedia(e,i,t,n),hv(this._cardWideConfig,"Frigate Card CameraManager favorite request (",`Duration: ${((new Date).getTime()-r.getTime())/1e3}s,`,"Media:",t.getID(),", Favorite:",n,")")}areMediaQueriesResultsFresh(e,t){const n=new Date;for(const i of e){const e=this._store.getEnginesForCameraIDs(i.cameraIDs);for(const[a,r]of e??[]){const e=a.getQueryResultMaxAge({...i,cameraIDs:r});if(null!==e&&Bm(t,{seconds:e})a?null:await o.getMediaSeekTime(e,this._store.getCameras(),t,n)}async _handleQuery(e,t,n){const i=Pf(t),a=new Map,r=new Date,o=async(t,i)=>{if(!i)return;let r=null;gv.isEventQuery(i)?r=await t.getEvents(e,this._store.getCameras(),i,n):gv.isRecordingQuery(i)?r=await t.getRecordings(e,this._store.getCameras(),i,n):gv.isRecordingSegmentsQuery(i)?r=await t.getRecordingSegments(e,this._store.getCameras(),i,n):gv.isMediaMetadataQuery(i)&&(r=await t.getMediaMetadata(e,this._store.getCameras(),i,n)),r?.forEach(((e,t)=>a.set(t,e)))},s=async e=>{const t=this._store.getEnginesForCameraIDs(e.cameraIDs);t&&await Promise.all(Array.from(t.keys()).map((n=>o(n,{...e,cameraIDs:t.get(n)}))))};await Promise.all(i.map((e=>s(e))));const c=uv(Array.from(a.values()).map((e=>Number(e.cached??0))));return hv(this._cardWideConfig,"Frigate Card CameraManager request [Input queries:",i.length,", Cached output queries:",c,", Total output queries:",a.size,", Duration:",((new Date).getTime()-r.getTime())/1e3+"s,",", Queries:",i,", Results:",a,"]"),a}_convertQueryResultsToMedia(e,t){const n=[];for(const[i,a]of t.entries()){const t=this._store.getEngineOfType(a.engine);if(t){let r=null;gv.isEventQuery(i)&&vv.isEventQueryResult(a)?r=t.generateMediaFromEvents(e,this._store.getCameras(),i,a):gv.isRecordingQuery(i)&&vv.isRecordingQuery(a)&&(r=t.generateMediaFromRecordings(e,this._store.getCameras(),i,a)),r&&n.push(...r)}}return cv(n)}getCameraEndpoints(e,t){const n=this._store.getCameraConfig(e),i=this._store.getEngineForCameraID(e);return n&&i?i.getCameraEndpoints(n,t):null}getCameraMetadata(e,t){const n=this._store.getCameraConfig(t),i=this._store.getEngineForCameraID(t);return n&&i?i.getCameraMetadata(e,n):null}getCameraCapabilities(e){const t=this._store.getCameraConfig(e),n=this._store.getEngineForCameraID(e);return t&&n?n.getCameraCapabilities(t):null}getAggregateCameraCapabilities(e){const t=[...e??this._store.getCameraIDs()].map((e=>this.getCameraCapabilities(e)));return{canFavoriteEvents:t.some((e=>e?.canFavoriteEvents)),canFavoriteRecordings:t.some((e=>e?.canFavoriteRecordings)),canSeek:t.some((e=>e?.canSeek)),supportsClips:t.some((e=>e?.supportsClips)),supportsRecordings:t.some((e=>e?.supportsRecordings)),supportsSnapshots:t.some((e=>e?.supportsSnapshots)),supportsTimeline:t.some((e=>e?.supportsTimeline))}}}var yv='.dotdotdot:after {\n animation: dots 2s linear infinite;\n content: "";\n display: inline-block;\n width: 3em;\n}\n@keyframes dots {\n 0%, 20% {\n content: ".";\n }\n 40% {\n content: "..";\n }\n 60% {\n content: "...";\n }\n 90%, 100% {\n content: "";\n }\n}\n\n:host {\n display: block;\n height: 100%;\n width: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n user-select: text;\n -webkit-user-select: text;\n color: var(--primary-text-color);\n}\n\ndiv.wrapper {\n height: 100%;\n}\n\ndiv.message {\n display: flex;\n justify-content: center;\n align-items: center;\n box-sizing: border-box;\n height: 100%;\n}\n\ndiv.message.padded {\n padding: 20px;\n}\n\ndiv.message div.contents {\n display: flex;\n flex-direction: column;\n padding: 10px;\n margin-top: auto;\n margin-bottom: auto;\n min-width: 0;\n}\n\ndiv.message div.icon {\n padding: 10px;\n}\n\n.vertical {\n flex-direction: column;\n}\n\n.message a {\n color: var(--primary-text-color, white);\n word-break: break-word;\n}\n\n.message pre {\n margin-top: 20px;\n white-space: pre-wrap;\n word-break: break-all;\n}';let bv=class extends ge{constructor(){super(...arguments),this.message=""}render(){const e=this.icon?this.icon:"mdi:information-outline",t={dotdotdot:!!this.dotdotdot};return K`
+
+
+ +
+
+ + ${this.message?K`${this.message}${this.context&&"string"==typeof this.context?": "+this.context:""}`:""} + + ${this.context&&"string"!=typeof this.context?K`
${JSON.stringify(this.context,null,2)}
`:""} +
+
+
`}static get styles(){return b(yv)}};e([be({attribute:!1})],bv.prototype,"message",void 0),e([be({attribute:!1})],bv.prototype,"context",void 0),e([be({attribute:!1})],bv.prototype,"icon",void 0),e([be({attribute:!0,type:Boolean})],bv.prototype,"dotdotdot",void 0),bv=e([_e("frigate-card-message")],bv);let wv=class extends ge{render(){if(this.message)return K` ${Pm("error.troubleshooting")}.`} + .icon=${"mdi:alert-circle"} + .context=${this.message.context} + .dotdotdot=${this.message.dotdotdot} + > + `}static get styles(){return b(yv)}};e([be({attribute:!1})],wv.prototype,"message",void 0),wv=e([_e("frigate-card-error-message")],wv);let xv=class extends ge{constructor(){super(...arguments),this.message="",this.animated=!1,this.size="large"}render(){return K`
+ ${this.animated?K` + `:K``} + ${this.message?K`${this.message}`:K``} +
`}static get styles(){return b(yv)}};function Cv(e){return"error"===e.type?K` `:K` `}function $v(e){return K` + + + `}function kv(e,t,n,i){If(e,"message",{message:t,type:n,icon:i?.icon,context:i?.context})}function Ev(e,t,n){kv(e,t,"error",{context:n?.context})}function Mv(e,t){t instanceof Error&&Ev(e,t.message,{...t instanceof vu&&{context:t.context}})}function Sv(e,t,n){return null==e?e:function(e,t,n,i){if(!ot(e))return e;for(var a=-1,r=(t=gg(t,e)).length,o=r-1,s=e;null!=s&&++a{Sv(e,t,n)},Av=(e,t,n)=>kg(e,t,n),zv=(e,t)=>{let n=t,i=e;if(t&&t.split&&t.includes(".")){const a=t.split(".");n=a[a.length-1],i=Av(e,a.slice(0,-1).join("."))}i&&"object"==typeof i&&delete i[n]},jv=function(e){let t=!1;for(let n=0;nGi(e),Rv=function(e,t,n){return i=>{let a=e(i);return"number"!=typeof a||(a=t?Math.max(t,a):a,a=n?Math.min(n,a):a),a}},Dv=function(e){if("number"!=typeof e)return"string"!=typeof e?null:(e=e.replace(/px$/i,""),isNaN(e)?null:Number(e))},Pv=function(e){return null},Lv=(e,t)=>e.replace("#",`[${t.toString()}]`),Nv=function(e,t,n){return function(i){return((e,t,n,i)=>{const a=Av(e,t);if(void 0===a)return!1;const r=i?.transform?i.transform(a):a;return!(t===n&&Aa(a,r)||(null===r?i?.keepOriginal||(zv(e,t),0):void 0===r||(i?.keepOriginal||zv(e,t),Tv(e,n,r),0)))})(i,e,t,n)}},Uv=function(e,t,n){return function(i){let a=Nv(e,t,n)(i);return a=Zv(Yd,Nv(e,t,n),(e=>e.overrides))(i)||a,a}},Fv=function(e,t){return Uv(e,e,{transform:t})},Hv=function(e,t){return Nv(e,e,{transform:t})},Zv=function(e,t,n){return function(i){let a=!1;const r=Av(i,e);return Array.isArray(r)&&r.forEach((e=>{const i=n?n(e):e;i&&"object"==typeof i&&(a=t(i)||a)})),a}},qv=function(e){if("object"!=typeof e)return"boolean"!=typeof e?null:{enabled:e}},Vv=e=>{const t=`${e}.show_controls`;return function(n){let i=!1;return i=Uv(t,`${e}.show_favorite_control`,{keepOriginal:!0})(n)||i,i=Uv(t,`${e}.show_timeline_control`,{keepOriginal:!0})(n)||i,Fv(t,Pv)(n)||i}},Wv=(e,t)=>{const n=i=>{let a=!1;if(i&&"object"==typeof i){const r=t?t(i):i;r&&(a=e(r)||a),Array.isArray(i)?i.filter((e=>"object"==typeof e)).forEach((e=>{a=n(e)||a})):Object.keys(i).filter((e=>"object"==typeof i[e])).forEach((e=>{a=n(i[e])||a}))}return a};return n},Bv=e=>!("object"!=typeof e||!e||void 0===e.mediaLoaded)&&(e.media_loaded=e.mediaLoaded,delete e.mediaLoaded,!0),Yv=e=>!("object"!=typeof e||!e||"custom:frigate-card-action"!==e.action||"frigate_ui"!==e.frigate_card_action)&&(e.frigate_card_action="camera_ui",!0),Qv=[Fv(Rl,Rv(Dv,75,cu)),Fv("event_viewer.controls.thumbnails.size",Rv(Dv,75,cu)),Fv(jl,Rv(Dv,su)),Fv("event_viewer.controls.next_previous.size",Rv(Dv,su)),Fv(Ad,Rv(Dv,su)),Fv("event_gallery.min_columns",Pv),function(e){let t=!1;return t=Uv("menu.mode",Td,{transform:e=>{if("string"==typeof e){const t=e.match(/^(hover|hidden|overlay|above|below|none)/);if(t)switch(t[1]){case"hover":case"hidden":case"overlay":case"none":return t[1];case"above":case"below":return"outside"}}},keepOriginal:!0})(e)||t,t=Uv("menu.mode",Sd,{transform:e=>{if("string"==typeof e){const t=e.match(/(above|below|left|right|top|bottom)$/);if(t)switch(t[1]){case"left":case"right":case"top":case"bottom":return t[1];case"above":return"top";case"below":return"bottom"}}},keepOriginal:!0})(e)||t,Fv("menu.mode",Pv)(e)||t},Fv(Rd,qv),Fv(jd,qv),Fv(Nd,qv),Fv(Od,qv),Fv(Fd,qv),Fv(Ld,qv),Fv(Id,qv),Fv("menu.buttons.frigate_ui",qv),Fv(Pd,qv),Hv(Jl,(e=>"boolean"==typeof e?e?"all":"never":void 0)),Hv(Tl,(e=>"boolean"==typeof e?e?"all":"never":void 0)),Hv("event_viewer.auto_play",(e=>"boolean"==typeof e?e?"all":"never":void 0)),Hv("event_viewer.auto_unmute",(e=>"boolean"==typeof e?e?"all":"never":void 0)),Uv("event_viewer",Qc),Zv(Qs,Nv("camera_name","frigate.camera_name")),Zv(Qs,Nv("client_id","frigate.client_id")),Zv(Qs,Nv("label","frigate.label")),Zv(Qs,Nv("frigate_url","frigate.url")),Zv(Qs,Nv("zone","frigate.zone")),Vv("event_gallery.controls.thumbnails"),Vv("media_viewer.controls.thumbnails"),Vv("live.controls.thumbnails"),Vv("timeline.controls.thumbnails"),Zv(Yd,Bv,(e=>e.conditions)),e=>Wv(Bv,(e=>e.conditions))("object"==typeof e&&e?e.elements:{}),Uv("event_gallery",Hc),Uv("menu.buttons.frigate_ui",Dd),e=>Wv(Yv)("object"==typeof e&&e?e:{}),Zv(Qs,Fv("live_provider",(e=>"frigate-jsmpeg"===e?"jsmpeg":e))),Uv("live.image",$c),Uv("live.jsmpeg",kc),Uv("live.webrtc_card",Ec),Zv(Qs,Uv("frigate.zone","frigate.zones",{transform:e=>Pf(e)})),Zv(Qs,Uv("frigate.label","frigate.labels",{transform:e=>Pf(e)}))];class Gv extends Event{constructor(e,t){super("frigate-card:condition:evaluate",t),this.condition=e}}function Kv(e,t,n,i){const a=Iv(t);let r=!1;if(n)for(const t of n)e.evaluateCondition(t.conditions,i)&&(tr(a,t.overrides),r=!0);return r?a:t}class Xv{constructor(e){this._state={},this._epoch=this._createEpoch(),this._stateListeners=[],this._hasHAStateConditions=!1,this._mediaQueries=[],this._mediaQueryTrigger=()=>this._triggerChange(),e&&this._initConditions(e)}addStateListener(e){this._stateListeners.push(e)}removeStateListener(e){this._stateListeners=this._stateListeners.filter((t=>t!=e))}destroy(){this._mediaQueries.forEach((e=>e.removeEventListener("change",this._mediaQueryTrigger))),this._mediaQueries=[]}setState(e){this._state={...this._state,...e},this._triggerChange()}get hasHAStateConditions(){return this._hasHAStateConditions}getEpoch(){return this._epoch}evaluateCondition(e,t){const n={...this._state,...t};let i=!0;if(e.view?.length&&(i&&=!!n?.view&&e.view.includes(n.view)),void 0!==e.fullscreen&&(i&&=void 0!==n.fullscreen&&e.fullscreen==n.fullscreen),void 0!==e.expand&&(i&&=void 0!==n.expand&&e.expand==n.expand),e.camera?.length&&(i&&=!!n.camera&&e.camera.includes(n.camera)),e.state?.length)for(const t of e.state)i&&=!(!n.state||(t.state||t.state_not)&&(!(t.entity in n.state)||t.state&&n.state[t.entity].state!==t.state||t.state_not&&n.state[t.entity].state===t.state_not));return void 0!==e.media_loaded&&(i&&=void 0!==n.media_loaded&&e.media_loaded==n.media_loaded),e.media_query&&(i&&=window.matchMedia(e.media_query).matches),i}_createEpoch(){return{controller:this}}_triggerChange(){this._epoch=this._createEpoch(),this._stateListeners.forEach((e=>e()))}_initConditions(e){const t=(e=>{const t=[];e.overrides?.forEach((e=>t.push(e.conditions)));const n=e=>{const i=uh.safeParse(e);i.success?(t.push(i.data.conditions),i.data.elements?.forEach(n)):e&&"object"==typeof e&&Object.keys(e).forEach((t=>n(e[t])))};return e.elements?.forEach(n),t})(e);this._hasHAStateConditions=t.some((e=>!!e.state?.length)),t.forEach((e=>{if(e.media_query){const t=window.matchMedia(e.media_query);t.addEventListener("change",this._mediaQueryTrigger),this._mediaQueries.push(t)}}))}}let Jv=class extends ge{constructor(){super(...arguments),this._root=null}createRenderRoot(){return this}_createRoot(){const e=customElements.get("hui-conditional-element");if(!e||!this.hass)throw new Error(Pm("error.could_not_render_elements"));const t=new e;t.hass=this.hass;const n={type:"conditional",conditions:[],elements:this.elements};try{t.setConfig(n)}catch(e){throw console.error(e),new vu(Pm("error.invalid_elements_config"))}return t}willUpdate(e){try{!this.elements||this._root&&!e.has("elements")||(this._root=this._createRoot())}catch(e){return Mv(this,e)}}render(){return K`${this._root||""}`}updated(){this.hass&&this._root&&(this._root.hass=this.hass)}};e([be({attribute:!1})],Jv.prototype,"elements",void 0),e([be({attribute:!1})],Jv.prototype,"conditionControllerEpoch",void 0),e([be({attribute:!1})],Jv.prototype,"hass",void 0),Jv=e([_e("frigate-card-elements-core")],Jv);let e_=class extends ge{constructor(){super(...arguments),this._boundMenuRemoveHandler=this._menuRemoveHandler.bind(this)}_menuRemoveHandler(e){If(this,"menu-remove",e.detail)}_menuAddHandler(e){const t=e.composedPath();t.length&&(t[0].removeEventListener("frigate-card:menu-remove",this._boundMenuRemoveHandler),t[0].addEventListener("frigate-card:menu-remove",this._boundMenuRemoveHandler))}connectedCallback(){super.connectedCallback(),this.addEventListener("frigate-card:menu-add",this._menuAddHandler)}disconnectedCallback(){this.removeEventListener("frigate-card:menu-add",this._menuAddHandler),super.disconnectedCallback()}render(){return K` + `}static get styles(){return b(":host {\n position: absolute;\n inset: 0;\n overflow: hidden;\n pointer-events: none;\n}\n\n.element {\n position: absolute;\n transform: translate(-50%, -50%);\n pointer-events: auto;\n}\n\nhui-error-card.element {\n inset: 0px;\n transform: unset;\n}")}};e([be({attribute:!1})],e_.prototype,"hass",void 0),e([be({attribute:!1})],e_.prototype,"conditionControllerEpoch",void 0),e([be({attribute:!1})],e_.prototype,"elements",void 0),e_=e([_e("frigate-card-elements")],e_);let t_=class extends ge{setConfig(e){this._config=e}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),this.className=""}render(){if(function(e,t){if(!t)return!0;const n=new Gv(t,{bubbles:!0,composed:!0});return e.dispatchEvent(n),n.evaluation??!1}(this,this._config.conditions))return K` + `}};e([be({attribute:!1,hasChanged:()=>!0})],t_.prototype,"hass",void 0),t_=e([_e("frigate-card-conditional")],t_);class n_ extends ge{constructor(){super(...arguments),this._config=null}setConfig(e){this._config=e}connectedCallback(){super.connectedCallback(),this._config&&If(this,"menu-add",this._config)}disconnectedCallback(){this._config&&If(this,"menu-remove",this._config),super.disconnectedCallback()}}e([we()],n_.prototype,"_config",void 0);let i_=class extends n_{};i_=e([_e("frigate-card-menu-icon")],i_);let a_=class extends n_{};a_=e([_e("frigate-card-menu-state-icon")],a_);let r_=class extends n_{};r_=e([_e("frigate-card-menu-submenu")],r_);let o_=class extends n_{};o_=e([_e("frigate-card-menu-submenu-select")],o_);let s_=class extends ge{constructor(){super(...arguments),this._config=null}setConfig(e){this._config=e}willUpdate(e){e.has("_config")&&this.setAttribute("data-orientation",this._config?.orientation??"vertical")}_actionHandler(e,t){e.stopPropagation();const n=e.detail.action,i=mm(n,t);t&&i&&this.hass&&pm(this,this.hass,t,n,i)}render(){if(!this._config)return;const e=(e,t,n)=>{const i=gm(n?.hold_action),a=gm(n?.double_tap_action);return K`this._actionHandler(e,n)} + >`};return K`
+
+ ${e("right","mdi:arrow-right",this._config.actions_right)} + ${e("left","mdi:arrow-left",this._config.actions_left)} + ${e("up","mdi:arrow-up",this._config.actions_up)} + ${e("down","mdi:arrow-down",this._config.actions_down)} +
+ ${this._config.actions_zoom_in||this._config.actions_zoom_out?K`
+ ${e("zoom_in","mdi:plus",this._config.actions_zoom_in)} + ${e("zoom_out","mdi:minus",this._config.actions_zoom_out)} +
`:K``} + ${this._config.actions_home?K` +
+ ${e("home","mdi:home",this._config.actions_home)} +
+ `:K``} +
`}static get styles(){return b(":host {\n position: relative;\n width: fit-content;\n height: fit-content;\n --frigate-card-ptz-icon-size: 24px;\n}\n\n/*****************\n * Main Containers\n *****************/\n.ptz {\n display: flex;\n gap: 10px;\n color: var(--light-primary-color);\n opacity: 0.4;\n transition: opacity 0.3s ease-in-out;\n}\n\n:host([data-orientation=vertical]) .ptz {\n flex-direction: column;\n}\n\n:host([data-orientation=horizontal]) .ptz {\n flex-direction: row;\n}\n\n.ptz:hover {\n opacity: 1;\n}\n\n:host([data-orientation=vertical]) .ptz div {\n width: calc(var(--frigate-card-ptz-icon-size) * 3);\n}\n\n:host([data-orientation=horizontal]) .ptz div {\n height: calc(var(--frigate-card-ptz-icon-size) * 3);\n}\n\n.ptz-move,\n.ptz-zoom,\n.ptz-home {\n position: relative;\n background-color: rgba(0, 0, 0, 0.3);\n}\n\n.ptz-move {\n height: calc(var(--frigate-card-ptz-icon-size) * 3);\n width: calc(var(--frigate-card-ptz-icon-size) * 3);\n border-radius: 50%;\n}\n\n:host([data-orientation=horizontal]) .ptz .ptz-zoom,\n:host([data-orientation=horizontal]) .ptz .ptz-home {\n width: calc(var(--frigate-card-ptz-icon-size) * 1.5);\n}\n\n:host([data-orientation=vertical]) .ptz .ptz-zoom,\n:host([data-orientation=vertical]) .ptz .ptz-home {\n height: calc(var(--frigate-card-ptz-icon-size) * 1.5);\n}\n\n.ptz-zoom,\n.ptz-home {\n border-radius: var(--ha-card-border-radius, 4px);\n}\n\n/***********\n * PTZ Icons\n ***********/\nha-icon {\n position: absolute;\n --mdc-icon-size: var(--frigate-card-ptz-icon-size);\n}\n\nha-icon:not(.disabled) {\n cursor: pointer;\n}\n\n.disabled {\n color: var(--disabled-text-color);\n}\n\n.up {\n top: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.down {\n bottom: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.left {\n left: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.right {\n right: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n:host([data-orientation=vertical]) .zoom_in {\n right: 5px;\n top: 50%;\n}\n\n:host([data-orientation=vertical]) .zoom_out {\n left: 5px;\n top: 50%;\n}\n\n:host([data-orientation=horizontal]) .zoom_in {\n left: 50%;\n top: 5px;\n}\n\n:host([data-orientation=horizontal]) .zoom_out {\n left: 50%;\n bottom: 5px;\n}\n\n:host([data-orientation=vertical]) .zoom_in,\n:host([data-orientation=vertical]) .zoom_out {\n transform: translateY(-50%);\n}\n\n:host([data-orientation=horizontal]) .zoom_in,\n:host([data-orientation=horizontal]) .zoom_out {\n transform: translateX(-50%);\n}\n\n.home {\n top: 50%;\n left: 50%;\n transform: translateX(-50%) translateY(-50%);\n}")}};e([be({attribute:!1})],s_.prototype,"hass",void 0),e([we()],s_.prototype,"_config",void 0),s_=e([_e("frigate-card-ptz")],s_); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const c_=e=>null!=e?e:J;const l_="m 4.8759466,22.743573 c 0.0866,0.69274 0.811811,1.16359 0.37885,1.27183 -0.43297,0.10824 -2.32718,-3.43665 -2.7601492,-4.95202 -0.4329602,-1.51538 -0.6764993,-3.22017 -0.5682593,-4.19434 0.1082301,-0.97417 5.7097085,-2.48955 5.7097085,-2.89545 0,-0.4059 -1.81304,-0.0271 -1.89422,-0.35178 -0.0812,-0.32472 1.36925,-0.12989 1.75892,-0.64945 0.60885,-0.81181 1.3800713,-0.6765 1.8671505,-1.1094696 0.4870902,-0.4329599 1.0824089,-2.0836399 1.1906589,-2.7871996 0.108241,-0.70357 -1.0824084,-1.51538 -1.4071389,-2.05658 -0.3247195,-0.54121 0.7035702,-0.92005 3.1931099,-1.94834 2.48954,-1.02829 10.39114,-3.30134994 10.49938,-3.03074994 0.10824,0.27061 -2.59779,1.40713994 -4.492,2.11069994 -1.89422,0.70357 -4.97909,2.05658 -4.97909,2.43542 0,0.37885 0.16236,0.67651 0.0541,1.54244 -0.10824,0.86593 -0.12123,1.2702597 -0.32472,1.8400997 -0.1353,0.37884 -0.2706,1.27183 0,2.0836295 0.21648,0.64945 0.92005,1.13653 1.24477,1.24478 0.2706,0.018 1.01746,0.0433 1.8401,0 1.02829,-0.0541 2.48954,0.0541 2.48954,0.32472 0,0.2706 -2.21894,0.10824 -2.21894,0.48708 0,0.37885 2.27306,-0.0541 2.21894,0.32473 -0.0541,0.37884 -1.89422,0.21648 -2.86839,0.21648 -0.77933,0 -1.93031,-0.0361 -2.43542,-0.21648 l -0.10824,0.37884 c -0.18038,0 -0.55744,0.10824 -0.94711,0.10824 -0.48708,0 -0.51414,0.16236 -1.40713,0.16236 -0.892989,0 -0.622391,-0.0541 -1.4341894,-0.10824 -0.81181,-0.0541 -3.842561,2.27306 -4.383761,3.03075 -0.54121,0.75768 -0.21649,2.59778 -0.21649,3.43665 0,0.75379 -0.10824,2.43542 0,3.30135 z";const d_=(e,t,n)=>{const i=e.states[t],a=n?.state?n.state:i?i.state:null;if(!a)return null;const r=s(t),o=i?i.attributes:null;return n?.entity?.translation_key&&e.localize(`component.${n.entity.platform}.entity.${r}.${n.entity.translation_key}.state.${a}`)||o?.device_class&&e.localize(`component.${r}.state.${o.device_class}.${a}`)||e.localize(`component.${r}.state._.${a}`)||a};let u_=class extends ge{_renderItem(e){if(!this.hass)return;const t=gp(this.hass,{...e});return K` + {t.detail.config=e}} + .actionHandler=${wm({hasHold:gm(e.hold_action),hasDoubleClick:gm(e.double_tap_action)})} + > + ${t.title||""} + ${e.subtitle?K`${e.subtitle}`:""} + ${(e=>e.icon?K` + `:K``)(t)} + + `}render(){return this.submenu?K` + e.stopPropagation()} + @click=${e=>vm(e)} + > + + + + ${this.submenu.items.map(this._renderItem.bind(this))} + + `:K``}static get styles(){return b("ha-icon-button.button {\n color: var(--secondary-color, white);\n background-color: rgba(0, 0, 0, 0.6);\n border-radius: 50%;\n padding: 0px;\n margin: 3px;\n --ha-icon-display: block;\n /* Buttons can always be clicked */\n pointer-events: auto;\n opacity: 0.9;\n}\n\n@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\nha-icon[data-domain=alert][data-state=on],\nha-icon[data-domain=automation][data-state=on],\nha-icon[data-domain=binary_sensor][data-state=on],\nha-icon[data-domain=calendar][data-state=on],\nha-icon[data-domain=camera][data-state=streaming],\nha-icon[data-domain=cover][data-state=open],\nha-icon[data-domain=fan][data-state=on],\nha-icon[data-domain=humidifier][data-state=on],\nha-icon[data-domain=light][data-state=on],\nha-icon[data-domain=input_boolean][data-state=on],\nha-icon[data-domain=lock][data-state=unlocked],\nha-icon[data-domain=media_player][data-state=on],\nha-icon[data-domain=media_player][data-state=paused],\nha-icon[data-domain=media_player][data-state=playing],\nha-icon[data-domain=script][data-state=on],\nha-icon[data-domain=sun][data-state=above_horizon],\nha-icon[data-domain=switch][data-state=on],\nha-icon[data-domain=timer][data-state=active],\nha-icon[data-domain=vacuum][data-state=cleaning],\nha-icon[data-domain=group][data-state=on],\nha-icon[data-domain=group][data-state=home],\nha-icon[data-domain=group][data-state=open],\nha-icon[data-domain=group][data-state=locked],\nha-icon[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=pending],\nha-icon[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=plant][data-state=problem],\nha-icon[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\nha-icon-button[data-domain=alert][data-state=on],\nha-icon-button[data-domain=automation][data-state=on],\nha-icon-button[data-domain=binary_sensor][data-state=on],\nha-icon-button[data-domain=calendar][data-state=on],\nha-icon-button[data-domain=camera][data-state=streaming],\nha-icon-button[data-domain=cover][data-state=open],\nha-icon-button[data-domain=fan][data-state=on],\nha-icon-button[data-domain=humidifier][data-state=on],\nha-icon-button[data-domain=light][data-state=on],\nha-icon-button[data-domain=input_boolean][data-state=on],\nha-icon-button[data-domain=lock][data-state=unlocked],\nha-icon-button[data-domain=media_player][data-state=on],\nha-icon-button[data-domain=media_player][data-state=paused],\nha-icon-button[data-domain=media_player][data-state=playing],\nha-icon-button[data-domain=script][data-state=on],\nha-icon-button[data-domain=sun][data-state=above_horizon],\nha-icon-button[data-domain=switch][data-state=on],\nha-icon-button[data-domain=timer][data-state=active],\nha-icon-button[data-domain=vacuum][data-state=cleaning],\nha-icon-button[data-domain=group][data-state=on],\nha-icon-button[data-domain=group][data-state=home],\nha-icon-button[data-domain=group][data-state=open],\nha-icon-button[data-domain=group][data-state=locked],\nha-icon-button[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon-button[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon-button[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon-button[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon-button[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=pending],\nha-icon-button[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=plant][data-state=problem],\nha-icon-button[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon-button[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\n:host {\n pointer-events: auto;\n}\n\nmwc-list-item {\n z-index: 20;\n}")}};e([be({attribute:!1})],u_.prototype,"hass",void 0),e([be({attribute:!1})],u_.prototype,"submenu",void 0),u_=e([_e("frigate-card-submenu")],u_);let h_=class extends ge{shouldUpdate(e){const t=e.get("hass");return!e.has("hass")||!t||!this.submenuSelect||up(this.hass,t,[this.submenuSelect.entity])}async _refreshOptionTitles(){if(!this.hass||!this.submenuSelect)return;const e=this.submenuSelect.entity,t=this.hass.states[e]?.attributes?.options,n=await(this.entityRegistryManager?.getEntity(this.hass,e))??null,i={};for(const a of t){const t=d_(this.hass,e,{...n&&{entity:n},state:a});t&&(i[a]=t)}this._optionTitles=i}willUpdate(){if(!this.submenuSelect||!this.hass)return;this._optionTitles||this._refreshOptionTitles();const e=this.submenuSelect.entity,t=this.hass.states[e],n=t?.attributes?.options;if(!t||!n)return;const i={icon:op("select"),...gp(this.hass,this.submenuSelect),...this.submenuSelect,type:"custom:frigate-card-menu-submenu",items:[]};delete i.options;for(const a of n){const n=this._optionTitles?.[a]??a;i.items.push({state_color:!0,selected:t.state===a,enabled:!0,title:n||a,...(e.startsWith("select.")||e.startsWith("input_select."))&&{tap_action:{action:"call-service",service:e.startsWith("select.")?"select.select_option":"input_select.select_option",service_data:{entity_id:e,option:a}}},...this.submenuSelect.options&&this.submenuSelect.options[a]})}this._generatedSubmenu=i}render(){return K` `}};var m_;e([be({attribute:!1})],h_.prototype,"hass",void 0),e([be({attribute:!1})],h_.prototype,"submenuSelect",void 0),e([be({attribute:!1})],h_.prototype,"entityRegistryManager",void 0),e([we()],h_.prototype,"_optionTitles",void 0),h_=e([_e("frigate-card-submenu-select")],h_);let p_=m_=class extends ge{constructor(){super(...arguments),this.expanded=!1,this.buttons=[]}set menuConfig(e){this._menuConfig=e,e&&this.style.setProperty("--frigate-card-menu-button-size",`${e.button_size}px`),this.setAttribute("data-style",e.style),this.setAttribute("data-position",e.position),this.setAttribute("data-alignment",e.alignment)}static isHidingMenu(e){return"hidden"===e?.style??!1}toggleMenu(){this._isHidingMenu()&&(this.expanded=!this.expanded)}_isHidingMenu(){return m_.isHidingMenu(this._menuConfig)}_isMenuToggleAction(e){if(!e)return!1;const t=um(e);return!!t&&"menu_toggle"==t.frigate_card_action}_actionHandler(e,t){if(!e)return;e.detail.config&&(t=e.detail.config),e.stopPropagation();const n=e.detail.action;let i=mm(n,t);if(!t||!n)return;let a=!1,r=!1;if(Array.isArray(i)){const e=i.length;i=i.filter((e=>!this._isMenuToggleAction(e))),i.length!=e&&(r=!0),i.length&&(a=pm(this,this.hass,t,n,i))}else this._isMenuToggleAction(i)?r=!0:a=pm(this,this.hass,t,n,i);this._isHidingMenu()&&(r?this.expanded=!this.expanded:a&&(this.expanded=!1))}willUpdate(e){const t=this._menuConfig?.style,n=(e,n)=>{if("hidden"===t){if(e.icon===nu)return-1;if(n.icon===nu)return 1}return void 0===e.priority||void 0!==n.priority&&n.priority>e.priority?1:void 0===n.priority||void 0!==e.priority&&n.priority + `;if("custom:frigate-card-menu-submenu-select"===e.type)return K` + `;let t={...e};const n=t.icon===nu?l_:"";this.hass&&"custom:frigate-card-menu-state-icon"===e.type&&(t=gp(this.hass,t));const i=gm(e.hold_action),a=gm(e.double_tap_action);return K` this._actionHandler(t,e)} + > + ${n?K``:K``} + `}render(){if(!this._menuConfig)return;const e=this._menuConfig.style;if("none"===e)return;const t=("hidden"!==e||this.expanded?this.buttons.filter((e=>!e.alignment||"matching"===e.alignment)):this.buttons.filter((e=>e.icon===nu))).filter((e=>!1!==e.enabled)),n="hidden"!==e||this.expanded?this.buttons.filter((e=>"opposing"===e.alignment&&!1!==e.enabled)):[],i={flex:String(t.length)},a={flex:String(n.length)};return K`
+ ${t.map((e=>this._renderButton(e)))} +
+
+ ${n.map((e=>this._renderButton(e)))} +
`}static get styles(){return b('ha-icon-button.button {\n color: var(--secondary-color, white);\n background-color: rgba(0, 0, 0, 0.6);\n border-radius: 50%;\n padding: 0px;\n margin: 3px;\n --ha-icon-display: block;\n /* Buttons can always be clicked */\n pointer-events: auto;\n opacity: 0.9;\n}\n\n@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\nha-icon[data-domain=alert][data-state=on],\nha-icon[data-domain=automation][data-state=on],\nha-icon[data-domain=binary_sensor][data-state=on],\nha-icon[data-domain=calendar][data-state=on],\nha-icon[data-domain=camera][data-state=streaming],\nha-icon[data-domain=cover][data-state=open],\nha-icon[data-domain=fan][data-state=on],\nha-icon[data-domain=humidifier][data-state=on],\nha-icon[data-domain=light][data-state=on],\nha-icon[data-domain=input_boolean][data-state=on],\nha-icon[data-domain=lock][data-state=unlocked],\nha-icon[data-domain=media_player][data-state=on],\nha-icon[data-domain=media_player][data-state=paused],\nha-icon[data-domain=media_player][data-state=playing],\nha-icon[data-domain=script][data-state=on],\nha-icon[data-domain=sun][data-state=above_horizon],\nha-icon[data-domain=switch][data-state=on],\nha-icon[data-domain=timer][data-state=active],\nha-icon[data-domain=vacuum][data-state=cleaning],\nha-icon[data-domain=group][data-state=on],\nha-icon[data-domain=group][data-state=home],\nha-icon[data-domain=group][data-state=open],\nha-icon[data-domain=group][data-state=locked],\nha-icon[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=pending],\nha-icon[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=plant][data-state=problem],\nha-icon[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\nha-icon-button[data-domain=alert][data-state=on],\nha-icon-button[data-domain=automation][data-state=on],\nha-icon-button[data-domain=binary_sensor][data-state=on],\nha-icon-button[data-domain=calendar][data-state=on],\nha-icon-button[data-domain=camera][data-state=streaming],\nha-icon-button[data-domain=cover][data-state=open],\nha-icon-button[data-domain=fan][data-state=on],\nha-icon-button[data-domain=humidifier][data-state=on],\nha-icon-button[data-domain=light][data-state=on],\nha-icon-button[data-domain=input_boolean][data-state=on],\nha-icon-button[data-domain=lock][data-state=unlocked],\nha-icon-button[data-domain=media_player][data-state=on],\nha-icon-button[data-domain=media_player][data-state=paused],\nha-icon-button[data-domain=media_player][data-state=playing],\nha-icon-button[data-domain=script][data-state=on],\nha-icon-button[data-domain=sun][data-state=above_horizon],\nha-icon-button[data-domain=switch][data-state=on],\nha-icon-button[data-domain=timer][data-state=active],\nha-icon-button[data-domain=vacuum][data-state=cleaning],\nha-icon-button[data-domain=group][data-state=on],\nha-icon-button[data-domain=group][data-state=home],\nha-icon-button[data-domain=group][data-state=open],\nha-icon-button[data-domain=group][data-state=locked],\nha-icon-button[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon-button[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon-button[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon-button[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon-button[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=pending],\nha-icon-button[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=plant][data-state=problem],\nha-icon-button[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon-button[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\n:host {\n --frigate-card-menu-button-size: 40px;\n --mdc-icon-button-size: var(--frigate-card-menu-button-size);\n --mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);\n pointer-events: none;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n/***********************************\n * Aligned divs: matching & opposing\n ***********************************/\ndiv.matching,\ndiv.opposing {\n display: flex;\n flex-wrap: wrap;\n flex-direction: row;\n align-items: flex-start;\n min-width: 0px;\n min-height: 0px;\n}\n\ndiv.matching {\n justify-content: flex-start;\n}\n\ndiv.opposing {\n justify-content: flex-end;\n}\n\n/********************\n * Outside menu style\n ********************/\n:host([data-style=outside]) {\n width: 100%;\n background: var(--secondary-background-color);\n}\n\n/************************************\n * Match menu rounded corners to card\n ************************************/\n:host([data-position=top]),\n:host([data-position=left]) {\n border-top-left-radius: var(--ha-card-border-radius, 4px);\n}\n\n:host([data-position=top]),\n:host([data-position=right]) {\n border-top-right-radius: var(--ha-card-border-radius, 4px);\n}\n\n:host([data-position=bottom]),\n:host([data-position=left]) {\n border-bottom-left-radius: var(--ha-card-border-radius, 4px);\n}\n\n:host([data-position=bottom]),\n:host([data-position=right]) {\n border-bottom-right-radius: var(--ha-card-border-radius, 4px);\n}\n\n/**************************************\n * Positioning for absolute menu styles\n **************************************/\n:host(:not([data-style=outside])[data-position=top]),\n:host(:not([data-style=outside])[data-position=left][data-alignment=top]),\n:host(:not([data-style=outside])[data-position=right][data-alignment=top]) {\n top: 0px;\n}\n\n:host(:not([data-style=outside])[data-position=bottom]),\n:host(:not([data-style=outside])[data-position=left][data-alignment=bottom]),\n:host(:not([data-style=outside])[data-position=right][data-alignment=bottom]) {\n bottom: 0px;\n}\n\n:host(:not([data-style=outside])[data-position=left]),\n:host(:not([data-style=outside])[data-position=top][data-alignment=left]),\n:host(:not([data-style=outside])[data-position=bottom][data-alignment=left]) {\n left: 0px;\n}\n\n:host(:not([data-style=outside])[data-position=right]),\n:host(:not([data-style=outside])[data-position=top][data-alignment=right]),\n:host(:not([data-style=outside])[data-position=bottom][data-alignment=right]) {\n right: 0px;\n}\n\n/********************************************************\n * Hack: Ensure host & div expand for column flex layouts\n ********************************************************/\n:host(:not([data-style=outside])[data-position=left]) {\n writing-mode: vertical-lr;\n}\n\n:host(:not([data-style=outside])[data-position=right]) {\n writing-mode: vertical-rl;\n}\n\n:host(:not([data-style=outside])[data-style=overlay][data-position=left]) div > *,\n:host(:not([data-style=outside])[data-style=overlay][data-position=right]) div > *,\n:host(:not([data-style=outside])[data-style*=hover][data-position=left]) div > *,\n:host(:not([data-style=outside])[data-style*=hover][data-position=right]) div > *,\n:host(:not([data-style=outside])[data-style=hidden][data-position=left]) div > *,\n:host(:not([data-style=outside])[data-style=hidden][data-position=right]) div > * {\n writing-mode: horizontal-tb;\n}\n\n/**********************\n * "Reverse" alignments\n **********************/\n:host(:not([data-style=outside])[data-position=left][data-alignment=bottom]),\n:host(:not([data-style=outside])[data-position=right][data-alignment=bottom]),\n:host([data-position=top][data-alignment=right]),\n:host([data-position=bottom][data-alignment=right]),\n:host(:not([data-style=outside])[data-position=left][data-alignment=bottom]) div,\n:host(:not([data-style=outside])[data-position=right][data-alignment=bottom]) div,\n:host([data-position=top][data-alignment=right]) div,\n:host([data-position=bottom][data-alignment=right]) div {\n flex-direction: row-reverse;\n}\n\n/****************************\n * Wrap upwards on the bottom\n ****************************/\n:host(:not([data-style=outside])[data-position=bottom]) div {\n flex-wrap: wrap-reverse;\n}\n\n/********************************************\n * Positioning for absolute based menu styles\n ********************************************/\n:host([data-style=overlay]),\n:host([data-style*=hover]),\n:host([data-style=hidden]) {\n position: absolute;\n overflow: hidden;\n width: calc(var(--frigate-card-menu-button-size) + 6px);\n height: calc(var(--frigate-card-menu-button-size) + 6px);\n}\n\n:host([data-style=overlay][data-position=top]),\n:host([data-style=overlay][data-position=bottom]),\n:host([data-style*=hover][data-position=top]),\n:host([data-style*=hover][data-position=bottom]),\n:host([data-style=hidden][data-position=top][expanded]),\n:host([data-style=hidden][data-position=bottom][expanded]) {\n width: 100%;\n height: auto;\n overflow: visible;\n background: linear-gradient(90deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));\n}\n\n:host([data-style=overlay][data-position=left]),\n:host([data-style=overlay][data-position=right]),\n:host([data-style*=hover][data-position=left]),\n:host([data-style*=hover][data-position=right]),\n:host([data-style=hidden][data-position=left][expanded]),\n:host([data-style=hidden][data-position=right][expanded]) {\n height: 100%;\n width: auto;\n overflow: visible;\n background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));\n}')}};function f_(e){return"number"==typeof e}function g_(e){return"[object Object]"===Object.prototype.toString.call(e)}function v_(e){return g_(e)||function(e){return Array.isArray(e)}(e)}function __(e){return Math.abs(e)}function y_(e){return e?e/__(e):0}function b_(e,t){return __(e-t)}function w_(e){return $_(e).map(Number)}function x_(e){return e[C_(e)]}function C_(e){return Math.max(0,e.length-1)}function $_(e){return Object.keys(e)}function k_(e,t){return[e,t].reduce((function(e,t){return $_(t).forEach((function(n){var i=e[n],a=t[n],r=g_(i)&&g_(a);e[n]=r?k_(i,a):a})),e}),{})}function E_(e,t){var n=$_(e),i=$_(t);return n.length===i.length&&n.every((function(n){var i=e[n],a=t[n];return"function"==typeof i?"".concat(i)==="".concat(a):v_(i)&&v_(a)?E_(i,a):i===a}))}function M_(e,t){var n={start:function(){return 0},center:function(e){return i(e)/2},end:i};function i(e){return t-e}var a={measure:function(i){return f_(e)?t*Number(e):n[e](i)}};return a}function S_(e,t){var n=__(e-t);function i(t){return tt}function r(e){return i(e)||a(e)}var o={length:n,max:t,min:e,constrain:function(n){return r(n)?i(n)?e:t:n},reachedAny:r,reachedMax:a,reachedMin:i,removeOffset:function(e){return n?e-n*Math.ceil((e-t)/n):e}};return o}function T_(e,t,n){var i=S_(0,e),a=i.min,r=i.constrain,o=e+1,s=c(t);function c(e){return n?__((o+e)%o):r(e)}function l(){return s}function d(e){return s=c(e),u}var u={add:function(e){return d(l()+e)},clone:function(){return T_(e,l(),n)},get:l,set:d,min:a,max:e};return u}function A_(){var e=[];var t={add:function(n,i,a,r){return void 0===r&&(r={passive:!0}),n.addEventListener(i,a,r),e.push((function(){return n.removeEventListener(i,a,r)})),t},removeAll:function(){return e=e.filter((function(e){return e()})),t}};return t}function z_(e){var t=e;function n(e){return t/=e,a}function i(e){return f_(e)?e:e.get()}var a={add:function(e){return t+=i(e),a},divide:n,get:function(){return t},multiply:function(e){return t*=e,a},normalize:function(){return 0!==t&&n(t),a},set:function(e){return t=i(e),a},subtract:function(e){return t-=i(e),a}};return a}function j_(e,t,n,i,a,r,o,s,c,l,d,u,h,m,p,f){var g=e.cross,v=["INPUT","SELECT","TEXTAREA"],_={passive:!1},y=z_(0),b=A_(),w=A_(),x=h.measure(20),C={mouse:300,touch:400},$={mouse:500,touch:600},k=p?5:16,E=1,M=0,S=0,T=!1,A=!1,z=!1,j=!1;function O(e){if(!((j=!a.isTouchEvent(e))&&0!==e.button||function(e){var t=e.nodeName||"";return v.indexOf(t)>-1}(e.target))){var t,o=b_(i.get(),r.get())>=2,s=j||!o;T=!0,a.pointerDown(e),y.set(i),i.set(r),c.useBaseMass().useSpeed(80),t=j?document:n,w.add(t,"touchmove",I,_).add(t,"touchend",R).add(t,"mousemove",I,_).add(t,"mouseup",R),M=a.readPoint(e),S=a.readPoint(e,g),u.emit("pointerDown"),s&&(z=!1)}}function I(e){if(!A&&!j){if(!e.cancelable)return R(e);var n=a.readPoint(e),r=a.readPoint(e,g),s=b_(n,M),c=b_(r,S);if(!(A=s>c)&&!z)return R(e)}var l=a.pointerMove(e);!z&&l&&(z=!0),o.start(),i.add(t.apply(l)),e.preventDefault()}function R(e){var n=l.byDistance(0,!1).index!==d.get(),r=a.pointerUp(e)*(p?$:C)[j?"mouse":"touch"],o=function(e,t){var n=d.clone().add(-1*y_(e)),i=n.get()===d.min||n.get()===d.max,a=l.byDistance(e,!p).distance;return p||__(e)=.5,v=n&&h>.75,_=__(r)0?e.concat([n]):e}),[])}function f(n,a){var r="start"===a,l=r?-i:i,d=o.findSlideBounds([l]);return n.map((function(n){var a=r?0:-i,o=r?i:0,l=d.filter((function(e){return e.index===n}))[0],u=l[r?"end":"start"],h=z_(-1),m=z_(-1),p=N_(e,t,c[n]);return{index:n,location:m,translate:p,target:function(){return h.set(s.get()>u?a:o)}}}))}var g={canLoop:function(){return h.every((function(e){var t=e.index,i=d.filter((function(e){return e!==t}));return m(i,n)<=.1}))},clear:function(){h.forEach((function(e){return e.translate.clear()}))},loop:function(){h.forEach((function(e){var t=e.target,n=e.translate,i=e.location,a=t();a.get()!==i.get()&&(0===a.get()?n.clear():n.to(a),i.set(a))}))},loopPoints:h};return g}function F_(e,t,n,i,a,r,o){var s=a.removeOffset,c=a.constrain,l=.5,d=r?[0,t,-t]:[0],u=h(d,o);function h(t,a){var r=t||d,o=function(e){var t=e||0;return n.map((function(e){return S_(l,e-l).constrain(e*t)}))}(a);return r.reduce((function(t,a){var r=i.map((function(t,i){return{start:t-n[i]+o[i]+a,end:t+e-o[i]+a,index:i}}));return t.concat(r)}),[])}return{check:function(e,t){var n=r?s(e):c(e);return(t||u).reduce((function(e,t){var i=t.index,a=t.start,r=t.end;return!(-1!==e.indexOf(i))&&(an)?e.concat([i]):e}),[])},findSlideBounds:h}}function H_(e,t,n){var i=f_(n);var a={groupSlides:function(a){return i?function(e,t){return w_(e).filter((function(e){return e%t==0})).map((function(n){return e.slice(n,n+t)}))}(a,n):function(n){return w_(n).reduce((function(n,i){var a=t.slice(x_(n),i+1).reduce((function(e,t){return e+t}),0);return!i||a>e?n.concat(i):n}),[]).map((function(e,t,i){return n.slice(e,i[t+1])}))}(a)}};return a}function Z_(e,t,n,i,a){var r=i.align,o=i.axis,s=i.direction,c=i.startIndex,l=i.inViewThreshold,d=i.loop,u=i.speed,h=i.dragFree,m=i.slidesToScroll,p=i.skipSnaps,f=i.containScroll,g=t.getBoundingClientRect(),v=n.map((function(e){return e.getBoundingClientRect()})),_=function(e){var t="rtl"===e?-1:1,n={apply:function(e){return e*t}};return n}(s),y=function(e,t){var n="y"===e?"y":"x";return{scroll:n,cross:"y"===e?"x":"y",startEdge:"y"===n?"top":"rtl"===t?"right":"left",endEdge:"y"===n?"bottom":"rtl"===t?"left":"right",measureSize:function(e){var t=e.width,i=e.height;return"x"===n?t:i}}}(o,s),b=y.measureSize(g),w=function(e){var t={measure:function(t){return e*(t/100)}};return t}(b),x=M_(r,b),C=!d&&""!==f,$=function(e,t,n,i,a){var r=e.measureSize,o=e.startEdge,s=e.endEdge,c=n[0]&&a,l=function(){if(!c)return 0;var e=n[0];return __(t[o]-e[o])}(),d=function(){if(!c)return 0;var e=window.getComputedStyle(x_(i));return parseFloat(e.getPropertyValue("margin-".concat(s)))}(),u=n.map(r),h=n.map((function(e,t,n){var i=!t,a=t===C_(n);return i?u[t]+l:a?u[t]+d:n[t+1][o]-e[o]})).map(__);return{slideSizes:u,slideSizesWithGaps:h}}(y,g,v,n,d||""!==f),k=$.slideSizes,E=$.slideSizesWithGaps,M=H_(b,E,m),S=function(e,t,n,i,a,r,o){var s,c=e.startEdge,l=e.endEdge,d=r.groupSlides,u=d(i).map((function(e){return x_(e)[l]-e[0][c]})).map(__).map(t.measure),h=i.map((function(e){return n[c]-e[c]})).map((function(e){return-__(e)})),m=(s=x_(h)-x_(a),d(h).map((function(e){return e[0]})).map((function(e,t,n){var i=!t,a=t===C_(n);return o&&i?0:o&&a?s:e+u[t]})));return{snaps:h,snapsAligned:m}}(y,x,g,v,E,M,C),T=S.snaps,A=S.snapsAligned,z=-x_(T)+x_(E),j=R_(b,z,A,f).snapsContained,O=C?j:A,I=function(e,t,n){var i,a;return{limit:(i=t[0],a=x_(t),S_(n?i-e:a,i))}}(z,O,d).limit,R=T_(C_(O),c,d),D=R.clone(),P=w_(n),L=function(e){var t=0;function n(e,n){return function(){e===!!t&&n()}}function i(){t=window.requestAnimationFrame(e)}return{proceed:n(!0,i),start:n(!1,i),stop:n(!0,(function(){window.cancelAnimationFrame(t),t=0}))}}((function(){d||B.scrollBounds.constrain(B.dragHandler.pointerDown()),B.scrollBody.seek(F).update();var e=B.scrollBody.settle(F);e&&!B.dragHandler.pointerDown()&&(B.animation.stop(),a.emit("settle")),e||a.emit("scroll"),d&&(B.scrollLooper.loop(B.scrollBody.direction()),B.slideLooper.loop()),B.translate.to(U),B.animation.proceed()})),N=O[R.get()],U=z_(N),F=z_(N),H=O_(U,u,1),Z=L_(d,O,z,I,F),q=function(e,t,n,i,a,r){function o(i){var o=i.distance,s=i.index!==t.get();o&&(e.start(),a.add(o)),s&&(n.set(t.get()),t.set(i.index),r.emit("select"))}var s={distance:function(e,t){o(i.byDistance(e,t))},index:function(e,n){var a=t.clone().set(e);o(i.byIndex(a.get(),n))}};return s}(L,R,D,Z,F,a),V=F_(b,z,k,T,I,d,l),W=j_(y,_,e,F,function(e){var t,n,i=170;function a(e){return"undefined"!=typeof TouchEvent&&e instanceof TouchEvent}function r(e){return e.timeStamp}function o(t,n){var i=n||e.scroll,r="client".concat("x"===i?"X":"Y");return(a(t)?t.touches[0]:t)[r]}return{isTouchEvent:a,pointerDown:function(e){return t=e,n=e,o(e)},pointerMove:function(e){var a=o(e)-o(n),s=r(e)-r(t)>i;return n=e,s&&(t=e),a},pointerUp:function(e){if(!t||!n)return 0;var a=o(n)-o(t),s=r(e)-r(t),c=r(e)-r(n)>i,l=a/s;return s&&!c&&__(l)>.1?l:0},readPoint:o}}(y),U,L,q,H,Z,R,a,w,d,h,p),B={containerRect:g,slideRects:v,animation:L,axis:y,direction:_,dragHandler:W,eventStore:A_(),percentOfView:w,index:R,indexPrevious:D,limit:I,location:U,options:i,scrollBody:H,scrollBounds:I_(I,U,F,H,w),scrollLooper:D_(z,I,U,[U,F]),scrollProgress:P_(I),scrollSnaps:O,scrollTarget:Z,scrollTo:q,slideLooper:U_(y,_,b,z,E,O,V,U,n),slidesToScroll:M,slidesInView:V,slideIndexes:P,target:F,translate:N_(y,_,t)};return B}e([be({attribute:!1})],p_.prototype,"hass",void 0),e([be({attribute:!0,type:Boolean,reflect:!0})],p_.prototype,"expanded",void 0),e([we()],p_.prototype,"_menuConfig",void 0),e([be({attribute:!1})],p_.prototype,"buttons",void 0),e([be({attribute:!1})],p_.prototype,"entityRegistryManager",void 0),p_=m_=e([_e("frigate-card-menu")],p_);var q_={align:"center",axis:"x",containScroll:"",direction:"ltr",slidesToScroll:1,breakpoints:{},dragFree:!1,draggable:!0,inViewThreshold:0,loop:!1,skipSnaps:!1,speed:10,startIndex:0,active:!0};function V_(){function e(e,t){return k_(e,t||{})}var t={merge:e,areEqual:function(e,t){return JSON.stringify($_(e.breakpoints||{}))===JSON.stringify($_(t.breakpoints||{}))&&E_(e,t)},atMedia:function(t){var n=t.breakpoints||{},i=$_(n).filter((function(e){return window.matchMedia(e).matches})).map((function(e){return n[e]})).reduce((function(t,n){return e(t,n)}),{});return e(t,i)}};return t}function W_(e,t,n){var i,a,r,o,s,c=A_(),l=V_(),d=function(){var e=V_(),t=e.atMedia,n=e.areEqual,i=[],a=[];function r(e){var i=t(e.options);return function(){return!n(i,t(e.options))}}var o={init:function(e,n){return a=e.map(r),(i=e.filter((function(e){return t(e.options).active}))).forEach((function(e){return e.init(n)})),e.reduce((function(e,t){var n;return Object.assign(e,((n={})[t.name]=t,n))}),{})},destroy:function(){i=i.filter((function(e){return e.destroy()}))},haveChanged:function(){return a.some((function(e){return e()}))}};return o}(),u=function(){var e={};function t(t){return e[t]||[]}var n={emit:function(e){return t(e).forEach((function(t){return t(e)})),n},off:function(i,a){return e[i]=t(i).filter((function(e){return e!==a})),n},on:function(i,a){return e[i]=t(i).concat([a]),n}};return n}(),h=u.on,m=u.off,p=w,f=!1,g=l.merge(q_,W_.globalOptions),v=l.merge(g),_=[],y=0;function b(t,n){if(!f){var c,h;if(c="container"in e&&e.container,h="slides"in e&&e.slides,r="root"in e?e.root:e,o=c||r.children[0],s=h||[].slice.call(o.children),g=l.merge(g,t),v=l.atMedia(g),i=Z_(r,o,s,v,u),y=i.axis.measureSize(r.getBoundingClientRect()),!v.active)return x();if(i.translate.to(i.location),_=n||_,a=d.init(_,E),v.loop){if(!i.slideLooper.canLoop())return x(),b({loop:!1},n),void(g=l.merge(g,{loop:!0}));i.slideLooper.loop()}v.draggable&&o.offsetParent&&s.length&&i.dragHandler.addActivationEvents()}}function w(e,t){var n=k();x(),b(l.merge({startIndex:n},e),t),u.emit("reInit")}function x(){i.dragHandler.removeAllEvents(),i.animation.stop(),i.eventStore.removeAll(),i.translate.clear(),i.slideLooper.clear(),d.destroy()}function C(e){var t=i[e?"target":"location"].get(),n=v.loop?"removeOffset":"constrain";return i.slidesInView.check(i.limit[n](t))}function $(e,t,n){v.active&&!f&&(i.scrollBody.useBaseMass().useSpeed(t?100:v.speed),i.scrollTo.index(e,n||0))}function k(){return i.index.get()}var E={canScrollNext:function(){return i.index.clone().add(1).get()!==k()},canScrollPrev:function(){return i.index.clone().add(-1).get()!==k()},clickAllowed:function(){return i.dragHandler.clickAllowed()},containerNode:function(){return o},internalEngine:function(){return i},destroy:function(){f||(f=!0,c.removeAll(),x(),u.emit("destroy"))},off:m,on:h,plugins:function(){return a},previousScrollSnap:function(){return i.indexPrevious.get()},reInit:p,rootNode:function(){return r},scrollNext:function(e){$(i.index.clone().add(1).get(),!0===e,-1)},scrollPrev:function(e){$(i.index.clone().add(-1).get(),!0===e,1)},scrollProgress:function(){return i.scrollProgress.get(i.location.get())},scrollSnapList:function(){return i.scrollSnaps.map(i.scrollProgress.get)},scrollTo:$,selectedScrollSnap:k,slideNodes:function(){return s},slidesInView:C,slidesNotInView:function(e){var t=C(e);return i.slideIndexes.filter((function(e){return-1===t.indexOf(e)}))}};return b(t,n),c.add(window,"resize",(function(){var e=l.atMedia(g),t=!l.areEqual(e,v),n=i.axis.measureSize(r.getBoundingClientRect()),a=y!==n,o=d.haveChanged();(a||t||o)&&w(),u.emit("resize")})),setTimeout((function(){return u.emit("init")}),0),E}function B_(){return B_=Object.assign||function(e){for(var t=1;t=e;case"y":return Math.abs(a)>=e;case"z":return Math.abs(r)>=e;default:return!1}}(r,i)&&e.preventDefault(),l.isStarted?l.isMomentum&&r>Math.max(2,2*l.lastAbsDelta)&&($(!0),x()):x(),0===r&&Object.is&&Object.is(e.deltaX,-0)?d=!0:(t=e,l.axisMovement=Q_(l.axisMovement,i),l.lastAbsDelta=r,l.scrollPointsToMerge.push({axisDelta:i,timeStamp:a}),f(),m({axisDelta:i,isStart:!l.isStartPublished}),l.isStartPublished=!0,C())},f=function(){var e;l.scrollPointsToMerge.length===iy?(l.scrollPoints.unshift({axisDeltaSum:l.scrollPointsToMerge.map((function(e){return e.axisDelta})).reduce(Q_),timeStamp:(e=l.scrollPointsToMerge.map((function(e){return e.timeStamp})),e.reduce((function(e,t){return e+t}))/e.length)}),v(),l.scrollPointsToMerge.length=0,l.scrollPoints.length=1,l.isMomentum||b()):l.isStartPublished||g()},g=function(){var e;l.axisVelocity=(e=l.scrollPointsToMerge,e[e.length-1]).axisDelta.map((function(e){return e/l.willEndTimeout}))},v=function(){var e=l.scrollPoints,t=e[0],n=e[1];if(n&&t){var i=t.timeStamp-n.timeStamp;if(!(i<=0)){var a=t.axisDeltaSum.map((function(e){return e/i})),r=a.map((function(e,t){return e/(l.axisVelocity[t]||1)}));l.axisVelocity=a,l.accelerationFactors.push(r),_(i)}}},_=function(e){var t=10*Math.ceil(e/10)*1.2;l.isMomentum||(t=Math.max(100,2*t)),l.willEndTimeout=Math.min(1e3,Math.round(t))},y=function(e){return 0===e||e<=ny&&e>=ty},b=function(){if(l.accelerationFactors.length>=ay){if(d&&(d=!1,G_(l.axisVelocity)>=.2))return void w();var e=l.accelerationFactors.slice(-1*ay);e.every((function(e){var t=!!e.reduce((function(e,t){return e&&e<1&&e===t?1:0})),n=e.filter(y).length===e.length;return t||n}))&&w(),l.accelerationFactors=e}},w=function(){l.isMomentum=!0},x=function(){(l=sy()).isStarted=!0,l.startTime=Date.now(),n=void 0,d=!1},C=function(){clearTimeout(i),i=setTimeout($,l.willEndTimeout)},$=function(e){void 0===e&&(e=!1),l.isStarted&&(l.isMomentum&&e?m({isEnding:!0,isMomentumCancel:!0}):m({isEnding:!0}),l.isMomentum=!1,l.isStarted=!1)},k=function(e){var t=[],n=function(n){n.removeEventListener("wheel",e),t=t.filter((function(e){return e!==n}))};return K_({observe:function(i){return i.addEventListener("wheel",e,{passive:!1}),t.push(i),function(){return n(i)}},unobserve:n,disconnect:function(){t.forEach(n)}})}(u),E=k.observe,M=k.unobserve,S=k.disconnect;return h(e),K_({on:r,off:o,observe:E,unobserve:M,disconnect:S,feedWheel:u,updateOptions:h})}var ly={active:!0,breakpoints:{},wheelDraggingClass:"is-wheel-dragging",forceWheelAxis:void 0,target:void 0};function dy(e){var t,n=W_.optionsHandler(),i=n.merge(ly,dy.globalOptions),a=function(){};var r={name:"wheelGestures",options:n.merge(i,e),init:function(e){var i,o;t=n.atMedia(r.options);var s,c=e.internalEngine(),l=null!=(i=t.target)?i:e.containerNode().parentNode,d=null!=(o=t.forceWheelAxis)?o:c.options.axis,u=cy({preventWheelAction:d,reverseSign:[!0,!0,!1]}),h=u.observe(l),m=u.on("wheel",(function(e){var n=e.axisDelta,i=n[0],r=n[1],o="x"===d?i:r,c="x"===d?r:i,u=e.isMomentum&&e.previous&&!e.previous.isMomentum,h=e.isEnding&&!e.isMomentum||u;Math.abs(o)>Math.abs(c)&&!p&&!e.isMomentum&&function(e){try{_(s=new MouseEvent("mousedown",e.event))}catch(e){return a()}p=!0,document.documentElement.addEventListener("mousemove",g,!0),document.documentElement.addEventListener("mouseup",g,!0),void document.documentElement.addEventListener("mousedown",g,!0),t.wheelDraggingClass&&l.classList.add(t.wheelDraggingClass)}(e);if(!p)return;h?function(e){p=!1,_(v("mouseup",e)),f(),t.wheelDraggingClass&&l.classList.remove(t.wheelDraggingClass)}(e):_(v("mousemove",e))})),p=!1;function f(){document.documentElement.removeEventListener("mousemove",g,!0),document.documentElement.removeEventListener("mouseup",g,!0),document.documentElement.removeEventListener("mousedown",g,!0)}function g(e){p&&e.isTrusted&&e.stopImmediatePropagation()}function v(e,t){var n,i;if(d===c.options.axis){var a=t.axisMovement;n=a[0],i=a[1]}else{var r=t.axisMovement;i=r[0],n=r[1]}return new MouseEvent(e,{clientX:s.clientX+n,clientY:s.clientY+i,screenX:s.screenX+n,screenY:s.screenY+i,movementX:n,movementY:i,button:0,bubbles:!0,cancelable:!0,composed:!0})}function _(t){e.containerNode().dispatchEvent(t)}a=function(){h(),m(),f()}},destroy:function(){return a()}};return r}dy.globalOptions=void 0;var uy=":host {\n display: flex;\n flex-direction: column;\n width: 100%;\n margin-left: 5px;\n padding: 5px;\n color: var(--primary-text-color);\n overflow: hidden;\n column-gap: 5%;\n}\n\ndiv.title {\n font-size: 1.2rem;\n font-weight: bold;\n}\n\ndiv.details {\n flex: 1;\n display: flex;\n flex-direction: column;\n flex-wrap: wrap;\n --mdc-icon-size: 16px;\n min-height: 0px;\n}";const hy=(e,t,n,i)=>{const a={...i?.cardWideConfig&&{cardWideConfig:i.cardWideConfig}};return K` ${t.render({initial:()=>i?.inProgressFunc?.()??$v(a),pending:()=>i?.inProgressFunc?.()??$v(a),error:t=>{Nf(t),Mv(e,t)},complete:n})}`},my=0,py=Symbol(); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */class fy{constructor(e,t,n){this.o=0,this.status=0,this.autoRun=!0,this.i=e,this.i.addController(this);const i="object"==typeof t?t:{task:t,args:n};this.t=i.task,this.h=i.args,void 0!==i.autoRun&&(this.autoRun=i.autoRun),this.taskComplete=new Promise(((e,t)=>{this.l=e,this.u=t}))}hostUpdated(){this.performTask()}async performTask(){var e;const t=null===(e=this.h)||void 0===e?void 0:e.call(this);this.shouldRun(t)&&this.run(t)}shouldRun(e){return this.autoRun&&this.v(e)}async run(e){var t;let n,i;null!=e||(e=null===(t=this.h)||void 0===t?void 0:t.call(this)),2!==this.status&&3!==this.status||(this.taskComplete=new Promise(((e,t)=>{this.l=e,this.u=t}))),this.status=1,this._=void 0,this.m=void 0,this.i.requestUpdate();const a=++this.o;try{n=await this.t(e)}catch(e){i=e}this.o===a&&(n===py?this.status=0:(void 0===i?(this.status=2,this.l(n)):(this.status=3,this.u(i)),this.m=n,this._=i),this.i.requestUpdate())}get value(){return this.m}get error(){return this._}render(e){var t,n,i,a;switch(this.status){case 0:return null===(t=e.initial)||void 0===t?void 0:t.call(e);case 1:return null===(n=e.pending)||void 0===n?void 0:n.call(e);case 2:return null===(i=e.complete)||void 0===i?void 0:i.call(e,this.value);case 3:return null===(a=e.error)||void 0===a?void 0:a.call(e,this.error);default:this.status}}v(e){const t=this.T;return this.T=e,Array.isArray(e)&&Array.isArray(t)?e.length===t.length&&e.some(((e,n)=>A(e,t[n]))):e!==t}}const gy=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,vy=(e,t,n,i=!0)=>new fy(e,{args:()=>[!!t(),n()],task:async([e,n])=>{const i=t();return e&&i&&n?(async(e,t)=>e&&t?t.startsWith("data:")||t.match(gy)?t:new Promise(((n,i)=>{e?e.fetchWithAuth(t).then((e=>e.blob())).then((e=>{const t=new FileReader;t.onload=()=>{const e=t.result;n("string"==typeof e?e:null)},t.onerror=e=>i(e),t.readAsDataURL(e)})):i()})):null)(i,n):null},autoRun:i});class _y{static isEvent(e){return this.isClip(e)||this.isSnapshot(e)}static isRecording(e){return"recording"===e.getMediaType()}static isClip(e){return"clip"===e.getMediaType()}static isSnapshot(e){return"snapshot"===e.getMediaType()}static isVideo(e){return this.isClip(e)||this.isRecording(e)}}const yy=(e,t="download")=>{const n=new URL(e).origin===window.location.origin,i=e.startsWith("data:");if(navigator.userAgent.startsWith("Home Assistant/")||navigator.userAgent.startsWith("HomeAssistant/")||!n&&!i)window.open(e,"_blank");else{const n=document.createElement("a");n.setAttribute("download",t),n.href=e,n.click(),n.remove()}},by=async(e,t,n)=>{const i=await t.getMediaDownloadPath(e,n);if(!i)throw new vu(Pm("error.download_no_media"));let a=i.endpoint;if(i.sign){let t;try{t=await lp(e,i.endpoint)}catch(e){Nf(e)}if(!t)throw new vu(Pm("error.download_sign_failed"));a=t}yy(a)},wy=300;let xy=class extends ge{constructor(){super(),this._intersectionObserver=new IntersectionObserver(this._intersectionHandler.bind(this))}connectedCallback(){this._intersectionObserver.observe(this),super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this._intersectionObserver.disconnect()}willUpdate(e){e.has("thumbnail")&&(this._embedThumbnailTask=vy(this,(()=>this.hass),(()=>this.thumbnail),!1),this._intersectionObserver.unobserve(this),this._intersectionObserver.observe(this))}_intersectionHandler(e){this._embedThumbnailTask?.status===my&&e.some((e=>e.isIntersecting))&&this._embedThumbnailTask?.run()}render(){if(!this._embedThumbnailTask)return;const e=K` `;return K`${this.thumbnail?hy(this,this._embedThumbnailTask,(e=>e?K``:K``),{inProgressFunc:()=>e}):e} `}static get styles(){return b(":host {\n display: block;\n overflow: hidden;\n aspect-ratio: 1/1;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\nimg {\n display: block;\n}\n\nimg,\nha-icon {\n display: inline-block;\n vertical-align: top;\n margin: 0;\n border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));\n max-width: var(--frigate-card-thumbnail-size);\n max-height: 100%;\n aspect-ratio: 1/1;\n object-fit: cover;\n}\n\nha-icon {\n --mdc-icon-size: 50%;\n color: var(--primary-text-color);\n display: flex;\n justify-content: center;\n align-items: center;\n border: 1px solid rgba(255, 255, 255, 0.3);\n box-sizing: border-box;\n opacity: 0.2;\n}")}};e([be({attribute:!1})],xy.prototype,"thumbnail",void 0),e([be({attribute:!1})],xy.prototype,"hass",void 0),xy=e([_e("frigate-card-thumbnail-feature-event")],xy);let Cy=class extends ge{render(){if(this.date)return K` +
${Of(this.date,"HH:mm")}
+
${Of(this.date,"MMM do")}
+ ${this.cameraTitle?K`
${this.cameraTitle}
`:K``} + `}static get styles(){return b(":host {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n aspect-ratio: 1/1;\n overflow: hidden;\n max-width: var(--frigate-card-thumbnail-size);\n max-height: var(--frigate-card-thumbnail-size);\n padding: 10px;\n border: 1px solid var(--secondary-color);\n background-color: var(--secondary-background-color);\n border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));\n box-sizing: border-box;\n color: var(--primary-text-color);\n}\n\ndiv {\n text-align: center;\n}\n\ndiv.title {\n font-size: 1.5rem;\n}\n\ndiv.camera {\n font-size: 0.7em;\n}")}};e([be({attribute:!1})],Cy.prototype,"date",void 0),e([be({attribute:!1})],Cy.prototype,"cameraTitle",void 0),Cy=e([_e("frigate-card-thumbnail-feature-recording")],Cy);let $y=class extends ge{render(){if(!this.media)return;const e=this.media.getScore(),t=e?(100*e).toFixed(2)+"%":null,n=this.media.getStartTime(),i=n?Ff(n):null,a=this.media.getEndTime(),r=n&&a?qf(n,a):null,o=this.media.inProgress()?Pm("event.in_progress"):null,s=Rf(this.media.getWhat()?.join(", "))??null,c=Rf(this.media.getWhere()?.join(", "))??null,l=Rf(this.media.getTags()?.join(", "))??null,d=s||l?(s??"")+(s&&l?": ":"")+(l??""):null,u=this.seek?Of(this.seek,"HH:mm:ss"):null;return K` + ${d?K`
+ ${d} + ${t?K`${t}`:""} +
`:""} +
+ ${i?K`
+ + ${i} +
+ ${r||o?K`
+ + ${r?K`${r}`:""} + ${o?K`${o}`:""} +
`:""}`:""} + ${this.cameraTitle?K`
+ + ${this.cameraTitle} +
`:""} + ${c?K`
+ + ${c} +
`:K``} + ${l?K`
+ + ${l} +
`:K``} + ${u?K`
+ + ${u} +
`:K``} +
+ `}static get styles(){return b(uy)}};e([be({attribute:!1})],$y.prototype,"media",void 0),e([be({attribute:!1})],$y.prototype,"seek",void 0),e([be({attribute:!1})],$y.prototype,"cameraTitle",void 0),$y=e([_e("frigate-card-thumbnail-details-event")],$y);let ky=class extends ge{render(){if(!this.media)return;const e=this.media.getStartTime(),t=e?Ff(e):null,n=this.media.getEndTime(),i=e&&n?qf(e,n):null,a=this.media.inProgress()?Pm("recording.in_progress"):null,r=this.seek?Of(this.seek,"HH:mm:ss"):null,o=this.media.getEventCount();return K` + ${this.cameraTitle?K`
+ ${this.cameraTitle} +
`:""} +
+ ${t?K`
+ + ${t} +
+ ${i||a?K`
+ + ${i?K`${i}`:""} + ${a?K`${a}`:""} +
`:""}`:""} + ${r?K`
+ + ${r} +
`:K``} + ${null!==o?K`
+ + ${o} +
`:""} +
+ `}static get styles(){return b(uy)}};e([be({attribute:!1})],ky.prototype,"media",void 0),e([be({attribute:!1})],ky.prototype,"seek",void 0),e([be({attribute:!1})],ky.prototype,"cameraTitle",void 0),ky=e([_e("frigate-card-thumbnail-details-recording")],ky);let Ey=class extends ge{constructor(){super(...arguments),this.details=!1,this.show_favorite_control=!1,this.show_timeline_control=!1,this.show_download_control=!1}render(){if(!this.media||!this.cameraManager||!this.hass)return;const e=this.media.getThumbnail(),t=this.media.getTitle()??"",n={star:!0,starred:!!this.media?.isFavorite()},i=this.show_timeline_control&&this.view&&(!_y.isRecording(this.media)||this.media.getStartTime()&&this.media.getEndTime()),a=this.cameraManager?.getMediaCapabilities(this.media),r=this.show_favorite_control&&this.media&&this.hass&&a?.canFavorite,o=this.show_download_control&&this.hass&&this.media.getID()&&a?.canDownload,s=this.cameraManager.getCameraMetadata(this.hass,this.media.getCameraID())?.title;return K` + ${_y.isEvent(this.media)?K``:_y.isRecording(this.media)?K``:K``} + ${r?K` {if(vm(e),this.hass&&this.media){try{await(this.cameraManager?.favoriteMedia(this.hass,this.media,!this.media?.isFavorite()))}catch(e){return void Nf(e)}this.requestUpdate()}}} + />`:""} + ${this.details&&_y.isEvent(this.media)?K``:this.details&&_y.isRecording(this.media)?K``:K``} + ${i?K`{vm(e),this.view&&this.media&&this.view.evolve({view:"timeline",queryResults:this.view.queryResults?.clone().selectResultIfFound((e=>e===this.media))}).removeContext("timeline").dispatchChangeEvent(this)}} + >`:""} + ${o?K` {if(vm(e),this.hass&&this.cameraManager&&this.media)try{await by(this.hass,this.cameraManager,this.media)}catch(e){Mv(this,e)}}} + >`:""} + `}static get styles(){return b(":host {\n display: flex;\n flex-direction: row;\n box-sizing: border-box;\n position: relative;\n overflow: hidden;\n transition: transform 0.2s linear;\n}\n\n:host(:not([details])) {\n aspect-ratio: 1/1;\n}\n\n:host([details]) {\n border: 1px solid var(--primary-color);\n border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));\n padding: 2px;\n background-color: var(--primary-background-color, black);\n}\n\n:host(:hover) {\n transform: scale(1.04);\n}\n\nha-icon {\n position: absolute;\n border-radius: 50%;\n opacity: 0.5;\n color: var(--primary-color);\n cursor: pointer;\n transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;\n}\n\nha-icon:hover {\n opacity: 1;\n}\n\nha-icon.star {\n top: 3px;\n left: 3px;\n}\n\nha-icon.star.starred {\n color: gold;\n}\n\nha-icon.timeline {\n top: 3px;\n right: 3px;\n}\n\nha-icon.download {\n right: 3px;\n bottom: 3px;\n}\n\nfrigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording {\n flex: 1;\n}")}};e([be({attribute:!1})],Ey.prototype,"hass",void 0),e([be({attribute:!1})],Ey.prototype,"cameraManager",void 0),e([be({attribute:!0})],Ey.prototype,"media",void 0),e([be({attribute:!0,type:Boolean})],Ey.prototype,"details",void 0),e([be({attribute:!0,type:Boolean})],Ey.prototype,"show_favorite_control",void 0),e([be({attribute:!0,type:Boolean})],Ey.prototype,"show_timeline_control",void 0),e([be({attribute:!0,type:Boolean})],Ey.prototype,"show_download_control",void 0),e([be({attribute:!1})],Ey.prototype,"seek",void 0),e([be({attribute:!1})],Ey.prototype,"view",void 0),Ey=e([_e("frigate-card-thumbnail")],Ey);let My=class extends ge{constructor(){super(...arguments),this.direction="horizontal",this.selected=0,this._refSlot=Le(),this._scrolling=!1,this._reInitOnSettle=!1,this._carouselReInitInPlace=yr(this._carouselReInitInPlaceInternal.bind(this),500,{trailing:!0})}connectedCallback(){super.connectedCallback(),this.requestUpdate()}disconnectedCallback(){this._destroyCarousel(),super.disconnectedCallback()}willUpdate(e){["direction","carouselOptions","carouselPlugins"].some((t=>e.has(t)))&&this._destroyCarousel()}getCarouselSelected(){const e=this._carousel?.selectedScrollSnap(),t=void 0!==e?this._carousel?.slideNodes()[e]??null:null;return void 0!==e&&t?{index:e,element:t}:null}carousel(){return this._carousel??null}_carouselReInitInPlaceInternal(){(e=>{window.requestAnimationFrame((()=>{this._carousel?.reInit({...e})}))})({startIndex:this.selected})}carouselReInitWhenSafe(){this._scrolling?this._reInitOnSettle=!0:this._carouselReInitInPlace()}getCarouselPlugins(){return this._carousel?.plugins()??null}updated(e){super.updated(e),this._carousel||this._initCarousel(),e.has("selected")&&this._carousel?.scrollTo(this.selected,"none"===this.transitionEffect)}_destroyCarousel(){this._carousel&&this._carousel.destroy(),this._carousel=void 0}_initCarousel(){const e=this.renderRoot.querySelector(".embla__viewport"),t={root:e,slides:this._refSlot.value?.assignedElements({flatten:!0})};if(e&&t.slides){this._carousel=W_(t,{axis:"horizontal"==this.direction?"x":"y",speed:30,startIndex:this.selected,...this.carouselOptions},this.carouselPlugins);const e=()=>{const e=this.getCarouselSelected();e&&If(this,"carousel:select",e),this.requestUpdate()};this._carousel.on("init",e),this._carousel.on("select",e),this._carousel.on("scroll",(()=>{this._scrolling=!0})),this._carousel.on("settle",(()=>{this._scrolling=!1,this._reInitOnSettle&&(this._reInitOnSettle=!1,this._carouselReInitInPlace())})),this._carousel.on("settle",(()=>{const e=this.getCarouselSelected();e&&If(this,"carousel:settle",e)}))}}_slotChanged(){this._destroyCarousel(),this.requestUpdate()}render(){const e=this._refSlot.value?.assignedElements({flatten:!0})||[],t=this.carouselOptions?.loop||this.selected>0,n=this.carouselOptions?.loop||this.selected+1 + ${t?K``:""} +
+
+ +
+
+ ${n?K``:""} + `}static get styles(){return b(":host {\n display: block;\n height: 100%;\n width: 100%;\n}\n\n.embla {\n width: 100%;\n height: 100%;\n margin-left: auto;\n margin-right: auto;\n}\n\n.embla__container {\n display: flex;\n width: 100%;\n height: 100%;\n user-select: none;\n -webkit-touch-callout: none;\n -khtml-user-select: none;\n -webkit-tap-highlight-color: transparent;\n}\n\n:host([direction=vertical]) .embla__container {\n flex-direction: column;\n}\n\n:host([direction=horizontal]) .embla__container {\n flex-direction: row;\n}\n\n.embla__viewport {\n width: 100%;\n height: 100%;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.embla__viewport.is-draggable {\n cursor: move;\n cursor: grab;\n}\n\n.embla__viewport.is-dragging {\n cursor: grabbing;\n}\n\n:host([direction=vertical]) ::slotted(.embla__slide) {\n margin-bottom: 5px;\n}\n\n:host([direction=horizontal]) ::slotted(.embla__slide) {\n margin-right: 5px;\n}")}};e([be({attribute:!0,reflect:!0})],My.prototype,"direction",void 0),e([be({attribute:!1})],My.prototype,"carouselOptions",void 0),e([be({attribute:!1})],My.prototype,"carouselPlugins",void 0),e([be({attribute:!1})],My.prototype,"selected",void 0),e([be({attribute:!0})],My.prototype,"transitionEffect",void 0),My=e([_e("frigate-card-carousel")],My);let Sy=class extends ge{constructor(){super(),this._refCarousel=Le(),this.selected=0,this._carouselOptions={containScroll:"keepSnaps",dragFree:!0},this._carouselPlugins=[dy({forceWheelAxis:"y"})],this._resizeObserver=new ResizeObserver(this._resizeHandler.bind(this))}_resizeHandler(){this._refCarousel.value?.carouselReInitWhenSafe()}connectedCallback(){super.connectedCallback(),this._resizeObserver.observe(this)}disconnectedCallback(){this._resizeObserver.disconnect(),super.disconnectedCallback()}_getSlides(){if(!this.view?.query||!this.view.queryResults?.hasResults())return[];const e=[];for(let t=0;t{this.view&&this.view.queryResults&&If(this,"thumbnail-carousel:tap",{queryResults:this.view.queryResults.clone().selectResult(e)}),vm(t)}} + > + `}_getDirection(){return"left"===this.config?.mode||"right"===this.config?.mode?"vertical":"above"===this.config?.mode||"below"===this.config?.mode?"horizontal":void 0}render(){const e=this._getSlides();if(e.length&&this.config&&"none"!==this.config.mode)return K` + ${e} + `}static get styles(){return b(":host {\n --frigate-card-thumbnail-size-max: 175px;\n --frigate-card-thumbnail-details-width: calc(\n var(--frigate-card-thumbnail-size) + 200px\n );\n}\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n --frigate-card-carousel-thumbnail-opacity: 1;\n}\n\n:host([direction=vertical]) {\n height: 100%;\n}\n\n:host([direction=horizontal]) {\n height: auto;\n}\n\n.embla__slide {\n flex: 0 0 auto;\n opacity: var(--frigate-card-carousel-thumbnail-opacity);\n}\n\n.embla__slide.slide-selected {\n opacity: 1;\n}\n\nfrigate-card-thumbnail {\n width: var(--frigate-card-thumbnail-size);\n height: var(--frigate-card-thumbnail-size);\n max-width: 100%;\n}\n\nfrigate-card-thumbnail[details] {\n width: var(--frigate-card-thumbnail-details-width);\n}")}};e([be({attribute:!1})],Sy.prototype,"hass",void 0),e([be({attribute:!1})],Sy.prototype,"view",void 0),e([be({attribute:!1})],Sy.prototype,"cameraManager",void 0),e([be({attribute:!1})],Sy.prototype,"config",void 0),e([be({attribute:!1})],Sy.prototype,"selected",void 0),Sy=e([_e("frigate-card-thumbnail-carousel")],Sy);class Ty{constructor(e){this._queries=null,e&&(this._queries=e)}clone(){return Gi(this)}getQueries(){return this._queries}setQueries(e){this._queries=e}}class Ay extends Ty{convertToClipsQueries(){for(const e of this._queries??[])delete e.hasSnapshot,e.hasClip=!0}clone(){return Gi(this)}}class zy extends Ty{}class jy{static areEventQueries(e){return e instanceof Ay}static areRecordingQueries(e){return e instanceof zy}}class Oy{constructor(e){this.view=e.view,this.camera=e.camera,this.query=e.query??null,this.queryResults=e.queryResults??null,this.context=e.context??null}static isMajorMediaChange(e,t){return!e||!t||e.view!==t.view||e.camera!==t.camera||"live"===t.view&&e.context?.live?.overrides?.get(e.camera)!==t.context?.live?.overrides?.get(t.camera)||"live"!==t.view&&e.queryResults!==t.queryResults}static adoptFromViewIfAppropriate(e,t){if(!t)return;let n=null;if(jy.areEventQueries(t.query)){const e=t.query.getQueries();e?.every((e=>e.hasClip))?n="clips":e?.every((e=>e.hasSnapshot))&&(n="snapshots")}else jy.areRecordingQueries(t.query)&&(n="recordings");const i=!e.query||!e.queryResults,a=t.isViewerView()&&e.isGalleryView()&&e.view===n,r=t?.is("media")&&e.is("media");if(i&&(a?(t.query&&(e.query=t.query),t.queryResults&&(e.queryResults=t.queryResults)):r&&n&&(e.view="clips"===n?"clip":"snapshots"===n?"snapshot":"recording")),t.is("live")&&e.is("live")&&t.context?.live?.overrides&&!e.context?.live?.overrides){const n=e.context?.live??{};n.overrides=t.context.live.overrides,e.mergeInContext({live:n})}}clone(){return new Oy({view:this.view,camera:this.camera,query:this.query?.clone()??null,queryResults:this.queryResults?.clone()??null,context:this.context})}evolve(e){return new Oy({view:void 0!==e.view?e.view:this.view,camera:void 0!==e.camera?e.camera:this.camera,query:void 0!==e.query?e.query:this.query?.clone()??null,queryResults:void 0!==e.queryResults?e.queryResults:this.queryResults?.clone()??null,context:void 0!==e.context?e.context:this.context})}mergeInContext(e){return this.context={...this.context,...e},this}removeContext(e){return this.context&&delete this.context[e],this}is(e){return this.view==e}isGalleryView(){return["clips","snapshots","recordings"].includes(this.view)}isAnyMediaView(){return this.isViewerView()||this.is("live")||this.is("image")}isViewerView(){return["clip","snapshot","media","recording"].includes(this.view)}getDefaultMediaType(){return["clip","clips"].includes(this.view)?"clips":["snapshot","snapshots"].includes(this.view)?"snapshots":["recording","recordings"].includes(this.view)?"recordings":null}dispatchChangeEvent(e){If(e,"view:change",this)}}const Iy=(e,t)=>{If(e,"view:change-context",t)},Ry=document.createElement("template");Ry.innerHTML='\n
\n
\n';class Dy extends HTMLElement{constructor(){super();const e=this.attachShadow({mode:"open"});e.appendChild(Ry.content.cloneNode(!0)),this._freeSpaceDiv=e.getElementById("fs")}connectedCallback(){this._freeSpaceDiv&&this._freeSpaceDiv.addEventListener("click",this.handleFreeSpaceDivClick),this.upgradeProperty("open")}disconnectedCallback(){document.removeEventListener("keyup",this.handleKeyUp)}upgradeProperty(e){if(this.hasOwnProperty(e)){let t=this[e];delete this[e],this[e]=t}}handleKeyUp=e=>{if(!e.altKey&&"Escape"===e.key)e.preventDefault(),this.open=!1};get open(){return this.hasAttribute("open")}set open(e){e?this.hasAttribute("open")||this.setAttribute("open",""):this.hasAttribute("open")&&this.removeAttribute("open")}static get observedAttributes(){return["open"]}attributeChangedCallback(e,t,n){"open"===e&&(this.open?(this.setAttribute("tabindex","0"),this.setAttribute("aria-disabled","false"),this.focus({preventScroll:!0}),document.addEventListener("keyup",this.handleKeyUp),this.dispatchEvent(new CustomEvent("open",{bubbles:!0}))):(this.setAttribute("tabindex","-1"),this.setAttribute("aria-disabled","true"),document.removeEventListener("keyup",this.handleKeyUp),this.dispatchEvent(new CustomEvent("close",{bubbles:!0}))))}handleFreeSpaceDivClick=()=>{this.open=!1}}customElements.define("side-drawer",Dy);let Py=class extends ge{constructor(){super(...arguments),this.location="left",this.control=!0,this.open=!1,this.empty=!0,this._refDrawer=Le(),this._refSlot=Le(),this._resizeObserver=new ResizeObserver((()=>this._hideDrawerIfNecessary())),this._isHoverableDevice=Uf()}firstUpdated(e){super.firstUpdated(e);const t=document.createElement("style");t.innerHTML=":host {\n width: unset;\n}\n\n#fs {\n display: none;\n width: 100%;\n inset: 0;\n}\n\n#d,\n#fs {\n height: 100%;\n position: absolute;\n}\n\n#d {\n overflow: visible;\n max-width: 90%;\n}\n\n:host([location=right]) #d {\n left: unset;\n right: 0;\n transform: translateX(100%);\n}\n\n:host([location=right][open]) #d {\n transform: none;\n box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 25px 0px black);\n}\n\n#ifs {\n height: 100%;\n}",this._refDrawer.value?.shadowRoot?.appendChild(t)}_slotChanged(){const e=this._refSlot.value?.assignedElements({flatten:!0});this._resizeObserver.disconnect();for(const t of e??[])this._resizeObserver.observe(t);this._hideDrawerIfNecessary()}_hideDrawerIfNecessary(){if(!this._refDrawer.value)return;const e=this._refSlot.value?.assignedElements({flatten:!0});this.empty=!e||!e.length||e.every((e=>{const t=e.getBoundingClientRect();return!t.width||!t.height}))}render(){return K` + {this.open&&(this.open=!1)}} + > + ${this.control?K` +
{vm(e),this.open=!this.open}} + > + {this._isHoverableDevice&&!this.open&&(this.open=!0)}} + > + +
+ `:""} + this._slotChanged()}> +
+ `}static get styles(){return b("side-drawer {\n background-color: var(--card-background-color);\n}\n\ndiv.control-surround {\n position: absolute;\n bottom: 50%;\n transform: translateY(50%);\n z-index: 0;\n padding-top: 20px;\n padding-bottom: 20px;\n}\n\n:host([location=left]) div.control-surround {\n padding-right: 12px;\n left: 100%;\n}\n\n:host([location=right]) div.control-surround {\n padding-left: 12px;\n right: 100%;\n}\n\n:host([empty]), :host([empty]) > * {\n visibility: hidden;\n}\n\n:host(:not([empty])), :host(:not([empty])) > * {\n visibility: visible;\n}\n\nha-icon.control {\n color: var(--secondary-color, white);\n background-color: rgba(0, 0, 0, 0.7);\n opacity: 0.5;\n pointer-events: all;\n --mdc-icon-size: 20px;\n padding-top: 20px;\n padding-bottom: 20px;\n transition: opacity 0.5s ease;\n}\n\n:host([open]) ha-icon.control, ha-icon.control:hover {\n opacity: 1;\n background-color: black;\n}\n\n:host([location=left]) ha-icon.control {\n border-top-right-radius: 20px;\n border-bottom-right-radius: 20px;\n}\n\n:host([location=right]) ha-icon.control {\n border-top-left-radius: 20px;\n border-bottom-left-radius: 20px;\n}")}};e([be({attribute:!0,reflect:!0})],Py.prototype,"location",void 0),e([be({attribute:!0,reflect:!0,type:Boolean})],Py.prototype,"control",void 0),e([be({type:Boolean,reflect:!0,attribute:!0})],Py.prototype,"open",void 0),e([be({attribute:!1})],Py.prototype,"icons",void 0),e([be({type:Boolean,reflect:!0,attribute:!0})],Py.prototype,"empty",void 0),Py=e([_e("frigate-card-drawer")],Py);let Ly=class extends ge{constructor(){super(...arguments),this._refDrawerLeft=Le(),this._refDrawerRight=Le(),this._boundDrawerHandler=this._drawerHandler.bind(this)}connectedCallback(){super.connectedCallback(),this.addEventListener("frigate-card:drawer:open",this._boundDrawerHandler),this.addEventListener("frigate-card:drawer:close",this._boundDrawerHandler)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("frigate-card:drawer:open",this._boundDrawerHandler),this.removeEventListener("frigate-card:drawer:close",this._boundDrawerHandler)}_drawerHandler(e){const t=e.detail.drawer,n=e.type.endsWith(":open");"left"===t&&this._refDrawerLeft.value?this._refDrawerLeft.value.open=n:"right"===t&&this._refDrawerRight.value&&(this._refDrawerRight.value.open=n)}render(){return K` + + + + + + + + `}static get styles(){return b(":host {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n overflow: hidden;\n}\n\n::slotted(:not([slot])) {\n flex: 1;\n min-height: 0px;\n}")}};e([be({attribute:!1})],Ly.prototype,"drawerIcons",void 0),Ly=e([_e("frigate-card-surround-basic")],Ly);var Ny=4;class Uy{constructor(e,t){this._results=null,this._resultsTimestamp=null,this._selectedIndex=null,e&&this.setResults(e),void 0!==t&&this.selectResult(t)}clone(){return Bi(this,Ny)}isSupersetOf(e){if(!this._results||!e._results)return!1;const t=new Set(this._results.map((e=>e.getID()))),n=new Set(e._results.map((e=>e.getID())));return!(!t||!n||t.has(null)||n.has(null))&&((e,t)=>{for(const n of t)if(!e.has(n))return!1;return!0})(t,n)}getResults(){return this._results}getResultsCount(){return this._results?.length??0}hasResults(){return!!this._results}setResults(e){this._results=e,this._resultsTimestamp=new Date}getResult(e){return this._results&&void 0!==e?this._results[e]:null}getSelectedResult(){return null===this._selectedIndex?null:this.getResult(this._selectedIndex)}getSelectedIndex(){return this._selectedIndex}hasSelectedResult(){return null!==this.getSelectedResult()}resetSelectedResult(){return this._selectedIndex=null,this}getResultsTimestamp(){return this._resultsTimestamp}selectResult(e){return(null===e||this._results&&e>=0&&e{const o=pv(n,a.camera);if(!o)return;const s=Hy(n,i,o,{mediaType:r?.mediaType});s&&(await Vy(e,t,n,a,s,{targetView:r?.targetView,select:r?.select}))?.dispatchChangeEvent(e)},Hy=(e,t,n,i)=>{const a=t.performance?.features.media_chunk_size??50,r=e.generateDefaultEventQueries(n,{limit:a,..."clips"===i?.mediaType&&{hasClip:!0},..."snapshots"===i?.mediaType&&{hasSnapshot:!0}});return r?new Ay(r):null},Zy=async(e,t,n,i,a,r)=>{const o=pv(n,a.camera);if(!o)return;const s=qy(n,i,o);s&&(await Vy(e,t,n,a,s,{targetView:r?.targetView,select:r?.select}))?.dispatchChangeEvent(e)},qy=(e,t,n,i)=>{const a=t.performance?.features.media_chunk_size??50,r=e.generateDefaultRecordingQueries(n,{limit:a,...i?.start&&{start:i.start},...i?.end&&{end:i.end}});return r?new zy(r):null},Vy=async(e,t,n,i,a,r)=>{let o;const s=a.getQueries();if(!s)return null;try{o=await n.executeMediaQueries(t,s)}catch(t){return Nf(t),Mv(e,t),null}if(!o)return null;const c=new Uy(o,"latest"===r?.select&&o.length?o.length-1:void 0);let l={};return"time"===r?.select&&r?.targetTime&&(c.selectBestResult((e=>Wy(e,r.targetTime))),l={mediaViewer:{seek:r.targetTime}}),i?.evolve({query:a,queryResults:c,view:r?.targetView,camera:r?.targetCameraID}).mergeInContext(l)??null},Wy=(e,t)=>{let n;for(const[i,a]of e.entries()){const e=a.getStartTime(),r=a.getUsableEndTime();if(a.includesTime(t)&&e&&r){const t=r.getTime()-e.getTime();(!n||t>n.duration)&&(n={index:i,duration:t})}}return n?n.index:null};let By=class extends ge{async _fetchMedia(){this.cameraManager&&this.cardWideConfig&&this.fetchMedia&&this.hass&&this.view&&!this.view.query&&this.thumbnailConfig&&"none"!==this.thumbnailConfig.mode&&(this.view.context?.thumbnails?.fetch??1)&&await Fy(this,this.hass,this.cameraManager,this.cardWideConfig,this.view,{targetView:this.view.view,mediaType:this.fetchMedia,select:"latest"})}_hasDrawer(){return!!this.thumbnailConfig&&["left","right"].includes(this.thumbnailConfig.mode)}willUpdate(e){this.timelineConfig?.mode&&"none"!==this.timelineConfig.mode&&import("./timeline-6aa9e747.js"),e.has("view")&&Oy.isMajorMediaChange(e.get("view"),this.view)&&(this._cameraIDsForTimeline=this._getCameraIDsForTimeline()??void 0),["view","fetch","browseMediaParams"].some((t=>e.has(t)))&&this._fetchMedia()}_getCameraIDsForTimeline(){return this.view?this.view?.is("live")?pv(this.cameraManager,this.view.camera):this.view.isViewerView()?new Set(this.view.query?.getQueries()?.map((e=>[...e.cameraIDs])).flat()):null:null}render(){if(!this.hass||!this.view)return;const e=(e,t)=>{this.thumbnailConfig&&this._hasDrawer()&&If(e.composedPath()[0],"drawer:"+t,{drawer:this.thumbnailConfig.mode})};return K` e(t,"open")} + @frigate-card:thumbnails:close=${t=>e(t,"close")} + > + ${this.thumbnailConfig&&"none"!==this.thumbnailConfig.mode?K` e(t,"close")} + @frigate-card:thumbnail-carousel:tap=${e=>{const t=e.detail.queryResults.getSelectedResult();t&&this.view?.evolve({view:"media",queryResults:e.detail.queryResults,...t.getCameraID()&&{camera:t.getCameraID()}}).removeContext("timeline").dispatchChangeEvent(e.composedPath()[0])}} + > + `:""} + ${this.timelineConfig&&"none"!==this.timelineConfig.mode?K` + `:""} + + `}static get styles(){return b(":host {\n width: 100%;\n height: 100%;\n display: block;\n}")}};e([be({attribute:!1})],By.prototype,"hass",void 0),e([be({attribute:!1})],By.prototype,"view",void 0),e([be({attribute:!1,hasChanged:Lf})],By.prototype,"thumbnailConfig",void 0),e([be({attribute:!1,hasChanged:Lf})],By.prototype,"timelineConfig",void 0),e([be({attribute:!1,hasChanged:Lf})],By.prototype,"fetchMedia",void 0),e([be({attribute:!1})],By.prototype,"cameraManager",void 0),e([be({attribute:!1})],By.prototype,"cardWideConfig",void 0),By=e([_e("frigate-card-surround")],By);let Yy=class extends ge{willUpdate(e){(e.has("view")||e.has("config"))&&((this.view?.is("live")||this._shouldLivePreload())&&import("./live-e0c9196c.js"),this.view?.isGalleryView()?import("./gallery-6281c347.js"):this.view?.isViewerView()?import("./viewer-b95bc789.js"):this.view?.is("image")?import("./image-0b99ab11.js"):this.view?.is("timeline")&&import("./timeline-6aa9e747.js")),e.has("hide")&&(this.hide?this.setAttribute("hidden",""):this.removeAttribute("hidden"))}shouldUpdate(e){return!0}_shouldLivePreload(){return!!this.config?.live.preload}render(){if(!this.hass||!this.config||!this.nonOverriddenConfig)return K``;const e={hidden:this._shouldLivePreload()&&!this.view?.is("live")},t={hidden:!!this.hide},n=this.view?.is("live")?this.config.live.controls.thumbnails:this.view?.isViewerView()?this.config.media_viewer.controls.thumbnails:this.view?.is("timeline")?this.config.timeline.controls.thumbnails:void 0,i=this.view?.is("live")?this.config.live.controls.timeline:this.view?.isViewerView()?this.config.media_viewer.controls.timeline:void 0,a=this.view?this.cameraManager?.getStore().getCameraConfig(this.view.camera)??null:null;return K` + ${!this.hide&&this.view?.is("image")&&a?K` + `:""} + ${!this.hide&&this.view?.isGalleryView()?K` + `:""} + ${!this.hide&&this.view?.isViewerView()?K` + + + `:""} + ${!this.hide&&this.view?.is("timeline")?K` + `:""} + ${this._shouldLivePreload()||!this.hide&&this.view?.is("live")?K` + r in e.overrides)).map((e=>({conditions:e.conditions,overrides:e.overrides[r]})))??[]} + .cameraManager=${this.cameraManager} + .cardWideConfig=${this.cardWideConfig} + .microphoneStream=${this.microphoneStream} + class="${Ee(e)}" + > + + `:""} + `;var r,o}static get styles(){return b(":host {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n:host([hidden]),\n.hidden {\n display: none;\n}")}};e([be({attribute:!1})],Yy.prototype,"hass",void 0),e([be({attribute:!1})],Yy.prototype,"view",void 0),e([be({attribute:!1})],Yy.prototype,"cameraManager",void 0),e([be({attribute:!1})],Yy.prototype,"config",void 0),e([be({attribute:!1})],Yy.prototype,"nonOverriddenConfig",void 0),e([be({attribute:!1})],Yy.prototype,"cardWideConfig",void 0),e([be({attribute:!1})],Yy.prototype,"resolvedMediaCache",void 0),e([be({attribute:!1})],Yy.prototype,"conditionControllerEpoch",void 0),e([be({attribute:!1})],Yy.prototype,"hide",void 0),e([be({attribute:!1})],Yy.prototype,"microphoneStream",void 0),Yy=e([_e("frigate-card-views")],Yy);const Qy={[Il]:"none",[sl]:"none",[bd]:"none",[Zl]:!1,[gl]:!1,[_d]:!1,[Sl]:"never",[Gc]:"never",[Kc]:"never",[Xc]:"never",[Jl]:"all",[Kl]:!1,[el]:!1,[td]:"none",[il]:"none",[nd]:!1,[rl]:"chevrons",[bl]:"none",[Wl]:"none",[Td]:"outside",[`${Rd}.enabled`]:!1,[`${Hd}.enabled`]:!1,[`${Hd}.enabled`]:!1,[`${Ud}.enabled`]:!1,[Vc]:!1,[Wc]:!1,[Bc]:!1,[qc]:!1,[Pl]:!1,[Ll]:!1,[Nl]:!1,[Dl]:!1,[ll]:!1,[dl]:!1,[ul]:!1,[cl]:!1,[Cd]:!1,[$d]:!1,[kd]:!1,[xd]:!1,[Gd]:!1,[Kd]:10,[eu]:!1,[Jd]:!1,[nl]:!1,[Mc]:!1,[Sc]:10},Gy=(e,t)=>{const n=iu(om).safeParse(e);if(n.success){const e=n.data;Object.entries(Qy).forEach((([n,i])=>((e,t,n,i)=>{void 0===Av(e,n)&&Tv(t,n,i)})(e,t,n,i)))}return t},Ky={box_shadow:"none",border_radius:"0px"};const Xy=Ws.object({model:Ws.string().nullable(),config_entries:Ws.string().array(),manufacturer:Ws.string().nullable()}).array();class Jy{constructor(){this._cache=new Map}has(e){return this._cache.has(e)}getMatches(e){return[...this._cache.values()].filter(e)}get(e){return this._cache.get(e)}set(e){const t=e=>this._cache.set(e.entity_id,e);Array.isArray(e)?e.forEach(t):t(e)}}const eb=Ws.object({config_entry_id:Ws.string().nullable(),device_id:Ws.string().nullable(),disabled_by:Ws.string().nullable(),entity_id:Ws.string(),hidden_by:Ws.string().nullable(),platform:Ws.string(),translation_key:Ws.string().nullable(),unique_id:Ws.string().or(Ws.number()).optional()}),tb=eb.array();class nb{constructor(e){this._fetchedEntityList=!1,this._cache=e}async getEntity(e,t){const n=this._cache.get(t);if(n)return n;const i=await cp(e,eb,{type:"config/entity_registry/get",entity_id:t});return this._cache.set(i),i}async getMatchingEntities(e,t){return await this.fetchEntityList(e),this._cache.getMatches(t)}async getEntities(e,t){const n=new Map;return await Promise.all(t.map((async t=>{let i=null;try{i=await this.getEntity(e,t)}catch{return}i&&n.set(t,i)}))),n}async fetchEntityList(e){if(this._fetchedEntityList)return;const t=await cp(e,tb,{type:"config/entity_registry/list"});this._cache.set(t),this._fetchedEntityList=!0}}class ib extends Map{constructor(e={}){if(super(),!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");if("number"==typeof e.maxAge&&0===e.maxAge)throw new TypeError("`maxAge` must be a number greater than 0");this.maxSize=e.maxSize,this.maxAge=e.maxAge||Number.POSITIVE_INFINITY,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_emitEvictions(e){if("function"==typeof this.onEviction)for(const[t,n]of e)this.onEviction(t,n.value)}_deleteIfExpired(e,t){return"number"==typeof t.expiry&&t.expiry<=Date.now()&&("function"==typeof this.onEviction&&this.onEviction(e,t.value),this.delete(e))}_getOrDeleteIfExpired(e,t){if(!1===this._deleteIfExpired(e,t))return t.value}_getItemValue(e,t){return t.expiry?this._getOrDeleteIfExpired(e,t):t.value}_peek(e,t){const n=t.get(e);return this._getItemValue(e,n)}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this._emitEvictions(this.oldCache),this.oldCache=this.cache,this.cache=new Map)}_moveToRecent(e,t){this.oldCache.delete(e),this._set(e,t)}*_entriesAscending(){for(const e of this.oldCache){const[t,n]=e;if(!this.cache.has(t)){!1===this._deleteIfExpired(t,n)&&(yield e)}}for(const e of this.cache){const[t,n]=e;!1===this._deleteIfExpired(t,n)&&(yield e)}}get(e){if(this.cache.has(e)){const t=this.cache.get(e);return this._getItemValue(e,t)}if(this.oldCache.has(e)){const t=this.oldCache.get(e);if(!1===this._deleteIfExpired(e,t))return this._moveToRecent(e,t),t.value}}set(e,t,{maxAge:n=this.maxAge}={}){const i="number"==typeof n&&n!==Number.POSITIVE_INFINITY?Date.now()+n:void 0;this.cache.has(e)?this.cache.set(e,{value:t,expiry:i}):this._set(e,{value:t,expiry:i})}has(e){return this.cache.has(e)?!this._deleteIfExpired(e,this.cache.get(e)):!!this.oldCache.has(e)&&!this._deleteIfExpired(e,this.oldCache.get(e))}peek(e){return this.cache.has(e)?this._peek(e,this.cache):this.oldCache.has(e)?this._peek(e,this.oldCache):void 0}delete(e){const t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}resize(e){if(!(e&&e>0))throw new TypeError("`maxSize` must be a number greater than 0");const t=[...this._entriesAscending()],n=t.length-e;n<0?(this.cache=new Map(t),this.oldCache=new Map,this._size=t.length):(n>0&&this._emitEvictions(t.slice(0,n)),this.oldCache=new Map(t.slice(n)),this.cache=new Map,this._size=0),this.maxSize=e}*keys(){for(const[e]of this)yield e}*values(){for(const[,e]of this)yield e}*[Symbol.iterator](){for(const e of this.cache){const[t,n]=e;!1===this._deleteIfExpired(t,n)&&(yield[t,n.value])}for(const e of this.oldCache){const[t,n]=e;if(!this.cache.has(t)){!1===this._deleteIfExpired(t,n)&&(yield[t,n.value])}}}*entriesDescending(){let e=[...this.cache];for(let t=e.length-1;t>=0;--t){const n=e[t],[i,a]=n;!1===this._deleteIfExpired(i,a)&&(yield[i,a.value])}e=[...this.oldCache];for(let t=e.length-1;t>=0;--t){const n=e[t],[i,a]=n;if(!this.cache.has(i)){!1===this._deleteIfExpired(i,a)&&(yield[i,a.value])}}}*entriesAscending(){for(const[e,t]of this._entriesAscending())yield[e,t.value]}get size(){if(!this._size)return this.oldCache.size;let e=0;for(const t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}entries(){return this.entriesAscending()}forEach(e,t=this){for(const[n,i]of this.entriesAscending())e.call(t,i,n,this)}get[Symbol.toStringTag](){return JSON.stringify([...this.entriesAscending()])}}class ab{constructor(){this._cache=new ib({maxSize:1e3})}has(e){return this._cache.has(e)}get(e){return this._cache.get(e)}set(e,t){this._cache.set(e,t)}}const rb=async(e,t,n)=>{const i=n?n.get(t):void 0;if(i)return i;const a={type:"media_source/resolve_media",media_content_id:t};let r=null;try{r=await cp(e,lm,a)}catch(e){Nf(e)}return n&&r&&n.set(t,r),r};var ob;!function(e){e.INITIALIZING="initializing",e.INITIALIZED="initialized"}(ob||(ob={}));class sb{constructor(){this._state=new Map}async initializeMultipleIfNecessary(e){return(await Vf(Object.entries(e),(async([e,t])=>this.initializeIfNecessary(e,t)))).every(Boolean)}async initializeIfNecessary(e,t){const n=this._state.get(e);return n===ob.INITIALIZED||n!==ob.INITIALIZING&&(t?(this._state.set(e,ob.INITIALIZING),await t(),this._state.set(e,ob.INITIALIZED)):this._state.set(e,ob.INITIALIZED),!0)}uninitialize(e){return this._state.delete(e)}isInitialized(e){return this._state.get(e)==ob.INITIALIZED}isInitializedMultiple(e){return e.every((e=>this.isInitialized(e)))}}class cb{constructor(){this._current=null,this._lastKnown=null}set(e){this._current=e,this._lastKnown=e}get(){return this._current}getLastKnown(){return this._lastKnown}clear(){this._current=null}has(){return!!this._current}}const lb=50,db=lb;function ub(e,t){let n;return n=e instanceof Event?e.composedPath()[0]:e,n instanceof HTMLImageElement?{width:n.naturalWidth,height:n.naturalHeight,...t}:n instanceof HTMLVideoElement?{width:n.videoWidth,height:n.videoHeight,...t}:n instanceof HTMLCanvasElement?{width:n.width,height:n.height,player:t?.player,...t}:null}function hb(e,t,n){const i=ub(t,n);i&&mb(e,i)}function mb(e,t){If(e,"media:loaded",t)}function pb(e){If(e,"media:unloaded")}function fb(e){If(e,"media:volumechange")}function gb(e){If(e,"media:play")}function vb(e){If(e,"media:pause")}function _b(e){return e.height>=lb&&e.width>=db}const yb=e=>{const t=e?.context?.live?.overrides?.get(e.camera);return!!t&&t!==e.camera};class bb{constructor(){this._dynamicMenuButtons=[]}addDynamicMenuButton(e){this._dynamicMenuButtons.includes(e)||this._dynamicMenuButtons.push(e)}removeDynamicMenuButton(e){this._dynamicMenuButtons=this._dynamicMenuButtons.filter((t=>t!=e))}calculateButtons(e,t,n,i,a,r){const o=n.getStore().getVisibleCameras(),s=i.camera,c=n.getStore().getCameraConfig(s),l=pv(n,s),d=i.queryResults?.getSelectedResult(),u=n.getAggregateCameraCapabilities(l),h=d?n?.getMediaCapabilities(d):null,m=[];if(m.push({icon:nu,...t.menu.buttons.frigate,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.frigate"),tap_action:"hidden"===t.menu?.style?hm("menu_toggle"):hm("default"),hold_action:hm("diagnostics")}),o){const i=Array.from(o,(([t,i])=>{const a=hm("camera_select",{camera:t}),r=n.getCameraMetadata(e,t)??void 0;return{enabled:!0,icon:r?.icon,entity:i.camera_entity,state_color:!0,title:r?.title,selected:s===t,...a&&{tap_action:a}}}));m.push({icon:"mdi:video-switch",...t.menu.buttons.cameras,type:"custom:frigate-card-menu-submenu",title:Pm("config.menu.buttons.cameras"),items:i})}if(s&&l&&i.is("live")){const a=[...l],r=i.context?.live?.overrides?.get(s);if(2===a.length)m.push({icon:"mdi:video-input-component",style:r&&r!==s?this._getEmphasizedStyle():{},title:Pm("config.menu.buttons.substreams"),...t.menu.buttons.substreams,type:"custom:frigate-card-menu-icon",tap_action:hm(yb(i)?"live_substream_off":"live_substream_on")});else if(a.length>2){const o=Array.from(a,(t=>{const a=hm("live_substream_select",{camera:t}),r=n.getCameraMetadata(e,t)??void 0,o=n.getStore().getCameraConfig(t);return{enabled:!0,icon:r?.icon,entity:o?.camera_entity,state_color:!0,title:r?.title,selected:(i.context?.live?.overrides?.get(s)??s)===t,...a&&{tap_action:a}}}));m.push({icon:"mdi:video-input-component",title:Pm("config.menu.buttons.substreams"),style:r&&r!==s?this._getEmphasizedStyle():{},...t.menu.buttons.substreams,type:"custom:frigate-card-menu-submenu",items:o})}}if(m.push({icon:"mdi:cctv",...t.menu.buttons.live,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.live"),style:i.is("live")?this._getEmphasizedStyle():{},tap_action:hm("live")}),u?.supportsClips&&m.push({icon:"mdi:filmstrip",...t.menu.buttons.clips,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.clips"),style:i?.is("clips")?this._getEmphasizedStyle():{},tap_action:hm("clips"),hold_action:hm("clip")}),u?.supportsSnapshots&&m.push({icon:"mdi:camera",...t.menu.buttons.snapshots,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.snapshots"),style:i?.is("snapshots")?this._getEmphasizedStyle():{},tap_action:hm("snapshots"),hold_action:hm("snapshot")}),u?.supportsRecordings&&m.push({icon:"mdi:album",...t.menu.buttons.recordings,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.recordings"),style:i.is("recordings")?this._getEmphasizedStyle():{},tap_action:hm("recordings"),hold_action:hm("recording")}),m.push({icon:"mdi:image",...t.menu.buttons.image,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.image"),style:i?.is("image")?this._getEmphasizedStyle():{},tap_action:hm("image")}),u?.supportsTimeline&&m.push({icon:"mdi:chart-gantt",...t.menu.buttons.timeline,type:"custom:frigate-card-menu-icon",title:Pm("config.view.views.timeline"),style:i.is("timeline")?this._getEmphasizedStyle():{},tap_action:hm("timeline")}),h?.canDownload&&!this._isBeingCasted()&&m.push({icon:"mdi:download",...t.menu.buttons.download,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.download"),tap_action:hm("download")}),r?.cameraURL&&m.push({icon:"mdi:web",...t.menu.buttons.camera_ui,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.camera_ui"),tap_action:hm("camera_ui")}),r?.microphoneController&&r?.currentMediaLoadedInfo?.capabilities?.supports2WayAudio){const e=r.microphoneController.isForbidden(),n=r.microphoneController.isMuted(),i=t.menu.buttons.microphone.type;m.push({icon:e?"mdi:microphone-message-off":n?"mdi:microphone-off":"mdi:microphone",...t.menu.buttons.microphone,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.microphone"),style:e||n?{}:this._getEmphasizedStyle(!0),...!e&&"momentary"===i&&{start_tap_action:hm("microphone_unmute"),end_tap_action:hm("microphone_mute")},...!e&&"toggle"===i&&{tap_action:hm(r.microphoneController.isMuted()?"microphone_unmute":"microphone_mute")}})}if($r.isEnabled&&!this._isBeingCasted()&&m.push({icon:$r.isFullscreen?"mdi:fullscreen-exit":"mdi:fullscreen",...t.menu.buttons.fullscreen,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.fullscreen"),tap_action:hm("fullscreen"),style:$r.isFullscreen?this._getEmphasizedStyle():{}}),m.push({icon:a?"mdi:arrow-collapse-all":"mdi:arrow-expand-all",...t.menu.buttons.expand,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.expand"),tap_action:hm("expand"),style:a?this._getEmphasizedStyle():{}}),r?.mediaPlayers?.length&&(i?.isViewerView()||i.is("live")&&c?.camera_entity)){const n=r.mediaPlayers.map((t=>{const n=vp(e,t)||t,i=e.states[t],a=hm("media_player",{media_player:t,media_player_action:"play"}),r=hm("media_player",{media_player:t,media_player_action:"stop"}),o=!i||"unavailable"===i.state;return{enabled:!0,selected:!1,icon:_p(e,t),entity:t,state_color:!1,title:n,disabled:o,...!o&&a&&{tap_action:a},...!o&&r&&{hold_action:r}}}));m.push({icon:"mdi:cast",...t.menu.buttons.media_player,type:"custom:frigate-card-menu-submenu",title:Pm("config.menu.buttons.media_player"),items:n})}if(r?.currentMediaLoadedInfo&&r.currentMediaLoadedInfo.player){if(r.currentMediaLoadedInfo.capabilities?.supportsPause){const e=r.currentMediaLoadedInfo.player.isPaused();m.push({icon:e?"mdi:play":"mdi:pause",...t.menu.buttons.play,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.play"),tap_action:hm(e?"play":"pause")})}if(r.currentMediaLoadedInfo.capabilities?.hasAudio){const e=r.currentMediaLoadedInfo.player.isMuted();m.push({icon:e?"mdi:volume-off":"mdi:volume-high",...t.menu.buttons.mute,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.mute"),tap_action:hm(e?"unmute":"mute")})}}r?.currentMediaLoadedInfo&&r.currentMediaLoadedInfo.player&&m.push({icon:"mdi:monitor-screenshot",...t.menu.buttons.screenshot,type:"custom:frigate-card-menu-icon",title:Pm("config.menu.buttons.screenshot"),tap_action:hm("screenshot")});const p=this._dynamicMenuButtons.map((e=>({style:this._getStyleFromActions(t,i,e),...e})));return m.concat(p)}_getEmphasizedStyle(e){return e?{animation:"pulse 3s infinite",color:"var(--error-color, white)"}:{color:"var(--primary-color, white)"}}_getStyleFromActions(e,t,n){for(const i of[n.tap_action,n.double_tap_action,n.hold_action,n.start_tap_action,n.end_tap_action]){const n=Array.isArray(i)?i:[i];for(const i of n){if(!i||"fire-dom-event"!==i.action||!("frigate_card_action"in i))continue;const n=i;if(du.some((e=>e===n.frigate_card_action&&t?.is(n.frigate_card_action)))||"default"===n.frigate_card_action&&t.is(e.view.default)||"fullscreen"===n.frigate_card_action&&$r.isEnabled&&$r.isFullscreen||"camera_select"===n.frigate_card_action&&t.camera===n.camera)return this._getEmphasizedStyle()}}return{}}_isBeingCasted(){return!!navigator.userAgent.match(/CrKey\//)}}class wb{constructor(e){this._timer=new _m,this._mute=!0,this._disconnectSeconds=e??0}async connect(){try{this._stream=await navigator.mediaDevices.getUserMedia({audio:!0,video:!1})}catch(e){Nf(e),this._stream=null}this._setMute()}async disconnect(){this._stream?.getTracks().forEach((e=>e.stop())),this._stream=void 0}getStream(){return this._stream??void 0}_setMute(){this._stream?.getTracks().forEach((e=>{e.enabled=!this._mute})),this._startTimer()}mute(){this._mute=!0,this._setMute()}unmute(){this._mute=!1,this._setMute()}isConnected(){return!!this._stream}isForbidden(){return null===this._stream}isMuted(){return!this._stream||this._stream.getTracks().every((e=>!e.enabled))}_startTimer(){this._disconnectSeconds&&this._timer.start(this._disconnectSeconds,(()=>{this.disconnect()}))}}const xb=()=>{const e=new URLSearchParams(window.location.search),t=[],n=new RegExp(/^frigate-card-action(:(?\w+))?:(?\w+)/);for(const[i,a]of e.entries()){const e=i.match(n);if(!e||!e.groups)continue;const r=e.groups.cardID,o=e.groups.action;let s=null;switch(o){case"camera_select":case"live_substream_select":a&&(s=hm(o,{camera:a,cardID:r}));break;case"camera_ui":case"clip":case"clips":case"default":case"diagnostics":case"download":case"expand":case"image":case"live":case"menu_toggle":case"recording":case"recordings":case"snapshot":case"snapshots":case"timeline":s=hm(o,{cardID:r});break;default:console.warn(`Frigate card received unknown card action in query string: ${o}`)}s&&t.push(s)}return t},Cb=e=>{const t=document.createElement("canvas");t.width=e.videoWidth,t.height=e.videoHeight;const n=t.getContext("2d");return n?(n.drawImage(e,0,0,t.width,t.height),t.toDataURL("image/jpeg")):null};var $b;console.info(`%c FRIGATE-HASS-CARD \n%c ${Pm("common.version")} ${Lr} `,"color: pink; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),window.customCards=window.customCards||[],window.customCards.push({type:"frigate-card",name:Pm("common.frigate_card"),description:Pm("common.frigate_card_description"),preview:!0,documentationURL:Bs}),function(e){e.LANGUAGES="languages",e.SIDE_LOAD_ELEMENTS="side-load-elements",e.MEDIA_PLAYERS="media-players",e.CAMERAS="cameras",e.MICROPHONE="microphone"}($b||($b={}));let kb=class extends ge{constructor(){super(),this._panel=!1,this._expand=!1,this._menuButtonController=new bb,this._mediaLoadedInfoController=new cb,this._refMenu=Le(),this._refMain=Le(),this._refElements=Le(),this._refViews=Le(),this._interactionTimer=new _m,this._updateTimer=new _m,this._untriggerTimer=new _m,this._message=null,this._resolvedMediaCache=new ab,this._boundMouseHandler=yr(this._mouseHandler.bind(this),1e3),this._boundCardActionEventHandler=this._cardActionEventHandler.bind(this),this._boundFullscreenHandler=this._fullscreenHandler.bind(this),this._triggers=new Map,this._initializer=new sb,this._locationChangeHandler=()=>{this.hasUpdated&&xb().forEach((e=>this._cardActionHandler(e)))},this._entityRegistryManager=new nb(new Jy)}set hass(e){this._hass=e,this._hass&&(this._refMenu.value&&(this._refMenu.value.hass=this._hass),this._refElements.value&&(this._refElements.value.hass=this._hass),this._refViews.value&&(this._refViews.value.hass=this._hass)),this._conditionController?.hasHAStateConditions&&this._conditionController.setState({state:this._hass.states}),this._setLightOrDarkMode()}static async getConfigElement(){return await import("./editor-7b16019d.js"),document.createElement("frigate-card-editor")}static getStubConfig(e,t){return{cameras:[{camera_entity:t.find((e=>e.startsWith("camera.")))}]}}_requestUpdateForComponentsThatUseConditions(){this._refViews.value&&(this._refViews.value.conditionControllerEpoch=this._conditionController?.getEpoch()),this._refElements.value&&(this._refElements.value.conditionControllerEpoch=this._conditionController?.getEpoch())}_overrideConfig(){if(!this._conditionController)return;const e=Kv(this._conditionController,this._config,this._config.overrides);Aa(e,this._overriddenConfig)||(Aa(e.cameras,this._overriddenConfig?.cameras)&&Aa(e.cameras_global,this._overriddenConfig?.cameras_global)||this._initializer.uninitialize($b.CAMERAS),this._overriddenConfig=e)}_getSelectedCameraConfig(){return this._view&&this._cameraManager?this._cameraManager.getStore().getCameraConfig(this._view.camera):null}setConfig(e){if(!e)throw new Error(Pm("error.invalid_configuration"));const t=om.safeParse(e);if(!t.success){const n=Ov(e),i=ru(t.error);let a="";throw n&&"yaml"!==p().mode&&(a=`${Pm("error.upgrade_available")}. `),new Error(a+`${Pm("error.invalid_configuration")}: `+(i&&i.size?JSON.stringify([...i],null," "):Pm("error.invalid_configuration_no_hint")))}const n="low"!==t.data.performance.profile?t.data:Gy(e,t.data);n.test_gui&&p().setEditMode(!0),this._rawConfig=e,this._config=n,this._cardWideConfig={performance:n.performance,debug:n.debug},this._overriddenConfig=void 0,this._cameraManager=void 0,this._view=void 0,this._message=null,this._setupConditionController(),this._automationsController=new Nm(this._config.automations),this._setLightOrDarkMode(),this._setPropertiesForMinMaxHeight(),this._untrigger()}_setupConditionController(){this._conditionController?.destroy(),this._conditionController=new Xv(this._config),this._conditionController.addStateListener(this._overrideConfig.bind(this)),this._conditionController.addStateListener(this._requestUpdateForComponentsThatUseConditions.bind(this)),this._conditionController.addStateListener(this._executeAutomations.bind(this)),this._conditionController.setState({view:void 0,fullscreen:this._isInFullscreen(),expand:this._expand,camera:void 0,...this._hass&&this._conditionController?.hasHAStateConditions&&{state:this._hass.states},media_loaded:this._mediaLoadedInfoController.has()})}_executeAutomations(){if("error"!==this._message?.type&&this._hass&&this._conditionController)try{this._automationsController?.execute(this,this._hass,this._conditionController)}catch(e){this._handleThrownError(e)}}_getConfig(){return this._overriddenConfig||this._config}_changeView(e){hv(this._cardWideConfig,"Frigate Card view change: ",e?.view??e?.viewName??"[default]");const t=e=>{Oy.isMajorMediaChange(this._view,e)&&this._mediaLoadedInfoController.clear(),this._view?.view!==e.view&&this._resetMainScroll(),Oy.adoptFromViewIfAppropriate(e,this._view),this._view=e,this._conditionController?.setState({view:this._view.view,camera:this._view.camera})};if((e?.resetMessage??1)&&(this._message=null),e?.view)t(e.view);else{let n=null;if(this._cameraManager){const t=this._cameraManager.getStore().getVisibleCameras();if(t)if(e?.cameraID&&t.has(e.cameraID))n=e.cameraID;else if(this._view?.camera&&this._getConfig().view.update_cycle_camera){const e=Array.from(t.keys()),i=e.indexOf(this._view.camera);n=e[i+1>=e.length?0:i+1]}else n=t.keys().next().value}n&&(t(new Oy({view:e?.viewName??this._getConfig().view.default,camera:n})),this._startUpdateTimer())}}_setLightOrDarkMode(){"on"===this._getConfig().view.dark_mode||"auto"===this._getConfig().view.dark_mode&&(!this._interactionTimer.isRunning()||this._hass?.themes.darkMode)?this.setAttribute("dark",""):this.removeAttribute("dark")}_changeViewHandler(e){this._changeView({view:e.detail})}_addViewContextHandler(e){this._changeView({view:this._view?.clone().mergeInContext(e.detail)})}willUpdate(e){e.has("_cardWideConfig")&&((e,t)=>{const n=t?.style??{};for(const t of Object.keys(n)){const i=`--frigate-card-css-${t.replaceAll("_","-")}`;!1===n[t]?e.style.setProperty(i,Ky[t]):e.style.removeProperty(i)}})(this,this._cardWideConfig?.performance),e.has("_view")&&this._setPropertiesForExpandedMode();const t=e.get("_overriddenConfig")??e.get("_config"),n=this._getConfig();if((!this._microphoneController||e.has("_overriddenConfig")||e.has("_config"))&&t?.live.microphone.disconnect_seconds!==n.live.microphone.disconnect_seconds){const e=this._getConfig();this._microphoneController=new wb(e.live.microphone.always_connected?void 0:e.live.microphone.disconnect_seconds)}this._initializeBackground()}_setPropertiesForMinMaxHeight(){this.style.setProperty("--frigate-card-max-height",this._getConfig().dimensions.max_height),this.style.setProperty("--frigate-card-min-height",this._getConfig().dimensions.min_height)}_getMostRecentTrigger(){const e=[...this._triggers.entries()].sort(((e,t)=>t[1].getTime()-e[1].getTime()));return e.length?e[0][0]:null}_updateTriggeredCameras(e){if(!this._view||!this._isAutomatedViewUpdateAllowed(!0))return!1;const t=new Date;let n=!1,i=!1;const a=this._cameraManager?.getStore().getVisibleCameras();for(const[n,r]of a?.entries()??[]){const a=r.triggers.entities??[],o=dp(this._hass,e,a,{stateOnly:!0}).some((e=>bp(e.newState))),s=a.every((e=>!bp(this._hass?.states[e])));o?(this._triggers.set(n,t),i=!0):s&&this._triggers.has(n)&&(this._triggers.delete(n),i=!0)}if(i)if(this._triggers.size){const e=this._getMostRecentTrigger();!e||this._view.camera===e&&this._view.is("live")||(this._changeView({view:new Oy({view:"live",camera:e})}),n=!0)}else this._startUntriggerTimer();return n}_isTriggered(){return!!this._triggers.size||this._untriggerTimer.isRunning()}_untrigger(){const e=this._isTriggered();this._triggers.clear(),this._untriggerTimer.stop(),e&&this.requestUpdate()}_startUntriggerTimer(){this._untriggerTimer.start(this._getConfig().view.scan.untrigger_seconds,(()=>{this._untrigger(),this._isAutomatedViewUpdateAllowed()&&this._getConfig().view.scan.untrigger_reset&&this._changeView()}))}_handleThrownError(e){e instanceof Error&&Nf(e),e instanceof vu&&this._setMessageAndUpdate({message:e.message,type:"error",context:e.context})}async _initializeCameras(e,t,n){this._cameraManager=new _v(new dv(this._entityRegistryManager,this._resolvedMediaCache,n),this._cardWideConfig);const i=t.cameras.map((e=>tr(Gi(t.cameras_global),e)));try{await this._cameraManager.initializeCameras(e,this._entityRegistryManager,i)}catch(e){this._handleThrownError(e)}if(!this._view){xb().find((e=>(e=>{switch(e.frigate_card_action){case"clip":case"clips":case"image":case"live":case"recording":case"recordings":case"snapshot":case"snapshots":case"timeline":return!0}return!1})(e)||"diagnostics"===e.frigate_card_action))||this._changeView({resetMessage:!1})}}async _initializeMicrophone(){await(this._microphoneController?.connect())}async _initializeMediaPlayers(e){const t=Object.keys(this._hass?.states||{}).filter((e=>{if(e.startsWith("media_player.")){const t=this._hass?.states[e];if(t&&"unavailable"!==t.state&&Ym(t,131072))return!0}return!1}));let n;try{n=await this._entityRegistryManager.getEntities(e,t)}catch(e){return void Nf(e)}this._mediaPlayers=t.filter((e=>{const t=n.get(e);return!t||!t.hidden_by}))}_initializeMandatory(){if(this._initializer.isInitializedMultiple([$b.LANGUAGES,$b.SIDE_LOAD_ELEMENTS,$b.CAMERAS]))return!0;const e=this._hass,t=this._getConfig(),n=this._cardWideConfig;return!!(e&&t&&n)&&(this._initializer.initializeMultipleIfNecessary({[$b.LANGUAGES]:async()=>await(async e=>{const t=Dm(e);"it"===t?Im[t]=await import("./lang-it-0e2e946c.js"):"pt"===t?Im[t]=await import("./lang-pt-PT-440b6dfd.js"):"pt_BR"===t&&(Im[t]=await import("./lang-pt-BR-1648942c.js")),t&&(Rm=t)})(e),[$b.SIDE_LOAD_ELEMENTS]:async()=>await yp()}).then((i=>!!i&&this._initializer.initializeIfNecessary($b.CAMERAS,(async()=>await this._initializeCameras(e,t,n))))).then((e=>{if(e)return this.requestUpdate()})),!1)}_initializeBackground(){const e=this._hass,t=this._getConfig();e&&t&&(this._initializer.isInitializedMultiple([...t.menu.buttons.media_player.enabled?[$b.MEDIA_PLAYERS]:[],...t.live.microphone.always_connected?[$b.MICROPHONE]:[]])||this._initializer.initializeMultipleIfNecessary({...t.menu.buttons.media_player.enabled&&{[$b.MEDIA_PLAYERS]:async()=>await this._initializeMediaPlayers(e)},...t.live.microphone.always_connected&&{[$b.MICROPHONE]:async()=>await this._initializeMicrophone()}}).then((e=>{e&&this.requestUpdate()})))}shouldUpdate(e){if(!this._initializeMandatory())return!1;const t=e.get("_hass");let n=!t||1!=e.size;if(!t&&!this._hass?.connected||t&&t.connected!==!!this._hass?.connected)return this._hass?.connected?this._changeView():this._setMessageAndUpdate({message:Pm("error.reconnecting"),icon:"mdi:lan-disconnect",type:"connection",dotdotdot:!0},!0),!0;if(t){const e=this._getSelectedCameraConfig();this._getConfig().view.scan.enabled&&this._updateTriggeredCameras(t)?n=!0:this._isAutomatedViewUpdateAllowed()&&up(this._hass,t,[...this._getConfig().view.update_entities||[],...e?.triggers.entities||[]])?(this._changeView(),n=!0):n||=up(this._hass,t,[...this._getConfig().view.render_entities??[],...this._mediaPlayers??[]])}return n}async _downloadViewerMedia(){const e=this._view?.queryResults?.getSelectedResult();if(this._hass&&this._cameraManager&&e)try{await by(this._hass,this._cameraManager,e)}catch(e){this._handleThrownError(e)}}_mediaPlayerAction(e,t){if(!(["play","stop"].includes(t)&&this._view&&this._hass&&this._cameraManager))return;let n=null,i=null,a=null,r=null;const o=this._getSelectedCameraConfig();if(!o)return;const s=o.camera_entity??null,c=this._view.queryResults?.getSelectedResult();this._view.isViewerView()&&c?(n=c.getContentID(),i=c.getContentType(),a=c.getTitle(),r=c.getThumbnail()):this._view?.is("live")&&s&&(n=`media-source://camera/${s}`,i="application/vnd.apple.mpegurl",a=this._cameraManager.getCameraMetadata(this._hass,this._view.camera)?.title??null,r=this._hass?.states[s]?.attributes?.entity_picture??null),n&&i&&("play"===t?this._hass?.callService("media_player","play_media",{entity_id:e,media_content_id:n,media_content_type:i,extra:{...a&&{title:a},...r&&{thumb:r}}}):"stop"===t&&this._hass?.callService("media_player","media_stop",{entity_id:e}))}_cardActionEventHandler(e){if("detail"in e){const t=um(e.detail);t&&this._cardActionHandler(t)}}_cardActionHandler(e){if(!this._cameraManager)return;if(e.card_id&&this._getConfig().card_id!==e.card_id)return;const t=e.frigate_card_action;switch(t){case"default":this._changeView();break;case"clip":case"clips":case"image":case"live":case"recording":case"recordings":case"snapshot":case"snapshots":case"timeline":this._changeView({viewName:t,cameraID:this._view?.camera});break;case"download":this._downloadViewerMedia();break;case"camera_ui":const n=this._getCameraURLFromContext();n&&window.open(n);break;case"expand":this._setExpand(!this._expand);break;case"fullscreen":$r.toggle(this);break;case"menu_toggle":this._refMenu.value?.toggleMenu();break;case"camera_select":const i=e.camera;if(this._view&&this._cameraManager?.getStore().hasVisibleCameraID(i)){const e=this._getConfig().view.camera_select,t="current"===e?this._view.view:e,n=this.isViewSupportedByCamera(i,t)?t:uu;this._changeView({view:new Oy({view:n,camera:i})})}break;case"live_substream_select":if(this._view){const t=((e,t)=>{const n=e.context?.live?.overrides??new Map;return n.set(e.camera,t),e.clone().mergeInContext({live:{overrides:n}})})(this._view,e.camera);t&&this._changeView({view:t})}break;case"live_substream_off":if(this._view){const e=(e=>{const t=e.clone(),n=t.context?.live?.overrides;return n&&n.has(e.camera)&&t.context?.live?.overrides?.delete(e.camera),t})(this._view);e&&this._changeView({view:e})}break;case"live_substream_on":if(this._view){const e=((e,t)=>{const n=[...pv(e,t.camera)];if(n.length<=1)return t.clone();const i=t.clone(),a=i.context?.live?.overrides??new Map,r=a.get(i.camera)??i.camera,o=n.indexOf(r),s=o<0?0:(o+1)%n.length;return a.set(t.camera,n[s]),i.mergeInContext({live:{overrides:a}}),i})(this._cameraManager,this._view);e&&this._changeView({view:e})}break;case"media_player":this._mediaPlayerAction(e.media_player,e.media_player_action);break;case"diagnostics":this._diagnostics();break;case"microphone_mute":this._microphoneController?.mute(),this.requestUpdate();break;case"microphone_unmute":this._microphoneController?.isConnected()||this._microphoneController?.isForbidden()?this._microphoneController?.isConnected()&&(this._microphoneController.unmute(),this.requestUpdate()):(this._microphoneController?.unmute(),this._initializeMicrophone().then((()=>this.requestUpdate())));break;case"mute":this._mediaLoadedInfoController.get()?.player?.mute();break;case"unmute":this._mediaLoadedInfoController.get()?.player?.unmute();break;case"play":this._mediaLoadedInfoController.get()?.player?.play();break;case"pause":this._mediaLoadedInfoController.get()?.player?.pause();break;case"screenshot":this._mediaLoadedInfoController.get()?.player?.getScreenshotURL().then((e=>{e&&yy(e,(e=>{if(e?.is("live")||e?.is("image"))return`${e.view}-${e.camera}-${Of(new Date,"yyyy-MM-dd-HH-mm-ss")}.jpg`;if(e?.isViewerView()){const t=e.queryResults?.getSelectedResult(),n=t?.getID()??null;return`${e.view}-${e.camera}${n?`-${n}`:""}.jpg`}return"screenshot.jpg"})(this._view))}));break;default:console.warn(`Frigate card received unknown card action: ${t}`)}}isViewSupportedByCamera(e,t){const n=this._cameraManager?.getCameraCapabilities(e);switch(t){case"live":case"image":return!0;case"clip":case"clips":return!!n?.supportsClips;case"snapshot":case"snapshots":return!!n?.supportsSnapshots;case"recording":case"recordings":return!!n?.supportsRecordings;case"timeline":return!!n?.supportsTimeline;case"media":return!!n?.supportsClips||!!n?.supportsSnapshots||!!n?.supportsRecordings}return!1}async _diagnostics(){if(this._hass){let e=[];try{e=await(async e=>await cp(e,Xy,{type:"config/device_registry/list"}))(this._hass)}catch(e){}const t=e.filter((e=>"Frigate"===e.manufacturer)),n=new Map;t.forEach((e=>{e.config_entries.forEach((t=>{e.model&&n.set(t,e.model)}))})),this._setMessageAndUpdate({message:Pm("error.diagnostics"),type:"diagnostics",icon:"mdi:information",context:{ha_version:this._hass.config.version,card_version:Lr,browser:navigator.userAgent,date:new Date,frigate_version:Object.fromEntries(n),lang:Dm(),timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,git:{build_version:Fr,build_date:Nr,commit_date:Ur},...this._rawConfig&&{config:this._rawConfig}}})}}_getCameraURLFromContext(){if(!this._view)return null;const e=this._view.camera,t=this._view.queryResults?.getSelectedResult()??null;return(this._cameraManager?.getCameraEndpoints(e,{view:this._view.view,...t&&{media:t}})??null)?.ui?.endpoint??null}_actionHandler(e,t){const n=e.detail.action,i=e.currentTarget,a=mm(n,t);this._hass&&t&&i&&n&&a&&pm(i,this._hass,t,e.detail.action,a),this._startInteractionTimer()}_mouseHandler(){this._startInteractionTimer()}_startInteractionTimer(){this._interactionTimer.stop(),this._untrigger(),this._getConfig().view.timeout_seconds&&this._interactionTimer.start(this._getConfig().view.timeout_seconds,(()=>{this._isAutomatedViewUpdateAllowed()&&(this._changeView(),this._setLightOrDarkMode())})),this._setLightOrDarkMode()}_startUpdateTimer(){this._updateTimer.stop(),this._getConfig().view.update_seconds&&this._updateTimer.start(this._getConfig().view.update_seconds,(()=>{this._isAutomatedViewUpdateAllowed()?this._changeView():this._startUpdateTimer()}))}_isAutomatedViewUpdateAllowed(e){return(e||!this._isTriggered())&&(this._getConfig().view.update_force||!this._interactionTimer.isRunning())}_renderMenu(){if(this._hass&&this._cameraManager&&this._view)return K` + + `}_setMessageAndUpdate(e,t){const n=this._message?cm[this._message.type]??0:0,i=cm[e.type]??0;(!this._message||i>=n)&&(this._message=e,this._mediaUnloadedHandler(),t||(this.requestUpdate(),this._resetMainScroll()))}_resetMainScroll(){this._refMain.value?.scroll({top:0})}_messageHandler(e){return this._setMessageAndUpdate(e.detail)}_mediaLoadedHandler(e){const t=e.detail;_b(t)&&(hv(this._cardWideConfig,"Frigate Card media load: ",t),this._mediaLoadedInfoController.set(t),this._setPropertiesForExpandedMode(),this._conditionController?.setState({media_loaded:this._mediaLoadedInfoController.has()}),this.requestUpdate())}_setPropertiesForExpandedMode(){const e=this._mediaLoadedInfoController.getLastKnown();this.style.setProperty("--frigate-card-expand-aspect-ratio",this._view?.isAnyMediaView()&&e?`${e.width} / ${e.height}`:"unset"),this.style.setProperty("--frigate-card-expand-width",this._view?.isAnyMediaView()?"none":"var(--frigate-card-expand-max-width)"),this.style.setProperty("--frigate-card-expand-height",this._view?.isAnyMediaView()?"none":"var(--frigate-card-expand-max-height)")}_mediaUnloadedHandler(){this._mediaLoadedInfoController.clear(),this._conditionController?.setState({media_loaded:!1})}firstUpdated(){this._locationChangeHandler()}connectedCallback(){super.connectedCallback(),$r.isEnabled&&$r.on("change",this._boundFullscreenHandler),this.addEventListener("mousemove",this._boundMouseHandler),this.addEventListener("ll-custom",this._boundCardActionEventHandler),this._panel=(e=>{const t=e.getRootNode();return!!(t&&t instanceof ShadowRoot&&"HUI-PANEL-VIEW"===t.host.tagName)})(this),window.addEventListener("location-changed",this._locationChangeHandler),window.addEventListener("popstate",this._locationChangeHandler),this._locationChangeHandler()}disconnectedCallback(){this._mediaUnloadedHandler(),$r.isEnabled&&$r.off("change",this._boundFullscreenHandler),this.removeEventListener("mousemove",this._boundMouseHandler),this.removeEventListener("ll-custom",this._boundCardActionEventHandler),window.removeEventListener("location-changed",this._locationChangeHandler),window.removeEventListener("popstate",this._locationChangeHandler),super.disconnectedCallback()}_isAspectRatioEnforced(){const e=this._getConfig().dimensions.aspect_ratio_mode;return!($r.isEnabled&&$r.isFullscreen||this._expand||"unconstrained"==e||"dynamic"==e&&(this._view?.isAnyMediaView()||this._view?.is("timeline")))}_getAspectRatioStyle(){if(!this._isAspectRatioEnforced())return"auto";const e=this._getConfig().dimensions.aspect_ratio_mode,t=this._mediaLoadedInfoController.getLastKnown();if(t&&"dynamic"===e)return`${t.width} / ${t.height}`;const n=this._getConfig().dimensions.aspect_ratio;return n?`${n[0]} / ${n[1]}`:"16 / 9"}_getMergedActions(){if(this._message||this._view?.is("timeline"))return{};let e;return this._view?.is("live")?e=this._getConfig().live.actions:this._view?.isGalleryView()?e=this._getConfig().media_gallery?.actions:this._view?.isViewerView()?e=this._getConfig().media_viewer.actions:this._view?.is("image")&&(e=this._getConfig().image?.actions),{...this._getConfig().view.actions,...e}}_isInFullscreen(){return $r.isEnabled&&$r.isFullscreen}_setExpand(e){e&&this._isInFullscreen()&&$r.exit(),this._expand=e,this._conditionController?.setState({expand:this._expand})}_fullscreenHandler(){this._isInFullscreen()&&(this._expand=!1),this._conditionController?.setState({fullscreen:this._isInFullscreen(),expand:this._expand}),this.requestUpdate()}_renderInDialogIfNecessary(e){return this._expand?K` {this._setExpand(!1)}} + > + ${e} + `:e}render(){if(!this._hass)return;const e={"aspect-ratio":this._getAspectRatioStyle()},t={triggered:!!this._isTriggered()&&this._getConfig().view.scan.show_trigger_status},n={main:!0,"curve-top":"outside"!==this._getConfig().menu.style||"top"!==this._getConfig().menu.position,"curve-bottom":"outside"!==this._getConfig().menu.style||"top"===this._getConfig().menu.position},i=this._getMergedActions(),a="outside"===this._getConfig().menu.style&&"top"===this._getConfig().menu.position;return this._renderInDialogIfNecessary(K` this._actionHandler(e,i)} + @frigate-card:message=${this._messageHandler.bind(this)} + @frigate-card:view:change=${this._changeViewHandler.bind(this)} + @frigate-card:view:change-context=${this._addViewContextHandler.bind(this)} + @frigate-card:media:loaded=${this._mediaLoadedHandler.bind(this)} + @frigate-card:media:unloaded=${this._mediaUnloadedHandler.bind(this)} + @frigate-card:media:volumechange=${()=>this.requestUpdate()} + @frigate-card:media:play=${()=>this.requestUpdate()} + @frigate-card:media:pause=${()=>this.requestUpdate()} + @frigate-card:render=${()=>this.requestUpdate()} + > + ${a?this._renderMenu():""} +
+ ${this._cameraManager?.isInitialized()||this._message?K``:$v({cardWideConfig:this._cardWideConfig})} + ${this._message?Cv(this._message):""} +
+ ${a?"":this._renderMenu()} + ${this._getConfig().elements?K` {this._menuButtonController.addDynamicMenuButton(e.detail),this.requestUpdate()}} + @frigate-card:menu-remove=${e=>{this._menuButtonController.removeDynamicMenuButton(e.detail),this.requestUpdate()}} + @frigate-card:condition:evaluate=${e=>{e.evaluation=this._conditionController?.evaluateCondition(e.condition)}} + > + `:""} +
`)}static get styles(){return b(":host {\n display: block;\n position: relative;\n background-color: var(--card-background-color);\n border-radius: var(--ha-card-border-radius, 4px);\n max-height: var(--frigate-card-max-height);\n min-height: var(--frigate-card-min-height);\n --frigate-card-expand-max-height: calc( ( 100vh - (2 * 56px) ) * 0.85 );\n --frigate-card-expand-max-width: 85vw;\n --frigate-card-expand-width: none;\n --frigate-card-expand-height: none;\n --frigate-card-expand-aspect-ratio: unset;\n --frigate-card-max-height: none;\n --frigate-card-min-height: none;\n}\n\n:host([dark]) {\n filter: brightness(75%);\n}\n\n:host([panel]) {\n height: 100%;\n}\n\ndiv.main {\n position: relative;\n overflow: auto;\n width: 100%;\n height: 100%;\n margin: auto;\n display: block;\n transform: translateZ(0);\n box-sizing: border-box;\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n/* Hide scrollbar for Chrome, Safari and Opera */\ndiv.main::-webkit-scrollbar {\n display: none;\n}\n\ndiv.main.curve-top {\n border-top-left-radius: var(--ha-card-border-radius, 4px);\n border-top-right-radius: var(--ha-card-border-radius, 4px);\n}\n\ndiv.main.curve-bottom {\n border-bottom-left-radius: var(--ha-card-border-radius, 4px);\n border-bottom-right-radius: var(--ha-card-border-radius, 4px);\n}\n\n/* The 'hover' menu mode is styled applied outside of the menu itself */\nfrigate-card-menu[data-style*=hover] {\n z-index: 1;\n transition: opacity 0.5s ease;\n}\n\n.main + frigate-card-menu[data-style*=hover] {\n opacity: 0;\n}\n\nfrigate-card-menu[data-style=hover]:hover {\n opacity: 1;\n}\n\n.main:hover + frigate-card-menu[data-style=hover-card],\nfrigate-card-menu[data-style=hover-card]:hover {\n opacity: 1;\n}\n\nha-card {\n display: flex;\n flex-direction: column;\n margin: auto;\n border: 0px;\n overflow: visible;\n width: 100%;\n height: 100%;\n position: static;\n color: var(--secondary-text-color, white);\n}\n\nha-card.triggered {\n animation: warning-pulse 5s infinite;\n}\n@keyframes warning-pulse {\n 0% {\n border: solid 2px rgba(0, 0, 0, 0);\n }\n 50% {\n border: solid 2px var(--warning-color);\n }\n 100% {\n border: solid 2px rgba(0, 0, 0, 0);\n }\n}\n\n/************\n * Fullscreen\n *************/\n:host(:fullscreen) #ha-card {\n border-radius: 0px;\n box-shadow: none;\n margin: 0;\n}\n\n:host(:-webkit-full-screen) #ha-card {\n border-radius: 0px;\n box-shadow: none;\n margin: 0;\n}\n\n:host(:fullscreen) div.main,\n:host(:fullscreen) frigate-card-menu {\n border-radius: 0px;\n}\n\n:host(:-webkit-full-screen) div.main,\n:host(:-webkit-full-screen) frigate-card-menu {\n border-radius: 0px;\n}\n\n/***************\n * Expanded mode\n ***************/\nweb-dialog {\n --dialog-padding: 0px;\n --dialog-container-padding: 0px;\n --dialog-max-height: var(--frigate-card-expand-max-height);\n --dialog-max-width: var(--frigate-card-expand-max-width);\n --dialog-width: var(--frigate-card-expand-width);\n --dialog-height: var(--frigate-card-expand-height);\n --dialog-overflow-x: visible;\n --dialog-overflow-y: visible;\n max-height: 100vh;\n}\n\nweb-dialog::part(dialog) {\n aspect-ratio: var(--frigate-card-expand-aspect-ratio);\n border-radius: 0px;\n background: transparent;\n}")}getCardSize(){const e=this._mediaLoadedInfoController.getLastKnown();return e?e.height/50:6}};e([we()],kb.prototype,"_hass",void 0),e([we()],kb.prototype,"_config",void 0),e([we()],kb.prototype,"_cardWideConfig",void 0),e([we()],kb.prototype,"_overriddenConfig",void 0),e([we()],kb.prototype,"_view",void 0),e([be({attribute:"panel",type:Boolean,reflect:!0})],kb.prototype,"_panel",void 0),e([we()],kb.prototype,"_expand",void 0),kb=e([_e("frigate-card")],kb);export{nd as $,wp as A,su as B,Pc as C,jv as D,jc as E,pu as F,Ac as G,zc as H,Oc as I,Dc as J,Rc as K,Ic as L,Td as M,Sd as N,Md as O,Ad as P,ed as Q,Kl as R,rd as S,lu as T,Xl as U,Jl as V,El as W,Ml as X,Sl as Y,Tl as Z,td as _,Gy as a,cd as a$,Al as a0,zl as a1,jl as a2,Rl as a3,Dl as a4,Ll as a5,Nl as a6,Pl as a7,Ol as a8,Il as a9,il as aA,nl as aB,al as aC,rl as aD,ol as aE,hl as aF,cl as aG,dl as aH,ul as aI,ll as aJ,sl as aK,bl as aL,wl as aM,fl as aN,vl as aO,_l as aP,ml as aQ,pl as aR,gl as aS,xl as aT,Cl as aU,$l as aV,dd as aW,hd as aX,ud as aY,md as aZ,sd as a_,Wl as aa,Bl as ab,Hl as ac,ql as ad,Vl as ae,Ul as af,Fl as ag,Zl as ah,Yl as ai,Ql as aj,Gl as ak,id as al,ad as am,Yc as an,qc as ao,Wc as ap,Bc as aq,Vc as ar,Zc as as,Gc as at,Kc as au,Xc as av,Jc as aw,el as ax,yl as ay,tl as az,yp as b,Ff as b$,ld as b0,wd as b1,xd as b2,$d as b3,kd as b4,Cd as b5,bd as b6,Vd as b7,qd as b8,Wd as b9,mc as bA,lc as bB,dc as bC,uc as bD,hc as bE,nc as bF,ic as bG,sc as bH,cc as bI,fc as bJ,gc as bK,yc as bL,_c as bM,wc as bN,bc as bO,xc as bP,sm as bQ,Qs as bR,Qg as bS,_p as bT,Ws as bU,Wf as bV,cp as bW,vu as bX,Fm as bY,Zm as bZ,Um as b_,Bd as ba,Xd as bb,Gd as bc,Kd as bd,tu as be,eu as bf,Jd as bg,zv as bh,Tv as bi,b as bj,e as bk,be as bl,we as bm,_e as bn,Df as bo,Gs as bp,vc as bq,pc as br,rc as bs,oc as bt,ac as bu,Ks as bv,ec as bw,Js as bx,tc as by,Xs as bz,Iv as c,_b as c$,Aa as c0,rv as c1,yr as c2,lv as c3,Wg as c4,_y as c5,Bg as c6,Zf as c7,Vf as c8,Yg as c9,Mf as cA,mf as cB,_f as cC,yf as cD,vf as cE,Op as cF,pf as cG,$e as cH,ke as cI,J as cJ,Ae as cK,Ev as cL,kv as cM,mb as cN,Lf as cO,Le as cP,dy as cQ,Kv as cR,Fe as cS,Iy as cT,vm as cU,pb as cV,X as cW,_m as cX,vy as cY,hy as cZ,vr as c_,Bm as ca,hv as cb,Gg as cc,Kg as cd,Rg as ce,uv as cf,Of as cg,Hf as ch,Kf as ci,Xf as cj,rb as ck,xp as cl,Jf as cm,Ug as cn,Yf as co,Xg as cp,Jg as cq,$p as cr,Cp as cs,kp as ct,Hp as cu,Fp as cv,Rp as cw,qp as cx,Lp as cy,Up as cz,vp as d,c_ as d0,If as d1,W_ as d2,x as d3,Ay as d4,Vy as d5,zy as d6,jy as d7,Nf as d8,Zy as d9,qy as dA,Ap as dB,Qf as dC,mr as dD,Qe as dE,fg as dF,Pe as dG,Me as dH,lp as dI,fy as dJ,Fy as da,$v as db,Bf as dc,wy as dd,Uy as de,Cv as df,qm as dg,Vm as dh,Cb as di,hb as dj,fb as dk,gb as dl,vb as dm,w as dn,Ce as dp,Se as dq,up as dr,ub as ds,Pg as dt,Lg as du,ov as dv,sv as dw,Fg as dx,Uf as dy,Wy as dz,Lc as e,Nc as f,Av as g,Uc as h,Ov as i,Fc as j,zd as k,Pm as l,yd as m,fd as n,Ee as o,Rf as p,gd as q,vd as r,ge as s,_d as t,cu as u,mv as v,Lv as w,l_ as x,K as y,l as z}; diff --git a/www/frigate-card/editor-7b16019d.js b/www/frigate-card/editor-7b16019d.js new file mode 100644 index 00000000..e6ce79b4 --- /dev/null +++ b/www/frigate-card/editor-7b16019d.js @@ -0,0 +1,381 @@ +import{l as e,s as a,c as n,i as t,a as i,b as o,y as s,g as r,d as l,p as d,o as c,C as m,e as u,f as h,h as _,j as b,k as g,F as p,m as v,n as f,q as $,r as y,t as w,B as S,T as M,u as x,v as I,w as C,x as O,z as k,A as N,D as T,E as B,G as A,H as P,I as H,J as z,K as L,L as E,M as j,N as q,O as U,P as Z,Q as F,R as V,S as R,U as D,V as G,W as J,X as K,Y as Q,Z as W,_ as X,$ as Y,a0 as ee,a1 as ae,a2 as ne,a3 as te,a4 as ie,a5 as oe,a6 as se,a7 as re,a8 as le,a9 as de,aa as ce,ab as me,ac as ue,ad as he,ae as _e,af as be,ag as ge,ah as pe,ai as ve,aj as fe,ak as $e,al as ye,am as we,an as Se,ao as Me,ap as xe,aq as Ie,ar as Ce,as as Oe,at as ke,au as Ne,av as Te,aw as Be,ax as Ae,ay as Pe,az as He,aA as ze,aB as Le,aC as Ee,aD as je,aE as qe,aF as Ue,aG as Ze,aH as Fe,aI as Ve,aJ as Re,aK as De,aL as Ge,aM as Je,aN as Ke,aO as Qe,aP as We,aQ as Xe,aR as Ye,aS as ea,aT as aa,aU as na,aV as ta,aW as ia,aX as oa,aY as sa,aZ as ra,a_ as la,a$ as da,b0 as ca,b1 as ma,b2 as ua,b3 as ha,b4 as _a,b5 as ba,b6 as ga,b7 as pa,b8 as va,b9 as fa,ba as $a,bb as ya,bc as wa,bd as Sa,be as Ma,bf as xa,bg as Ia,bh as Ca,bi as Oa,bj as ka,bk as Na,bl as Ta,bm as Ba,bn as Aa,bo as Pa,bp as Ha,bq as za,br as La,bs as Ea,bt as ja,bu as qa,bv as Ua,bw as Za,bx as Fa,by as Va,bz as Ra,bA as Da,bB as Ga,bC as Ja,bD as Ka,bE as Qa,bF as Wa,bG as Xa,bH as Ya,bI as en,bJ as an,bK as nn,bL as tn,bM as on,bN as sn,bO as rn,bP as ln,bQ as dn,bR as cn}from"./card-555679fd.js";const mn="buttons",un="cameras",hn="options",_n="scan",bn={cameras:{icon:"video",name:e("editor.cameras"),secondary:e("editor.cameras_secondary")},view:{icon:"eye",name:e("editor.view"),secondary:e("editor.view_secondary")},menu:{icon:"menu",name:e("editor.menu"),secondary:e("editor.menu_secondary")},live:{icon:"cctv",name:e("editor.live"),secondary:e("editor.live_secondary")},media_gallery:{icon:"grid",name:e("editor.media_gallery"),secondary:e("editor.media_gallery_secondary")},media_viewer:{icon:"filmstrip",name:e("editor.media_viewer"),secondary:e("editor.media_viewer_secondary")},image:{icon:"image",name:e("editor.image"),secondary:e("editor.image_secondary")},timeline:{icon:"chart-gantt",name:e("editor.timeline"),secondary:e("editor.timeline_secondary")},dimensions:{icon:"aspect-ratio",name:e("editor.dimensions"),secondary:e("editor.dimensions_secondary")},performance:{icon:"speedometer",name:e("editor.performance"),secondary:e("editor.performance_secondary")},overrides:{icon:"file-replace",name:e("editor.overrides"),secondary:e("editor.overrides_secondary")}};let gn=class extends a{constructor(){super(...arguments),this._defaults=n(dn),this._initialized=!1,this._configUpgradeable=!1,this._expandedMenus={},this._viewModes=[{value:"",label:""},{value:"live",label:e("config.view.views.live")},{value:"clips",label:e("config.view.views.clips")},{value:"snapshots",label:e("config.view.views.snapshots")},{value:"recordings",label:e("config.view.views.recordings")},{value:"clip",label:e("config.view.views.clip")},{value:"snapshot",label:e("config.view.views.snapshot")},{value:"recording",label:e("config.view.views.recording")},{value:"image",label:e("config.view.views.image")},{value:"timeline",label:e("config.view.views.timeline")}],this._cameraSelectViewModes=[...this._viewModes,{value:"current",label:e("config.view.views.current")}],this._filterModes=[{value:"",label:""},{value:"none",label:e("config.common.controls.filter.modes.none")},{value:"left",label:e("config.common.controls.filter.modes.left")},{value:"right",label:e("config.common.controls.filter.modes.right")}],this._menuStyles=[{value:"",label:""},{value:"none",label:e("config.menu.styles.none")},{value:"hidden",label:e("config.menu.styles.hidden")},{value:"overlay",label:e("config.menu.styles.overlay")},{value:"hover",label:e("config.menu.styles.hover")},{value:"hover-card",label:e("config.menu.styles.hover-card")},{value:"outside",label:e("config.menu.styles.outside")}],this._menuPositions=[{value:"",label:""},{value:"left",label:e("config.menu.positions.left")},{value:"right",label:e("config.menu.positions.right")},{value:"top",label:e("config.menu.positions.top")},{value:"bottom",label:e("config.menu.positions.bottom")}],this._menuAlignments=[{value:"",label:""},{value:"left",label:e("config.menu.alignments.left")},{value:"right",label:e("config.menu.alignments.right")},{value:"top",label:e("config.menu.alignments.top")},{value:"bottom",label:e("config.menu.alignments.bottom")}],this._nextPreviousControlStyles=[{value:"",label:""},{value:"chevrons",label:e("config.common.controls.next_previous.styles.chevrons")},{value:"icons",label:e("config.common.controls.next_previous.styles.icons")},{value:"none",label:e("config.common.controls.next_previous.styles.none")},{value:"thumbnails",label:e("config.common.controls.next_previous.styles.thumbnails")}],this._aspectRatioModes=[{value:"",label:""},{value:"dynamic",label:e("config.dimensions.aspect_ratio_modes.dynamic")},{value:"static",label:e("config.dimensions.aspect_ratio_modes.static")},{value:"unconstrained",label:e("config.dimensions.aspect_ratio_modes.unconstrained")}],this._thumbnailModes=[{value:"",label:""},{value:"none",label:e("config.common.controls.thumbnails.modes.none")},{value:"above",label:e("config.common.controls.thumbnails.modes.above")},{value:"below",label:e("config.common.controls.thumbnails.modes.below")},{value:"left",label:e("config.common.controls.thumbnails.modes.left")},{value:"right",label:e("config.common.controls.thumbnails.modes.right")}],this._thumbnailMedias=[{value:"",label:""},{value:"clips",label:e("config.common.controls.thumbnails.medias.clips")},{value:"snapshots",label:e("config.common.controls.thumbnails.medias.snapshots")}],this._titleModes=[{value:"",label:""},{value:"none",label:e("config.common.controls.title.modes.none")},{value:"popup-top-left",label:e("config.common.controls.title.modes.popup-top-left")},{value:"popup-top-right",label:e("config.common.controls.title.modes.popup-top-right")},{value:"popup-bottom-left",label:e("config.common.controls.title.modes.popup-bottom-left")},{value:"popup-bottom-right",label:e("config.common.controls.title.modes.popup-bottom-right")}],this._transitionEffects=[{value:"",label:""},{value:"none",label:e("config.media_viewer.transition_effects.none")},{value:"slide",label:e("config.media_viewer.transition_effects.slide")}],this._imageModes=[{value:"",label:""},{value:"camera",label:e("config.image.modes.camera")},{value:"screensaver",label:e("config.image.modes.screensaver")},{value:"url",label:e("config.image.modes.url")}],this._timelineMediaTypes=[{value:"",label:""},{value:"all",label:e("config.common.timeline.medias.all")},{value:"clips",label:e("config.common.timeline.medias.clips")},{value:"snapshots",label:e("config.common.timeline.medias.snapshots")}],this._timelineStyleTypes=[{value:"",label:""},{value:"ribbon",label:e("config.common.timeline.styles.ribbon")},{value:"stack",label:e("config.common.timeline.styles.stack")}],this._darkModes=[{value:"",label:""},{value:"on",label:e("config.view.dark_modes.on")},{value:"off",label:e("config.view.dark_modes.off")},{value:"auto",label:e("config.view.dark_modes.auto")}],this._mediaActionNegativeConditions=[{value:"",label:""},{value:"all",label:e("config.common.media_action_conditions.all")},{value:"unselected",label:e("config.common.media_action_conditions.unselected")},{value:"hidden",label:e("config.common.media_action_conditions.hidden")},{value:"never",label:e("config.common.media_action_conditions.never")}],this._mediaActionPositiveConditions=[{value:"",label:""},{value:"all",label:e("config.common.media_action_conditions.all")},{value:"selected",label:e("config.common.media_action_conditions.selected")},{value:"visible",label:e("config.common.media_action_conditions.visible")},{value:"never",label:e("config.common.media_action_conditions.never")}],this._layoutFits=[{value:"",label:""},{value:"contain",label:e("config.common.layout.fits.contain")},{value:"cover",label:e("config.common.layout.fits.cover")},{value:"fill",label:e("config.common.layout.fits.fill")}],this._miniTimelineModes=[{value:"",label:""},{value:"none",label:e("config.common.controls.timeline.modes.none")},{value:"above",label:e("config.common.controls.timeline.modes.above")},{value:"below",label:e("config.common.controls.timeline.modes.below")}],this._performanceProfiles=[{value:"",label:""},{value:"low",label:e("config.performance.profiles.low")},{value:"high",label:e("config.performance.profiles.high")}],this._go2rtcModes=[{value:"",label:""},{value:"mse",label:e("config.cameras.go2rtc.modes.mse")},{value:"webrtc",label:e("config.cameras.go2rtc.modes.webrtc")},{value:"mp4",label:e("config.cameras.go2rtc.modes.mp4")},{value:"mjpeg",label:e("config.cameras.go2rtc.modes.mjpeg")}],this._microphoneButtonTypes=[{value:"",label:""},{value:"momentary",label:e("config.menu.buttons.types.momentary")},{value:"toggle",label:e("config.menu.buttons.types.toggle")}]}setConfig(e){this._config=e,this._configUpgradeable=t(e);let a=null;try{a=this._config.performance?.profile}catch(e){}if("high"===a||"low"===a){const e=n(dn);"low"===a&&i(this._config,e),this._defaults=e}}willUpdate(){this._initialized||o().then((e=>{e&&(this._initialized=!0)}))}_renderOptionSetHeader(e,a){const n=bn[e];return s` +
+
+ +
${n.name}
+
+
${n.secondary}
+
+ `}_getLabel(a){const n=a.split(".").filter((e=>!e.match(/^\[[0-9]+\]$/))).join(".");return e(`config.${n}`)}_renderEntitySelector(e,a){if(this._config)return s` + this._valueChangedHandler(e,a)} + > + + `}_renderOptionSelector(e,a=[],n){if(this._config)return s` + this._valueChangedHandler(e,a)} + > + + `}_renderIconSelector(e,a){if(this._config)return s` + this._valueChangedHandler(e,a)} + > + + `}_renderNumberInput(e,a){if(!this._config)return;const n=r(this._config,e),t=void 0===a?.max?"box":"slider";return s` + this._valueChangedHandler(e,a)} + > + + `}_renderInfo(e){return s` ${e}`}_getEditorCameraTitle(a,n){return"string"==typeof n?.title&&n.title||("string"==typeof n?.camera_entity?l(this.hass,n.camera_entity):"")||"object"==typeof n?.webrtc_card&&n.webrtc_card&&"string"==typeof n.webrtc_card.entity&&n.webrtc_card.entity||("object"==typeof n?.frigate&&n.frigate&&"string"==typeof n?.frigate.camera_name&&n.frigate.camera_name?d(n.frigate.camera_name):"")||"string"==typeof n?.id&&n.id||e("editor.camera")+" #"+a}_renderViewScanMenu(){const a={submenu:!0,selected:!!this._expandedMenus[_n]};return s` +
+ + ${this._expandedMenus[_n]?s`
+ ${this._renderSwitch(u,this._defaults.view.scan.enabled,{label:e(`config.${u}`)})} + ${this._renderSwitch(h,this._defaults.view.scan.show_trigger_status,{label:e(`config.${h}`)})} + ${this._renderSwitch(_,this._defaults.view.scan.untrigger_reset)} + ${this._renderNumberInput(b,{default:this._defaults.view.scan.untrigger_seconds})} +
`:""} +
+ `}_renderMenuButton(a,n){const t=[{value:"",label:""},{value:"matching",label:e("config.menu.buttons.alignments.matching")},{value:"opposing",label:e("config.menu.buttons.alignments.opposing")}],i={submenu:!0,selected:this._expandedMenus[mn]===a};return s` +
+ + + ${this._expandedMenus[mn]===a?s`
+ ${this._renderSwitch(`${g}.${a}.enabled`,this._defaults.menu.buttons[a]?.enabled??!0,{label:e("config.menu.buttons.enabled")})} + ${this._renderOptionSelector(`${g}.${a}.alignment`,t,{label:e("config.menu.buttons.alignment")})} + ${this._renderNumberInput(`${g}.${a}.priority`,{max:p,default:this._defaults.menu.buttons[a]?.priority,label:e("config.menu.buttons.priority")})} + ${this._renderIconSelector(`${g}.${a}.icon`,{label:e("config.menu.buttons.icon")})} + ${n} +
`:""} +
+ `}_putInSubmenu(a,n,t,i,o){const r=this._expandedMenus[a]===n;return s`
+ + ${r?s`
${o}
`:""} +
`}_renderMediaLayout(a,n,t,i,o){return this._putInSubmenu(a,!0,n,{name:"mdi:page-layout-body"},s` + ${this._renderOptionSelector(t,this._layoutFits)} + ${this._renderNumberInput(i,{min:0,max:100,label:e("config.common.layout.position.x")})} + ${this._renderNumberInput(o,{min:0,max:100,label:e("config.common.layout.position.y")})} + `)}_renderTimelineCoreControls(a,n,t,i,o,r){return s` ${this._renderOptionSelector(a,this._timelineStyleTypes,{label:e(`config.common.${v}`)})} + ${this._renderNumberInput(n,{label:e(`config.common.${f}`)})} + ${this._renderNumberInput(t,{label:e(`config.common.${$}`)})} + ${this._renderOptionSelector(i,this._timelineMediaTypes,{label:e(`config.common.${y}`)})} + ${this._renderSwitch(o,r,{label:e(`config.common.${w}`)})}`}_renderMiniTimeline(a,n,t,i,o,r,l,d){return this._putInSubmenu(a,!0,"config.common.controls.timeline.editor_label",{name:"mdi:chart-gantt"},s` ${this._renderOptionSelector(n,this._miniTimelineModes,{label:e("config.common.controls.timeline.mode")})} + ${this._renderTimelineCoreControls(t,i,o,r,l,d)}`)}_renderNextPreviousControls(a,n,t,i){return this._putInSubmenu(a,!0,"config.common.controls.next_previous.editor_label",{name:"mdi:arrow-right-bold-circle"},s` + ${this._renderOptionSelector(n,this._nextPreviousControlStyles.filter((e=>!(!i?.allowThumbnails&&"thumbnails"===e.value||!i?.allowIcons&&"icons"===e.value))),{label:e("config.common.controls.next_previous.style")})} + ${this._renderNumberInput(t,{min:S,label:e("config.common.controls.next_previous.size")})} + `)}_renderThumbnailsControls(a,n,t,i,o,r,l,d){return this._putInSubmenu(a,!0,"config.common.controls.thumbnails.editor_label",{name:"mdi:image-text"},s` + ${d?.configPathMode?s`${this._renderOptionSelector(d.configPathMode,this._thumbnailModes,{label:e("config.common.controls.thumbnails.mode")})}`:s``} + ${d?.configPathMedia?s`${this._renderOptionSelector(d.configPathMedia,this._thumbnailMedias,{label:e("config.common.controls.thumbnails.media")})}`:s``} + ${this._renderNumberInput(n,{min:M,max:x,label:e("config.common.controls.thumbnails.size")})} + ${this._renderSwitch(t,l.show_details,{label:e("config.common.controls.thumbnails.show_details")})} + ${this._renderSwitch(i,l.show_favorite_control,{label:e("config.common.controls.thumbnails.show_favorite_control")})} + ${this._renderSwitch(o,l.show_timeline_control,{label:e("config.common.controls.thumbnails.show_timeline_control")})} + ${this._renderSwitch(r,l.show_download_control,{label:e("config.common.controls.thumbnails.show_download_control")})} + `)}_renderFilterControls(a,n){return this._putInSubmenu(a,!0,"config.common.controls.filter.editor_label",{name:"mdi:filter-cog"},s` + ${n?s`${this._renderOptionSelector(n,this._filterModes,{label:e("config.common.controls.filter.mode")})}`:s``} + `)}_renderTitleControls(a,n,t){return this._putInSubmenu(a,!0,"config.common.controls.title.editor_label",{name:"mdi:subtitles"},s` ${this._renderOptionSelector(n,this._titleModes,{label:e("config.common.controls.title.mode")})} + ${this._renderNumberInput(t,{min:0,max:60,label:e("config.common.controls.title.duration_seconds")})}`)}_renderCamera(a,t,i,o){const r=[{value:"",label:""},{value:"auto",label:e("config.cameras.live_providers.auto")},{value:"ha",label:e("config.cameras.live_providers.ha")},{value:"image",label:e("config.cameras.live_providers.image")},{value:"jsmpeg",label:e("config.cameras.live_providers.jsmpeg")},{value:"go2rtc",label:e("config.cameras.live_providers.go2rtc")},{value:"webrtc-card",label:e("config.cameras.live_providers.webrtc-card")}],l=[];a.forEach(((e,a)=>{a!==t&&l.push({value:I(e),label:this._getEditorCameraTitle(a,e)})}));const d=e=>{if(this._config){const a=n(this._config);e(a)&&this._updateConfig(a)}},m={submenu:!0,selected:this._expandedMenus[un]===t};return s` +
+ + ${this._expandedMenus[un]===t?s`
+
+ !o&&d((e=>!!(Array.isArray(e.cameras)&&t>0)&&(Pa(e.cameras,t,t-1),this._openMenu(un,t-1),!0)))} + > + + + =this._config.cameras.length-1} + @click=${()=>!o&&d((e=>!!(Array.isArray(e.cameras)&&t + + + {d((e=>!!Array.isArray(e.cameras)&&(e.cameras.splice(t,1),this._closeMenu(un),!0)))}} + > + + +
+ ${this._renderEntitySelector(C(Ha,t),"camera")} + ${this._renderOptionSelector(C(za,t),r)} + ${this._renderStringInput(C(La,t))} + ${this._renderIconSelector(C(Ea,t),{label:e("config.cameras.icon")})} + ${this._renderStringInput(C(ja,t))} + ${this._renderSwitch(C(qa,t),this._defaults.cameras.hide)} + ${this._putInSubmenu("cameras.engine",!0,"config.cameras.engines.editor_label",{name:"mdi:engine"},s`${this._putInSubmenu("cameras.frigate",t,"config.cameras.frigate.editor_label",{path:O},s` + ${this._renderStringInput(C(Ua,t))} + ${this._renderStringInput(C(Za,t))} + ${this._renderOptionSelector(C(Fa,t),[],{multiple:!0,label:e("config.cameras.frigate.labels")})} + ${this._renderOptionSelector(C(Va,t),[],{multiple:!0,label:e("config.cameras.frigate.zones")})} + ${this._renderStringInput(C(Ra,t))} + `)} + ${this._putInSubmenu("cameras.motioneye",t,"config.cameras.motioneye.editor_label",{path:"M 49.65,10.81 C 44.24,10.84 36.85,13.50 31.48,15.96 25.84,13.92 20.04,10.69 13.50,10.84 13.07,10.85 12.65,10.87 12.20,10.91 12.20,10.91 7.08,11.33 7.08,11.33 7.08,11.33 11.94,12.95 11.94,12.95 18.62,15.13 24.49,16.51 29.66,25.48 30.86,25.48 33.22,25.48 34.34,25.48 39.49,16.57 45.66,15.08 52.02,12.95 52.02,12.95 56.83,11.39 56.83,11.39 56.83,11.39 51.83,10.91 51.83,10.91 51.15,10.84 50.43,10.80 49.65,10.81 49.65,10.81 49.65,10.81 49.65,10.81 Z M 32.00,5.00 C 26.53,5.00 21.45,6.75 17.20,9.54 21.80,10.04 26.33,11.22 31.48,13.76 36.69,11.11 42.02,10.00 46.83,9.45 42.57,6.64 37.48,5.00 32.00,5.00 Z M 43.42,22.65 C 41.70,22.65 40.31,24.05 40.31,25.77 40.31,27.49 41.70,28.88 43.42,28.88 45.14,28.88 46.54,27.49 46.54,25.77 46.54,24.05 45.14,22.65 43.42,22.65 Z M 20.58,22.65 C 18.86,22.65 17.46,24.05 17.46,25.77 17.46,27.49 18.86,28.88 20.58,28.88 22.30,28.88 23.69,27.49 23.69,25.77 23.69,24.05 22.30,22.65 20.58,22.65 Z M 11.91,14.02 C 7.61,18.80 5.00,25.06 5.00,32.00 5.00,46.91 17.09,59.00 32.00,59.00 46.91,59.00 59.00,46.91 59.00,32.00 59.00,25.09 56.40,18.80 52.12,14.02 50.08,14.77 48.04,15.65 46.02,16.78 49.92,17.91 52.77,21.53 52.77,25.77 52.77,30.90 48.59,35.12 43.42,35.12 39.04,35.12 35.36,32.09 34.34,28.04 34.34,28.04 29.66,28.04 29.66,28.04 28.65,32.09 24.96,35.12 20.58,35.12 15.41,35.12 11.20,30.90 11.20,25.77 11.20,21.48 14.16,17.83 18.14,16.75 16.12,15.65 14.04,14.79 11.91,14.02 11.91,14.02 11.91,14.02 11.91,14.02 Z M 32.00,30.96 C 32.64,33.35 33.33,35.72 36.15,37.19 36.15,37.19 32.00,43.42 32.00,43.42 32.00,43.42 27.85,37.19 27.85,37.19 30.32,35.44 31.46,33.29 32.00,30.96 Z",viewBox:"0 0 64 64"},s` + ${this._renderStringInput(C(Da,t))} + ${this._renderStringInput(C(Ga,t))} + ${this._renderStringInput(C(Ja,t))} + ${this._renderStringInput(C(Ka,t))} + ${this._renderStringInput(C(Qa,t))} + `)} `)} + ${this._putInSubmenu("cameras.live_provider",!0,"config.cameras.live_provider_options.editor_label",{name:"mdi:cctv"},s` ${this._putInSubmenu("cameras.go2rtc",t,"config.cameras.go2rtc.editor_label",{name:"mdi:alpha-g-circle"},s`${this._renderOptionSelector(C(Wa,t),this._go2rtcModes,{multiple:!0,label:e("config.cameras.go2rtc.modes.editor_label")})} + ${this._renderStringInput(C(Xa,t))}`)} + ${this._putInSubmenu("cameras.image",!0,"config.cameras.image.editor_label",{name:"mdi:image"},s` + ${this._renderNumberInput(C(Ya,t))} + ${this._renderStringInput(C(en,t))} + `)} + ${this._putInSubmenu("cameras.webrtc_card",t,"config.cameras.webrtc_card.editor_label",{name:"mdi:webrtc"},s`${this._renderEntitySelector(C(an,t),"camera")} + ${this._renderStringInput(C(nn,t))}`)}`)} + ${this._putInSubmenu("cameras.dependencies",t,"config.cameras.dependencies.editor_label",{name:"mdi:graph"},s` ${this._renderSwitch(C(tn,t),this._defaults.cameras.dependencies.all_cameras)} + ${this._renderOptionSelector(C(on,t),l,{multiple:!0})}`)} + ${this._putInSubmenu("cameras.triggers",t,"config.cameras.triggers.editor_label",{name:"mdi:magnify-scan"},s` ${this._renderSwitch(C(sn,t),this._defaults.cameras.triggers.occupancy)} + ${this._renderSwitch(C(rn,t),this._defaults.cameras.triggers.motion)} + ${this._renderOptionSelector(C(ln,t),i,{multiple:!0})}`)} +
`:""} +
+ `}_renderStringInput(e,a){if(this._config)return s` + this._valueChangedHandler(e,a)} + > + + `}_renderSwitch(e,a,n){if(this._config)return s` + this._valueChangedHandler(e,a)} + > + + `}_updateConfig(e){this._config=e,k(this,"config-changed",{config:this._config})}render(){if(!this.hass||!this._config)return s``;const a=N(this.hass),t=r(this._config,cn)||[];return s` + ${this._configUpgradeable?s`
+ ${e("editor.upgrade_available")} + + {if(this._config){const e=n(this._config);T(e),this._updateConfig(e)}}} + > + + +
+
`:s``} +
+ ${this._renderOptionSetHeader("cameras")} + ${"cameras"===this._expandedMenus[hn]?s` +
+ ${t.map(((e,n)=>this._renderCamera(t,n,a)))} + ${this._renderCamera(t,t.length,a,!0)} +
+ `:""} + ${this._renderOptionSetHeader("view")} + ${"view"===this._expandedMenus[hn]?s` +
+ ${this._renderOptionSelector(B,this._viewModes)} + ${this._renderOptionSelector(A,this._cameraSelectViewModes)} + ${this._renderOptionSelector(P,this._darkModes)} + ${this._renderNumberInput(H)} + ${this._renderNumberInput(z)} + ${this._renderSwitch(L,this._defaults.view.update_force)} + ${this._renderSwitch(E,this._defaults.view.update_cycle_camera)} + ${this._renderViewScanMenu()} +
+ `:""} + ${this._renderOptionSetHeader("menu")} + ${"menu"===this._expandedMenus[hn]?s` +
+ ${this._renderOptionSelector(j,this._menuStyles)} + ${this._renderOptionSelector(q,this._menuPositions)} + ${this._renderOptionSelector(U,this._menuAlignments)} + ${this._renderNumberInput(Z,{min:S})} + ${this._renderMenuButton("frigate")} + ${this._renderMenuButton("cameras")} + ${this._renderMenuButton("substreams")} + ${this._renderMenuButton("live")} + ${this._renderMenuButton("clips")} + ${this._renderMenuButton("snapshots")} + ${this._renderMenuButton("recordings")} + ${this._renderMenuButton("image")} + ${this._renderMenuButton("download")} + ${this._renderMenuButton("camera_ui")} + ${this._renderMenuButton("fullscreen")} + ${this._renderMenuButton("expand")} + ${this._renderMenuButton("timeline")} + ${this._renderMenuButton("media_player")} + ${this._renderMenuButton("microphone",s`${this._renderOptionSelector(`${g}.microphone.type`,this._microphoneButtonTypes,{label:e("config.menu.buttons.type")})}`)} + ${this._renderMenuButton("play")} + ${this._renderMenuButton("mute")} + ${this._renderMenuButton("screenshot")} +
+ `:""} + ${this._renderOptionSetHeader("live")} + ${"live"===this._expandedMenus[hn]?s` +
+ ${this._renderSwitch(F,this._defaults.live.preload)} + ${this._renderSwitch(V,this._defaults.live.draggable)} + ${this._renderSwitch(R,this._defaults.live.zoomable)} + ${this._renderSwitch(D,this._defaults.live.lazy_load)} + ${this._renderOptionSelector(G,this._mediaActionNegativeConditions)} + ${this._renderOptionSelector(J,this._mediaActionPositiveConditions)} + ${this._renderOptionSelector(K,this._mediaActionNegativeConditions)} + ${this._renderOptionSelector(Q,this._mediaActionNegativeConditions)} + ${this._renderOptionSelector(W,this._mediaActionPositiveConditions)} + ${this._renderOptionSelector(X,this._transitionEffects)} + ${this._renderSwitch(Y,this._defaults.live.show_image_during_load)} + ${this._putInSubmenu("live.controls",!0,"config.live.controls.editor_label",{name:"mdi:gamepad"},s` + ${this._renderSwitch(ee,this._defaults.live.controls.builtin,{label:e("config.common.controls.builtin")})} + ${this._renderNextPreviousControls("live.controls.next_previous",ae,ne,{allowIcons:!0})} + ${this._renderThumbnailsControls("live.controls.thumbnails",te,ie,oe,se,re,this._defaults.live.controls.thumbnails,{configPathMedia:le,configPathMode:de})} + ${this._renderTitleControls("live.controls.title",ce,me)} + ${this._renderMiniTimeline("live.controls.timeline",ue,he,_e,be,ge,pe,this._defaults.live.controls.timeline.show_recordings)} + `)} + ${this._renderMediaLayout("live.layout","config.live.layout",ve,fe,$e)} + ${this._putInSubmenu("live.microphone",!0,"config.live.microphone.editor_label",{name:"mdi:microphone"},s` + ${this._renderNumberInput(ye)} + ${this._renderSwitch(we,this._defaults.live.microphone.always_connected)} + `)} +
+ `:""} + ${this._renderOptionSetHeader("media_gallery")} + ${"media_gallery"===this._expandedMenus[hn]?s`
+ ${this._renderThumbnailsControls("media_gallery.controls.thumbnails",Se,Me,xe,Ie,Ce,this._defaults.media_gallery.controls.thumbnails)} + ${this._renderFilterControls("media_gallery.controls.filter",Oe)} +
`:""} + ${this._renderOptionSetHeader("media_viewer")} + ${"media_viewer"===this._expandedMenus[hn]?s`
+ ${this._renderOptionSelector(ke,this._mediaActionPositiveConditions)} + ${this._renderOptionSelector(Ne,this._mediaActionNegativeConditions)} + ${this._renderOptionSelector(Te,this._mediaActionNegativeConditions)} + ${this._renderOptionSelector(Be,this._mediaActionPositiveConditions)} + ${this._renderSwitch(Ae,this._defaults.media_viewer.draggable)} + ${this._renderSwitch(Pe,this._defaults.media_viewer.zoomable)} + ${this._renderSwitch(He,this._defaults.media_viewer.lazy_load)} + ${this._renderOptionSelector(ze,this._transitionEffects)} + ${this._renderSwitch(Le,this._defaults.media_viewer.snapshot_click_plays_clip)} + ${this._putInSubmenu("media_viewer.controls",!0,"config.media_viewer.controls.editor_label",{name:"mdi:gamepad"},s` + ${this._renderSwitch(Ee,this._defaults.media_viewer.controls.builtin,{label:e("config.common.controls.builtin")})} + ${this._renderNextPreviousControls("media_viewer.controls.next_previous",je,qe,{allowThumbnails:!0})} + ${this._renderThumbnailsControls("media_viewer.controls.thumbnails",Ue,Ze,Fe,Ve,Re,this._defaults.media_viewer.controls.thumbnails,{configPathMode:De})} + ${this._renderTitleControls("media_viewer.controls.title",Ge,Je)} + ${this._renderMiniTimeline("media_viewer.controls.timeline",Ke,Qe,We,Xe,Ye,ea,this._defaults.media_viewer.controls.timeline.show_recordings)} + `)} + ${this._renderMediaLayout("media_viewer.layout","config.media_viewer.layout",aa,na,ta)} +
`:""} + ${this._renderOptionSetHeader("image")} + ${"image"===this._expandedMenus[hn]?s`
+ ${this._renderOptionSelector(ia,this._imageModes)} + ${this._renderStringInput(oa)} + ${this._renderNumberInput(sa)} + ${this._renderSwitch(ra,this._defaults.image.zoomable)} + ${this._renderMediaLayout("image.layout","config.image.layout",la,da,ca)} +
`:""} + ${this._renderOptionSetHeader("timeline")} + ${"timeline"===this._expandedMenus[hn]?s`
+ ${this._renderTimelineCoreControls(v,f,$,y,w,this._defaults.timeline.show_recordings)} + ${this._renderThumbnailsControls("timeline.controls.thumbnails",ma,ua,ha,_a,ba,this._defaults.timeline.controls.thumbnails,{configPathMode:ga})} +
`:""} + ${this._renderOptionSetHeader("dimensions")} + ${"dimensions"===this._expandedMenus[hn]?s`
+ ${this._renderOptionSelector(pa,this._aspectRatioModes)} + ${this._renderStringInput(va)} + ${this._renderStringInput(fa)} + ${this._renderStringInput($a)} +
`:""} + ${this._renderOptionSetHeader("performance","low"===r(this._config,ya)?"warning":void 0)} + ${"performance"===this._expandedMenus[hn]?s`
+ ${"low"===r(this._config,ya)?this._renderInfo(e("config.performance.warning")):s``} + ${this._renderOptionSelector(ya,this._performanceProfiles)} + ${this._putInSubmenu("performance.features",!0,"config.performance.features.editor_label",{name:"mdi:feature-search"},s` + ${this._renderSwitch(wa,this._defaults.performance.features.animated_progress_indicator)} + ${this._renderNumberInput(Sa,{max:Ma})} + `)} + ${this._putInSubmenu("performance.style",!0,"config.performance.style.editor_label",{name:"mdi:palette-swatch-variant"},s` + ${this._renderSwitch(xa,this._defaults.performance.style.border_radius)} + ${this._renderSwitch(Ia,this._defaults.performance.style.box_shadow)} + `)} +
`:""} + ${void 0!==this._config.overrides?s` ${this._renderOptionSetHeader("overrides")} + ${"overrides"===this._expandedMenus[hn]?s`
+ ${this._renderInfo(e("config.overrides.info"))} +
`:""}`:s``} +
+ `}_closeMenu(e){delete this._expandedMenus[e],this.requestUpdate()}_openMenu(e,a){this._expandedMenus[e]=a,this.requestUpdate()}_toggleMenu(e){if(e&&e.target){const a=e.target.domain,n=e.target.key;this._expandedMenus[a]===n?this._closeMenu(a):this._openMenu(a,n)}}_valueChangedHandler(e,a){if(!this._config||!this.hass)return;let t;if(a.detail&&void 0!==a.detail.value&&(t=a.detail.value,"string"==typeof t&&(t=t.trim())),r(this._config,e)===t)return;const i=n(this._config);""===t||void 0===t?Ca(i,e):Oa(i,e,t),this._updateConfig(i)}static get styles(){return ka('ha-icon-button.button {\n color: var(--secondary-color, white);\n background-color: rgba(0, 0, 0, 0.6);\n border-radius: 50%;\n padding: 0px;\n margin: 3px;\n --ha-icon-display: block;\n /* Buttons can always be clicked */\n pointer-events: auto;\n opacity: 0.9;\n}\n\n@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\nha-icon[data-domain=alert][data-state=on],\nha-icon[data-domain=automation][data-state=on],\nha-icon[data-domain=binary_sensor][data-state=on],\nha-icon[data-domain=calendar][data-state=on],\nha-icon[data-domain=camera][data-state=streaming],\nha-icon[data-domain=cover][data-state=open],\nha-icon[data-domain=fan][data-state=on],\nha-icon[data-domain=humidifier][data-state=on],\nha-icon[data-domain=light][data-state=on],\nha-icon[data-domain=input_boolean][data-state=on],\nha-icon[data-domain=lock][data-state=unlocked],\nha-icon[data-domain=media_player][data-state=on],\nha-icon[data-domain=media_player][data-state=paused],\nha-icon[data-domain=media_player][data-state=playing],\nha-icon[data-domain=script][data-state=on],\nha-icon[data-domain=sun][data-state=above_horizon],\nha-icon[data-domain=switch][data-state=on],\nha-icon[data-domain=timer][data-state=active],\nha-icon[data-domain=vacuum][data-state=cleaning],\nha-icon[data-domain=group][data-state=on],\nha-icon[data-domain=group][data-state=home],\nha-icon[data-domain=group][data-state=open],\nha-icon[data-domain=group][data-state=locked],\nha-icon[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=pending],\nha-icon[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=plant][data-state=problem],\nha-icon[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\nha-icon-button[data-domain=alert][data-state=on],\nha-icon-button[data-domain=automation][data-state=on],\nha-icon-button[data-domain=binary_sensor][data-state=on],\nha-icon-button[data-domain=calendar][data-state=on],\nha-icon-button[data-domain=camera][data-state=streaming],\nha-icon-button[data-domain=cover][data-state=open],\nha-icon-button[data-domain=fan][data-state=on],\nha-icon-button[data-domain=humidifier][data-state=on],\nha-icon-button[data-domain=light][data-state=on],\nha-icon-button[data-domain=input_boolean][data-state=on],\nha-icon-button[data-domain=lock][data-state=unlocked],\nha-icon-button[data-domain=media_player][data-state=on],\nha-icon-button[data-domain=media_player][data-state=paused],\nha-icon-button[data-domain=media_player][data-state=playing],\nha-icon-button[data-domain=script][data-state=on],\nha-icon-button[data-domain=sun][data-state=above_horizon],\nha-icon-button[data-domain=switch][data-state=on],\nha-icon-button[data-domain=timer][data-state=active],\nha-icon-button[data-domain=vacuum][data-state=cleaning],\nha-icon-button[data-domain=group][data-state=on],\nha-icon-button[data-domain=group][data-state=home],\nha-icon-button[data-domain=group][data-state=open],\nha-icon-button[data-domain=group][data-state=locked],\nha-icon-button[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon-button[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon-button[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon-button[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon-button[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=pending],\nha-icon-button[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=plant][data-state=problem],\nha-icon-button[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon-button[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\n.option {\n padding: 4px 4px;\n cursor: pointer;\n}\n\n.option.option-overrides .title {\n color: var(--warning-color);\n}\n\n.row {\n display: flex;\n margin-bottom: -14px;\n pointer-events: none;\n}\n\n.title {\n padding-left: 16px;\n margin-top: -6px;\n pointer-events: none;\n}\n\n.title.warning {\n color: var(--warning-color);\n}\n\n.secondary {\n padding-left: 40px;\n color: var(--secondary-text-color);\n pointer-events: none;\n}\n\n.values {\n background: var(--secondary-background-color);\n display: grid;\n}\n\n.values + .option,\n.submenu + .option {\n margin-top: 10px;\n}\n\ndiv.upgrade {\n width: auto;\n border: 1px dotted var(--primary-color);\n margin: 10px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\ndiv.upgrade span {\n padding: 10px;\n}\n\n.submenu-header {\n display: flex;\n padding: 10px;\n cursor: pointer;\n}\n\n.submenu.selected > .submenu-header {\n background-color: var(--primary-color);\n color: var(--primary-text-color);\n}\n\n.submenu-header * {\n flex-basis: auto;\n pointer-events: none;\n}\n\n.submenu-header .new-camera {\n font-style: italic;\n}\n\n.submenu:not(.selected) > .submenu-header .new-camera {\n color: var(--secondary-text-color, "black");\n}\n\n.submenu-header ha-icon,\n.submenu-header ha-svg-icon {\n padding-right: 15px;\n}\n\n.submenu.selected {\n border: 1px solid var(--primary-color);\n}\n\n.submenu {\n width: calc(100% - 20px);\n margin-left: auto;\n margin-right: auto;\n margin-bottom: 10px;\n}\n\n.submenu:first-child,\n:not(.submenu) + .submenu {\n margin-top: 10px;\n}\n\n.submenu .controls {\n display: inline-block;\n margin-left: auto;\n margin-right: 0px;\n margin-bottom: 5px;\n}\n\n.submenu .controls ha-icon-button.button {\n --mdc-icon-button-size: 32px;\n --mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);\n}\n\nspan.info {\n padding: 10px;\n}\n\nha-selector {\n padding: 10px;\n border: 1px solid var(--divider-color);\n}')}};Na([Ta({attribute:!1})],gn.prototype,"hass",void 0),Na([Ba()],gn.prototype,"_config",void 0),Na([Ba()],gn.prototype,"_defaults",void 0),Na([Ba()],gn.prototype,"_expandedMenus",void 0),gn=Na([Aa("frigate-card-editor")],gn);export{gn as FrigateCardEditor}; diff --git a/www/frigate-card/endpoint-aa68fc9e.js b/www/frigate-card/endpoint-aa68fc9e.js new file mode 100644 index 00000000..a09ee8ad --- /dev/null +++ b/www/frigate-card/endpoint-aa68fc9e.js @@ -0,0 +1 @@ +import{dI as r,d8 as t,cL as n,l as a}from"./card-555679fd.js";const e=async(e,s,i,l)=>{if(!i.sign)return i.endpoint;let c;try{c=await r(s,i.endpoint,l)}catch(r){return t(r),null}return c?c.replace(/^http/i,"ws"):(n(e,a("error.failed_sign")),null)};export{e as g}; diff --git a/www/frigate-card/engine-e412e9a0.js b/www/frigate-card/engine-e412e9a0.js new file mode 100644 index 00000000..e0a518f7 --- /dev/null +++ b/www/frigate-card/engine-e412e9a0.js @@ -0,0 +1 @@ +const e=1e4;export{e as C}; diff --git a/www/frigate-card/engine-frigate-2c5e3aa9.js b/www/frigate-card/engine-frigate-2c5e3aa9.js new file mode 100644 index 00000000..7770aac0 --- /dev/null +++ b/www/frigate-card/engine-frigate-2c5e3aa9.js @@ -0,0 +1 @@ +import{bU as e,bV as t,bW as r,bX as a,l as n,bY as i,bZ as s,b_ as o,b$ as c,p as u,c0 as g,c1 as l,c2 as d,bS as m,c3 as f,c4 as p,c5 as h,c6 as _,c7 as y,c8 as v,c9 as w,ca as b,cb as C,d as D,cc as T,cd as M,ce as S,cf as I,cg as x,ch as F}from"./card-555679fd.js";import{C as $}from"./engine-e412e9a0.js";import{u as N}from"./uniqWith-12b3ff8a.js";import{V as R,a as Y}from"./media-b0eb3f2a.js";import{g as E}from"./_commonjsHelpers-1789f0cf.js";import{GenericCameraManagerEngine as z}from"./engine-generic-395b8c68.js";const j=e.object({camera:e.string(),end_time:e.number().nullable(),false_positive:e.boolean().nullable(),has_clip:e.boolean(),has_snapshot:e.boolean(),id:e.string(),label:e.string(),sub_label:e.string().nullable(),start_time:e.number(),top_score:e.number().nullable(),zones:e.string().array(),retain_indefinitely:e.boolean().optional()}).array(),H=e.object({hour:e.preprocess((e=>Number(e)),e.number().min(0).max(23)),duration:e.number().min(0),events:e.number().min(0)}),Z=e.object({day:e.preprocess((e=>"string"==typeof e?t(e):e),e.date()),events:e.number(),hours:H.array()}).array(),U=e.object({start_time:e.number(),end_time:e.number(),id:e.string()}).array(),P=e.object({success:e.boolean(),message:e.string()}),Q=e.object({camera:e.string(),day:e.string(),label:e.string(),sub_label:e.string().nullable(),zones:e.string().array()}).array();const W=async(e,t)=>await r(e,j,{type:"frigate/events/get",...t},!0);function q(e){i(1,arguments);var t=o(e);return s(1e3*t)}var O={exports:{}},A={exports:{}},L={exports:{}};!function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){var n=function(e){if(!a[e]){var t=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:"America/New_York",year:"numeric",month:"numeric",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}).format(new Date("2014-06-25T04:00:00.123Z")),r="06/25/2014, 00:00:00"===t||"‎06‎/‎25‎/‎2014‎ ‎00‎:‎00‎:‎00"===t;a[e]=r?new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:e,year:"numeric",month:"numeric",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}):new Intl.DateTimeFormat("en-US",{hourCycle:"h23",timeZone:e,year:"numeric",month:"numeric",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}return a[e]}(t);return n.formatToParts?function(e,t){try{for(var a=e.formatToParts(t),n=[],i=0;i=0&&(n[s]=parseInt(a[i].value,10))}return n}catch(e){if(e instanceof RangeError)return[NaN];throw e}}(n,e):function(e,t){var r=e.format(t).replace(/\u200E/g,""),a=/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(r);return[a[3],a[1],a[2],a[4],a[5],a[6]]}(n,e)};var r={year:0,month:1,day:2,hour:3,minute:4,second:5};var a={};e.exports=t.default}(L,L.exports);var V,k,G={exports:{}};V=G,k=G.exports,Object.defineProperty(k,"__esModule",{value:!0}),k.default=function(e,t,r,a,n,i,s){var o=new Date(0);return o.setUTCFullYear(e,t,r),o.setUTCHours(a,n,i,s),o},V.exports=k.default,function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,r){var n,l,d;if(!e)return 0;if(n=o.timezoneZ.exec(e))return 0;if(n=o.timezoneHH.exec(e))return u(d=parseInt(n[1],10))?-d*i:NaN;if(n=o.timezoneHHMM.exec(e)){d=parseInt(n[1],10);var m=parseInt(n[2],10);return u(d,m)?(l=Math.abs(d)*i+m*s,d>0?-l:l):NaN}if(function(e){if(g[e])return!0;try{return new Intl.DateTimeFormat(void 0,{timeZone:e}),g[e]=!0,!0}catch(e){return!1}}(e)){t=new Date(t||Date.now());var f=r?t:function(e){return(0,a.default)(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds())}(t),p=c(f,e),h=r?p:function(e,t,r){var a=e.getTime(),n=a-t,i=c(new Date(n),r);if(t===i)return t;n-=i-t;var s=c(new Date(n),r);if(i===s)return i;return Math.max(i,s)}(t,p,e);return-h}return NaN};var r=n(L.exports),a=n(G.exports);function n(e){return e&&e.__esModule?e:{default:e}}var i=36e5,s=6e4,o={timezone:/([Z+-].*)$/,timezoneZ:/^(Z)$/,timezoneHH:/^([+-]\d{2})$/,timezoneHHMM:/^([+-]\d{2}):?(\d{2})$/};function c(e,t){var n=(0,r.default)(e,t),i=(0,a.default)(n[0],n[1]-1,n[2],n[3]%24,n[4],n[5],0).getTime(),s=e.getTime(),o=s%1e3;return i-(s-=o>=0?o:1e3+o)}function u(e,t){return-23<=e&&e<=23&&(null==t||0<=t&&t<=59)}var g={};e.exports=t.default}(A,A.exports);var B={exports:{}},X={exports:{}};!function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){if(null===e||!0===e||!1===e)return NaN;var t=Number(e);if(isNaN(t))return t;return t<0?Math.ceil(t):Math.floor(t)},e.exports=t.default}(X,X.exports);var J={exports:{}};!function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()));return t.setUTCFullYear(e.getFullYear()),e.getTime()-t.getTime()},e.exports=t.default}(J,J.exports);var K={exports:{}};!function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=/(Z|[+-]\d{2}(?::?\d{2})?| UTC| [a-zA-Z]+\/[a-zA-Z_]+(?:\/[a-zA-Z_]+)?)$/;t.default=r,e.exports=t.default}(K,K.exports),function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){if(arguments.length<1)throw new TypeError("1 argument required, but only "+arguments.length+" present");if(null===e)return new Date(NaN);var i=t||{},s=null==i.additionalDigits?u:(0,r.default)(i.additionalDigits);if(2!==s&&1!==s&&0!==s)throw new RangeError("additionalDigits must be 0, 1 or 2");if(e instanceof Date||"object"==typeof e&&"[object Date]"===Object.prototype.toString.call(e))return new Date(e.getTime());if("number"==typeof e||"[object Number]"===Object.prototype.toString.call(e))return new Date(e);if("string"!=typeof e&&"[object String]"!==Object.prototype.toString.call(e))return new Date(NaN);var d=function(e){var t,r={},a=g.dateTimePattern.exec(e);a?(r.date=a[1],t=a[3]):(a=g.datePattern.exec(e))?(r.date=a[1],t=a[2]):(r.date=null,t=e);if(t){var n=g.timeZone.exec(t);n?(r.time=t.replace(n[1],""),r.timeZone=n[1].trim()):r.time=t}return r}(e),m=function(e,t){var r,a=g.YYY[t],n=g.YYYYY[t];if(r=g.YYYY.exec(e)||n.exec(e)){var i=r[1];return{year:parseInt(i,10),restDateString:e.slice(i.length)}}if(r=g.YY.exec(e)||a.exec(e)){var s=r[1];return{year:100*parseInt(s,10),restDateString:e.slice(s.length)}}return{year:null}}(d.date,s),y=m.year,v=function(e,t){if(null===t)return null;var r,a,n,i;if(0===e.length)return(a=new Date(0)).setUTCFullYear(t),a;if(r=g.MM.exec(e))return a=new Date(0),p(t,n=parseInt(r[1],10)-1)?(a.setUTCFullYear(t,n),a):new Date(NaN);if(r=g.DDD.exec(e)){a=new Date(0);var s=parseInt(r[1],10);return function(e,t){if(t<1)return!1;var r=f(e);if(r&&t>366)return!1;if(!r&&t>365)return!1;return!0}(t,s)?(a.setUTCFullYear(t,0,s),a):new Date(NaN)}if(r=g.MMDD.exec(e)){a=new Date(0),n=parseInt(r[1],10)-1;var o=parseInt(r[2],10);return p(t,n,o)?(a.setUTCFullYear(t,n,o),a):new Date(NaN)}if(r=g.Www.exec(e))return h(t,i=parseInt(r[1],10)-1)?l(t,i):new Date(NaN);if(r=g.WwwD.exec(e)){i=parseInt(r[1],10)-1;var c=parseInt(r[2],10)-1;return h(t,i,c)?l(t,i,c):new Date(NaN)}return null}(m.restDateString,y);if(isNaN(v))return new Date(NaN);if(v){var w,b=v.getTime(),C=0;if(d.time&&(C=function(e){var t,r,a;if(t=g.HH.exec(e))return _(r=parseFloat(t[1].replace(",",".")))?r%24*o:NaN;if(t=g.HHMM.exec(e))return _(r=parseInt(t[1],10),a=parseFloat(t[2].replace(",",".")))?r%24*o+a*c:NaN;if(t=g.HHMMSS.exec(e)){r=parseInt(t[1],10),a=parseInt(t[2],10);var n=parseFloat(t[3].replace(",","."));return _(r,a,n)?r%24*o+a*c+1e3*n:NaN}return null}(d.time),isNaN(C)))return new Date(NaN);if(d.timeZone||i.timeZone){if(w=(0,n.default)(d.timeZone||i.timeZone,new Date(b+C)),isNaN(w))return new Date(NaN)}else w=(0,a.default)(new Date(b+C)),w=(0,a.default)(new Date(b+C+w));return new Date(b+C+w)}return new Date(NaN)};var r=s(X.exports),a=s(J.exports),n=s(A.exports),i=s(K.exports);function s(e){return e&&e.__esModule?e:{default:e}}var o=36e5,c=6e4,u=2,g={dateTimePattern:/^([0-9W+-]+)(T| )(.*)/,datePattern:/^([0-9W+-]+)(.*)/,plainTime:/:/,YY:/^(\d{2})$/,YYY:[/^([+-]\d{2})$/,/^([+-]\d{3})$/,/^([+-]\d{4})$/],YYYY:/^(\d{4})/,YYYYY:[/^([+-]\d{4})/,/^([+-]\d{5})/,/^([+-]\d{6})/],MM:/^-(\d{2})$/,DDD:/^-?(\d{3})$/,MMDD:/^-?(\d{2})-?(\d{2})$/,Www:/^-?W(\d{2})$/,WwwD:/^-?W(\d{2})-?(\d{1})$/,HH:/^(\d{2}([.,]\d*)?)$/,HHMM:/^(\d{2}):?(\d{2}([.,]\d*)?)$/,HHMMSS:/^(\d{2}):?(\d{2}):?(\d{2}([.,]\d*)?)$/,timeZone:i.default};function l(e,t,r){t=t||0,r=r||0;var a=new Date(0);a.setUTCFullYear(e,0,4);var n=7*t+r+1-(a.getUTCDay()||7);return a.setUTCDate(a.getUTCDate()+n),a}var d=[31,28,31,30,31,30,31,31,30,31,30,31],m=[31,29,31,30,31,30,31,31,30,31,30,31];function f(e){return e%400==0||e%4==0&&e%100!=0}function p(e,t,r){if(t<0||t>11)return!1;if(null!=r){if(r<1)return!1;var a=f(e);if(a&&r>m[t])return!1;if(!a&&r>d[t])return!1}return!0}function h(e,t,r){return!(t<0||t>52)&&(null==r||!(r<0||r>6))}function _(e,t,r){return(null==e||!(e<0||e>=25))&&((null==t||!(t<0||t>=60))&&(null==r||!(r<0||r>=60)))}e.exports=t.default}(B,B.exports),function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,n){var i=(0,a.default)(e,n),s=(0,r.default)(t,i,!0),o=new Date(i.getTime()-s),c=new Date(0);return c.setFullYear(o.getUTCFullYear(),o.getUTCMonth(),o.getUTCDate()),c.setHours(o.getUTCHours(),o.getUTCMinutes(),o.getUTCSeconds(),o.getUTCMilliseconds()),c};var r=n(A.exports),a=n(B.exports);function n(e){return e&&e.__esModule?e:{default:e}}e.exports=t.default}(O,O.exports);var ee=E(O.exports);class te extends R{constructor(e,t,r,a,n,i){super(e,t),this._event=r,this._contentID=a,this._thumbnail=n,this._subLabels=i??null}getStartTime(){return q(this._event.start_time)}getEndTime(){return this._event.end_time?q(this._event.end_time):null}inProgress(){return!this.getEndTime()}getVideoContentType(){return Y.HLS}getID(){return this._event.id}getContentID(){return this._contentID}getTitle(){return(e=>{const t=Intl.DateTimeFormat().resolvedOptions().timeZone,r=Math.round(e.end_time?e.end_time-e.start_time:Date.now()/1e3-e.start_time),a=null!==e.top_score?` ${Math.round(100*e.top_score)}%`:"";return`${c(ee(1e3*e.start_time,t))} [${r}s, ${u(e.label)}${a}]`})(this._event)}getThumbnail(){return this._thumbnail}isFavorite(){return this._event.retain_indefinitely??null}setFavorite(e){this._event.retain_indefinitely=e}getWhat(){return[this._event.label]}getWhere(){const e=this._event.zones;return e.length?e:null}getScore(){return this._event.top_score}getTags(){return this._subLabels}isGroupableWith(e){return this.getMediaType()===e.getMediaType()&&g(this.getWhere(),e.getWhere())&&g(this.getWhat(),e.getWhat())}}class re extends R{constructor(e,t,r,a,n,i){super(e,t),this._recording=r,this._id=a,this._contentID=n,this._title=i}getID(){return this._id}getStartTime(){return this._recording.startTime}getEndTime(){return this._recording.endTime}inProgress(){return!this.getEndTime()}getVideoContentType(){return Y.HLS}getContentID(){return this._contentID}getTitle(){return this._title}getEventCount(){return this._recording.events}}class ae{static createEventViewMedia(e,t,r,a,n){return"clip"===e&&!a.has_clip||"snapshot"===e&&!a.has_snapshot||!r.frigate.client_id||!r.frigate.camera_name?null:new te(e,t,a,((e,t,r,a)=>`media-source://frigate/${e}/event/${a}/${t}/${r.id}`)(r.frigate.client_id,r.frigate.camera_name,a,"clip"===e?"clips":"snapshots"),((e,t)=>`/api/frigate/${e}/thumbnail/${t.id}`)(r.frigate.client_id,a),n)}static createRecordingViewMedia(e,t,r,a){return r.frigate.client_id&&r.frigate.camera_name?new re("recording",e,t,((e,t)=>`${e.frigate?.client_id??""}/${e.frigate.camera_name??""}/${t.startTime.getTime()}/${t.endTime.getTime()}`)(r,t),((e,t,r)=>["media-source://frigate",e,"recordings",t,`${r.startTime.getFullYear()}-${String(r.startTime.getMonth()+1).padStart(2,"0")}-${String(String(r.startTime.getDate()).padStart(2,"0"))}`,String(r.startTime.getHours()).padStart(2,"0")].join("/"))(r.frigate.client_id,r.frigate.camera_name,t),((e,t)=>`${e} ${c(t.startTime)}`)(a,t)):null}}class ne{static isFrigateMedia(e){return this.isFrigateEvent(e)||this.isFrigateRecording(e)}static isFrigateEvent(e){return e instanceof te}static isFrigateRecording(e){return e instanceof re}}const ie="birdseye";class se{static isFrigateEventQueryResults(e){return e.engine===m.Frigate&&e.type===w.Event}static isFrigateRecordingQueryResults(e){return e.engine===m.Frigate&&e.type===w.Recording}static isFrigateRecordingSegmentsResults(e){return e.engine===m.Frigate&&e.type===w.RecordingSegments}}class oe extends z{constructor(e,t,r){super(),this._throttledSegmentGarbageCollector=d(this._garbageCollectSegments.bind(this),36e5,{leading:!1,trailing:!0}),this._cardWideConfig=e,this._recordingSegmentsCache=t,this._requestCache=r}getEngineType(){return m.Frigate}async initializeCamera(e,t,r){const a=!!r.frigate?.camera_name,i=r.triggers.motion||r.triggers.occupancy;let s=null;const o=f(r);if(o&&(!a||i))try{s=await t.getEntity(e,o)}catch(e){throw new p(n("error.no_camera_entity"),r)}if(s&&!a){const e=this._getFrigateCameraNameFromEntity(s);e&&(r.frigate.camera_name=e)}if(i){const a=await t.getMatchingEntities(e,(e=>e.config_entry_id===s?.config_entry_id&&!e.disabled_by&&e.entity_id.startsWith("binary_sensor.")));if(r.triggers.motion){const e=this._getMotionSensor(r,[...a.values()]);e&&r.triggers.entities.push(e)}if(r.triggers.occupancy){const e=this._getOccupancySensor(r,[...a.values()]);e&&r.triggers.entities.push(...e)}r.triggers.entities=(c=r.triggers.entities)&&c.length?l(c):[]}var c;return r}_getFrigateCameraNameFromEntity(e){if("frigate"===e.platform&&e.unique_id&&"string"==typeof e.unique_id){const t=e.unique_id.match(/:camera:(?[^:]+)$/);if(t&&t.groups)return t.groups.camera}return null}_getMotionSensor(e,t){return e.frigate.camera_name?t.find((t=>"string"==typeof t.unique_id&&!!t.unique_id?.match(new RegExp(`:motion_sensor:${e.frigate.camera_name}`))))?.entity_id??null:null}_getOccupancySensor(e,t){const r=[],a=(e,a)=>{const n=t.find((t=>"string"==typeof t.unique_id&&!!t.unique_id?.match(new RegExp(`:occupancy_sensor:${e}_${a}`))))?.entity_id??null;n&&r.push(n)};if(e.frigate.camera_name){const t=e.frigate.zones?.length?e.frigate.zones:[e.frigate.camera_name],n=e.frigate.labels?.length?e.frigate.labels:["all"];for(const e of t)for(const t of n)a(e,t);if(r.length)return r}return null}async getMediaDownloadPath(e,t,r){return ne.isFrigateEvent(r)?{endpoint:`/api/frigate/${t.frigate.client_id}/notifications/${r.getID()}/`+(h.isClip(r)?"clip.mp4":"snapshot.jpg")+"?download=true",sign:!0}:ne.isFrigateRecording(r)?{endpoint:`/api/frigate/${t.frigate.client_id}/recording/${t.frigate.camera_name}/start/${Math.floor(r.getStartTime().getTime()/1e3)}/end/${Math.floor(r.getEndTime().getTime()/1e3)}?download=true`,sign:!0}:null}generateDefaultEventQuery(e,t,r){const a=Array.from(t).map((t=>e.get(t))),n=N(a.map((e=>e?.frigate.zones)),g),i=N(a.map((e=>e?.frigate.labels)),g);if(1===n.length&&1===i.length)return[{type:_.Event,cameraIDs:t,...i[0]&&{what:new Set(i[0])},...n[0]&&{where:new Set(n[0])},...r}];const s=[];for(const a of t){const t=e.get(a);t&&s.push({type:_.Event,cameraIDs:new Set([a]),...t.frigate.labels&&{what:new Set(t.frigate.labels)},...t.frigate.zones&&{where:new Set(t.frigate.zones)},...r})}return s.length?s:null}generateDefaultRecordingQuery(e,t,r){return[{type:_.Recording,cameraIDs:t,...r}]}generateDefaultRecordingSegmentsQuery(e,t,r){return r.start&&r.end?[{type:_.RecordingSegments,cameraIDs:t,start:r.start,end:r.end,...r}]:null}async favoriteMedia(e,t,i,s){ne.isFrigateEvent(i)&&(await async function(e,t,i,s){const o={type:"frigate/event/retain",instance_id:t,event_id:i,retain:s},c=await r(e,P,o,!0);if(!c.success)throw new a(n("error.failed_retain"),{request:o,response:c})}(e,t.frigate.client_id,i.getID(),s),i.setFavorite(s))}_buildInstanceToCameraIDMapFromQuery(e,t){const r=new Map;for(const a of t){const t=this._getQueryableCameraConfig(e,a)?.frigate.client_id;t&&(r.has(t)||r.set(t,new Set),r.get(t)?.add(a))}return r}_getFrigateCameraNamesForCameraIDs(e,t){const r=new Set;for(const a of t){const t=this._getQueryableCameraConfig(e,a);t?.frigate.camera_name&&r.add(t.frigate.camera_name)}return r}async getEvents(e,t,r,a){const n=new Map,i=async(i,s)=>{if(!s||!s.size)return;const o={...r,cameraIDs:s},c=a?.useCache??1?this._requestCache.get(o):null;if(c)return void n.set(r,c);const u={instance_id:i,cameras:Array.from(this._getFrigateCameraNamesForCameraIDs(t,s)),...r.what&&{labels:Array.from(r.what)},...r.where&&{zones:Array.from(r.where)},...r.tags&&{sub_labels:Array.from(r.tags)},...r.end&&{before:Math.floor(r.end.getTime()/1e3)},...r.start&&{after:Math.floor(r.start.getTime()/1e3)},...r.limit&&{limit:r.limit},...r.hasClip&&{has_clip:r.hasClip},...r.hasSnapshot&&{has_snapshot:r.hasSnapshot},...r.favorite&&{favorites:r.favorite},limit:r?.limit??$},g={type:w.Event,engine:m.Frigate,instanceID:i,events:await W(e,u),expiry:b(new Date,{seconds:60}),cached:!1};(a?.useCache??1)&&this._requestCache.set(r,{...g,cached:!0},g.expiry),n.set(o,g)},s=this._buildInstanceToCameraIDMapFromQuery(t,r.cameraIDs);return await Promise.all(Array.from(s.keys()).map((e=>i(e,s.get(e))))),n.size?n:null}async getRecordings(e,t,a,n){const i=new Map,s=async(a,s)=>{const o={...a,cameraIDs:new Set([s])},c=n?.useCache??1?this._requestCache.get(o):null;if(c)return void i.set(o,c);const u=this._getQueryableCameraConfig(t,s);if(!u||!u.frigate.camera_name)return;const g=await(async(e,t,a)=>await r(e,Z,{type:"frigate/recordings/summary",instance_id:t,camera:a,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone},!0))(e,u.frigate.client_id,u.frigate.camera_name);let l=[];for(const e of g??[])for(const t of e.hours){const r=b(e.day,{hours:t.hour}),a=T(r),n=M(r);(!o.start||a>=o.start)&&(!o.end||n<=o.end)&&l.push({cameraID:s,startTime:a,endTime:n,events:t.events})}void 0!==o.limit&&(l=S(l,(e=>e.startTime),"desc").slice(0,o.limit));const d={type:w.Recording,engine:m.Frigate,instanceID:u.frigate.client_id,recordings:l,expiry:b(new Date,{seconds:60}),cached:!1};(n?.useCache??1)&&this._requestCache.set(o,{...d,cached:!0},d.expiry),i.set(o,d)};return await Promise.all(Array.from(a.cameraIDs).map((e=>s(a,e)))),i.size?i:null}async getRecordingSegments(e,t,a,n){const i=new Map,s=async(a,s)=>{const o={...a,cameraIDs:new Set([s])},c=this._getQueryableCameraConfig(t,s);if(!c||!c.frigate.camera_name)return;const u={start:o.start,end:o.end},g=n?.useCache??1?this._recordingSegmentsCache.get(s,u):null;if(g)return void i.set(o,{type:w.RecordingSegments,engine:m.Frigate,instanceID:c.frigate.client_id,segments:g,cached:!0});const l={instance_id:c.frigate.client_id,camera:c.frigate.camera_name,after:Math.floor(o.start.getTime()/1e3),before:Math.floor(o.end.getTime()/1e3)},d=await(async(e,t)=>await r(e,U,{type:"frigate/recordings/get",...t},!0))(e,l);(n?.useCache??1)&&this._recordingSegmentsCache.add(s,u,d),i.set(o,{type:w.RecordingSegments,engine:m.Frigate,instanceID:c.frigate.client_id,segments:d,cached:!1})};return await Promise.all(Array.from(a.cameraIDs).map((e=>s(a,e)))),y((()=>this._throttledSegmentGarbageCollector(e,t))),i.size?i:null}_getCameraIDMatch(e,t,r,a){if(1===t.cameraIDs.size)return[...t.cameraIDs][0];for(const[t,n]of e.entries())if(n.frigate.client_id===r&&n.frigate.camera_name===a)return t;return null}generateMediaFromEvents(e,t,r,a){if(!se.isFrigateEventQueryResults(a))return null;const n=[];for(const e of a.events){const i=this._getCameraIDMatch(t,r,a.instanceID,e.camera);if(!i)continue;const s=this._getQueryableCameraConfig(t,i);if(!s)continue;let o=null;if(r.hasClip||r.hasSnapshot||!e.has_clip&&!e.has_snapshot?r.hasSnapshot&&e.has_snapshot?o="snapshot":r.hasClip&&e.has_clip&&(o="clip"):o=e.has_clip?"clip":"snapshot",!o)continue;const c=ae.createEventViewMedia(o,i,s,e,e.sub_label?this._splitSubLabels(e.sub_label):void 0);c&&n.push(c)}return n}generateMediaFromRecordings(e,t,r,a){if(!se.isFrigateRecordingQueryResults(a))return null;const n=[];for(const r of a.recordings){const a=this._getQueryableCameraConfig(t,r.cameraID);if(!a)continue;const i=ae.createRecordingViewMedia(r.cameraID,r,a,this.getCameraMetadata(e,a).title);i&&n.push(i)}return n}getQueryResultMaxAge(e){return e.type===_.Event||e.type===_.Recording?60:null}async getMediaSeekTime(e,t,r,a,n){const i=r.getStartTime(),s=r.getEndTime();if(!i||!s||as)return null;const o=r.getCameraID(),c={cameraIDs:new Set([o]),start:i,end:s,type:_.RecordingSegments},u=await this.getRecordingSegments(e,t,c,n);return u?this._getSeekTimeInSegments(i,a,Array.from(u.values())[0].segments):null}_getQueryableCameraConfig(e,t){const r=e.get(t);return r&&r.frigate.camera_name!=ie?r:null}_splitSubLabels(e){return e.split(",").map((e=>e.trim()))}async getMediaMetadata(e,t,a,n){const i=new Map;if((n?.useCache??1)&&this._requestCache.has(a)){const e=this._requestCache.get(a);if(e)return i.set(a,e),i}const s=new Set,o=new Set,c=new Set,u=new Set,g=this._buildInstanceToCameraIDMapFromQuery(t,a.cameraIDs),l=async(a,n)=>{const i=this._getFrigateCameraNamesForCameraIDs(t,n);for(const t of await(async(e,t)=>await r(e,Q,{type:"frigate/events/summary",instance_id:t,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone},!0))(e,a))i.has(t.camera)&&(t.label&&s.add(t.label),t.zones.length&&t.zones.forEach(o.add,o),t.day&&c.add(t.day),t.sub_label&&this._splitSubLabels(t.sub_label).forEach(u.add,u))},d=async r=>{const a=await this.getRecordings(e,t,{type:_.Recording,cameraIDs:r},n);if(a)for(const e of a.values())if(se.isFrigateRecordingQueryResults(e))for(const t of e.recordings)c.add(F(t.startTime))};await v([...g.entries()],(([e,t])=>(async()=>{await Promise.all([l(e,t),d(t)])})()));const f={type:w.MediaMetadata,engine:m.Frigate,metadata:{...s.size&&{what:s},...o.size&&{where:o},...c.size&&{days:c},...u.size&&{tags:u}},expiry:b(new Date,{seconds:60}),cached:!1};return(n?.useCache??1)&&this._requestCache.set(a,{...f,cached:!0},f.expiry),i.set(a,f),i}async _garbageCollectSegments(e,t){const r=this._recordingSegmentsCache.getCameraIDs(),a={cameraIDs:new Set(r),type:_.Recording},n=()=>I(r.map((e=>this._recordingSegmentsCache.getSize(e)??0))),i=n(),s=(e,t)=>`${e}/${t.getDate()}/${t.getHours()}`,o=await this.getRecordings(e,t,a);if(o){for(const[e,t]of o){if(!se.isFrigateRecordingQueryResults(t))continue;const r=new Set;for(const e of t.recordings)r.add(s(e.cameraID,e.startTime));const a=Array.from(e.cameraIDs)[0];this._recordingSegmentsCache.expireMatches(a,(e=>{const t=s(a,q(e.start_time));return!r.has(t)}))}C(this._cardWideConfig,`Frigate Card recording segment garbage collection: Released ${i-n()} segment(s)`)}}_getSeekTimeInSegments(e,t,r){if(!r.length)return null;let a=0;for(const n of r){const r=q(n.start_time);if(r>t)break;const i=q(n.end_time),s=rt?t:i).getTime()-s.getTime()}return a/1e3}getCameraCapabilities(e){const t=e.frigate.camera_name===ie;return{canFavoriteEvents:!t,canFavoriteRecordings:!t,canSeek:!0,supportsClips:!t,supportsSnapshots:!t,supportsRecordings:!t,supportsTimeline:!t}}getMediaCapabilities(e){return{canFavorite:h.isEvent(e),canDownload:!0}}getCameraMetadata(e,t){const r=super.getCameraMetadata(e,t);return{title:t.title??D(e,t.camera_entity)??D(e,t.webrtc_card?.entity)??u(t.frigate?.camera_name)??t.id??"",icon:r.icon,engineLogo:"data:image/svg+xml,%3csvg width='512' height='512' viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.4999 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z' fill='white'/%3e%3c/svg%3e"}}getCameraEndpoints(e,t){const r=(()=>{if(!e.frigate.url)return null;if(!e.frigate.camera_name)return{endpoint:e.frigate.url};const r=`${e.frigate.url}/cameras/`+e.frigate.camera_name;if("live"===t?.view)return{endpoint:r};const a=`${e.frigate.url}/events?camera=`+e.frigate.camera_name,n=`${e.frigate.url}/recording/`+e.frigate.camera_name;switch(t?.media?.getMediaType()){case"clip":case"snapshot":return{endpoint:a};case"recording":const e=t.media.getStartTime();if(e)return{endpoint:n+x(e,"yyyy-MM-dd/HH")}}switch(t?.view){case"clip":case"clips":case"snapshots":case"snapshot":return{endpoint:a};case"recording":case"recordings":return{endpoint:n}}return{endpoint:r}})(),a={endpoint:`/api/frigate/${e.frigate.client_id}/mse/api/ws?src=${e.go2rtc?.stream??e.frigate.camera_name}`,sign:!0},n={endpoint:`/api/frigate/${e.frigate.client_id}/jsmpeg/${e.frigate.camera_name}`,sign:!0},i=(()=>{const t=e.frigate.camera_name?e.frigate.camera_name:null;return t?{endpoint:t}:null})();return{...r&&{ui:r},...a&&{go2rtc:a},...n&&{jsmpeg:n},...i&&{webrtcCard:i}}}}export{oe as FrigateCameraManagerEngine}; diff --git a/www/frigate-card/engine-generic-395b8c68.js b/www/frigate-card/engine-generic-395b8c68.js new file mode 100644 index 00000000..4062dbc5 --- /dev/null +++ b/www/frigate-card/engine-generic-395b8c68.js @@ -0,0 +1 @@ +import{bS as e,d as n,bT as t}from"./card-555679fd.js";class r{getEngineType(){return e.Generic}async initializeCamera(e,n,t){return t}generateDefaultEventQuery(e,n,t){return null}generateDefaultRecordingQuery(e,n,t){return null}generateDefaultRecordingSegmentsQuery(e,n,t){return null}async getEvents(e,n,t,r){return null}async getRecordings(e,n,t,r){return null}async getRecordingSegments(e,n,t,r){return null}generateMediaFromEvents(e,n,t,r){return null}generateMediaFromRecordings(e,n,t,r){return null}async getMediaDownloadPath(e,n,t){return null}async favoriteMedia(e,n,t,r){}getQueryResultMaxAge(e){return null}async getMediaSeekTime(e,n,t,r,a){return null}async getMediaMetadata(e,n,t,r){return null}getCameraMetadata(e,r){return{title:r.title??n(e,r.camera_entity)??n(e,r.webrtc_card?.entity)??r.id??"",icon:r?.icon??t(e,r.camera_entity)??"mdi:video"}}getCameraCapabilities(e){return{canFavoriteEvents:!1,canFavoriteRecordings:!1,canSeek:!1,supportsClips:!1,supportsRecordings:!1,supportsSnapshots:!1,supportsTimeline:!1}}getMediaCapabilities(e){return null}getCameraEndpoints(e,n){return null}}export{r as GenericCameraManagerEngine}; diff --git a/www/frigate-card/engine-motioneye-ae70fe08.js b/www/frigate-card/engine-motioneye-ae70fe08.js new file mode 100644 index 00000000..5b5c2e4a --- /dev/null +++ b/www/frigate-card/engine-motioneye-ae70fe08.js @@ -0,0 +1 @@ +import{cg as e,b$ as t,c0 as a,ci as i,cj as c,c4 as r,l as o,c6 as s,ck as n,cl as l,cm as d,cn as p,bS as f,co as u,cp as m,cq as y,c8 as h,c9 as g,ca as w,ce as _,ch as b}from"./card-555679fd.js";import{C as M}from"./engine-e412e9a0.js";import{p as x}from"./index-af8cf05c.js";import{GenericCameraManagerEngine as k}from"./engine-generic-395b8c68.js";import{V as G,a as v}from"./media-b0eb3f2a.js";class C extends G{constructor(t,a,i){super(t,a),this._browseMedia=i,i._metadata?.startDate?this._id=`${a}/${e(i._metadata.startDate,"yyyy-MM-dd HH:mm:ss")}`:this._id=i.media_content_id}getStartTime(){return this._browseMedia._metadata?.startDate??null}getEndTime(){return null}getVideoContentType(){return v.MP4}getID(){return this._id}getContentID(){return this._browseMedia.media_content_id}getTitle(){const e=this.getStartTime();return e?t(e):this._browseMedia.title}getThumbnail(){return this._browseMedia.thumbnail}getWhat(){return null}getScore(){return null}getTags(){return null}isGroupableWith(e){return this.getMediaType()===e.getMediaType()&&a(this.getWhere(),e.getWhere())&&a(this.getWhat(),e.getWhat())}}class D{static createEventViewMedia(e,t,a){return new C(e,a,t)}}const E=(e,t,a)=>{const i=t??a;return!i||!!e._metadata&&p({start:e._metadata.startDate,end:e._metadata.endDate},{start:t??i,end:a??i})};class S extends k{constructor(e,t,a){super(),this._cameraEntities=new Map,this._browseMediaManager=e,this._resolvedMediaCache=t,this._requestCache=a}async initializeCamera(e,t,a){const i=a.camera_entity?await t.getEntity(e,a.camera_entity):null;if(!i||!a.camera_entity)throw new r(o("error.no_camera_entity"),a);return this._cameraEntities.set(a.camera_entity,i),a}generateDefaultEventQuery(e,t,a){return[{type:s.Event,cameraIDs:t,...a}]}async getMediaDownloadPath(e,t,a){const i=a.getContentID();if(!i)return null;const c=await n(e,i,this._resolvedMediaCache);return c?{endpoint:l(e,c.url)}:null}getQueryResultMaxAge(e){return e.type===s.Event?d:null}getCameraCapabilities(e){const t=super.getCameraCapabilities(e);return t?{...t,supportsClips:!0,supportsSnapshots:!0,supportsTimeline:!0}:null}getMediaCapabilities(e){return{canFavorite:!1,canDownload:!0}}}class T{static isMotionEyeEventQueryResults(e){return e.engine===f.MotionEye&&e.type===g.Event}}const F={"%Y":"yyyy","%m":"MM","%d":"dd","%H":"HH","%M":"mm","%S":"ss"},B=new RegExp(/(%Y|%m|%d|%H|%M|%S)/g);class z extends S{getEngineType(){return f.MotionEye}_convertMotionEyeTimeFormatToDateFNS(e){return e.replace(B,((e,t)=>F[t]))}_motionEyeMetadataGeneratorFile(e,t,a,i){let c=i?._metadata?.startDate??new Date;if(t){const e=a.title.replace(/\.[^/.]+$/,"");if(c=x(e,t,c),!u(c))return null}return{cameraID:e,startDate:c,endDate:c}}_motionEyeMetadataGeneratorDirectory(e,t,a,i){let c=i?._metadata?.startDate??new Date;if(t){const e=x(a.title,t,c);if(!u(e))return null;c=m(e)}return{cameraID:e,startDate:c,endDate:i?._metadata?.endDate??y(c)}}async _getMatchingDirectories(e,t,a,i,c){const r=t.get(a)?.camera_entity,o=r?this._cameraEntities.get(r):null,s=o?.config_entry_id,n=o?.device_id,l=t.get(a);if(!s||!n||!l)return null;const d=(e,t)=>{const c=e.shift();if(!c)return[];const r=c.includes("%")?this._convertMotionEyeTimeFormatToDateFNS(c):null;return[{targets:t,metadataGenerator:(e,t)=>this._motionEyeMetadataGeneratorDirectory(a,r,e,t),matcher:e=>e.can_expand&&(!!r||e.title===c)&&E(e,i?.start,i?.end),advance:t=>d(e,t)}]};return await this._browseMediaManager.walkBrowseMedias(e,[...!1===i?.hasClip||i?.hasSnapshot?[]:d(l.motioneye.movies.directory_pattern.split("/"),[`media-source://motioneye/${s}#${n}#movies`]),...!1===i?.hasSnapshot||i?.hasClip?[]:d(l.motioneye.images.directory_pattern.split("/"),[`media-source://motioneye/${s}#${n}#images`])],{useCache:c?.useCache})}async getEvents(e,t,a,r){if(a.favorite||a.tags?.size||a.what?.size||a.where?.size)return null;const o=new Map,s=async s=>{const n={...a,cameraIDs:new Set([s])},l=r?.useCache??1?this._requestCache.get(n):null;if(l)return void o.set(n,l);const d=t.get(s);if(!d)return;const p=await this._getMatchingDirectories(e,t,s,n,r);if(!p||!p.length)return;const u=this._convertMotionEyeTimeFormatToDateFNS(d.motioneye.movies.file_pattern),m=this._convertMotionEyeTimeFormatToDateFNS(d.motioneye.images.file_pattern),y=await this._browseMediaManager.walkBrowseMedias(e,[{targets:p,metadataGenerator:(e,t)=>e.media_class===c||e.media_class===i?this._motionEyeMetadataGeneratorFile(s,e.media_class===c?m:u,e,t):null,matcher:e=>!e.can_expand&&E(e,n.start,n.end)}],{useCache:r?.useCache}),h=_(y,(e=>e._metadata?.startDate),"desc").slice(0,n.limit??M),w={type:g.Event,engine:f.MotionEye,browseMedia:h};(r?.useCache??1)&&this._requestCache.set(n,{...w,cached:!0},w.expiry),o.set(n,w)};return await h(a.cameraIDs,(e=>s(e))),o.size?o:null}generateMediaFromEvents(e,t,a,r){return T.isMotionEyeEventQueryResults(r)?(e=>{const t=new Map;for(const a of e){const e=a._metadata?.cameraID;if(!e)continue;const r=a.media_class===i?"clip":a.media_class===c?"snapshot":null;if(!r)continue;const o=D.createEventViewMedia(r,a,e);if(o){const e=o.getID(),a=t.get(e);(!a||"snapshot"===a.getMediaType()&&"clip"===o.getMediaType())&&t.set(e,o)}}return[...t.values()]})(r.browseMedia):null}async getMediaMetadata(e,t,a,i){const c=new Map;if((i?.useCache??1)&&this._requestCache.has(a)){const e=this._requestCache.get(a);if(e)return c.set(a,e),c}const r=new Set,o=async a=>{const c=await this._getMatchingDirectories(e,t,a,null,i);for(const e of c??[])e._metadata&&r.add(b(e._metadata?.startDate))};await h(a.cameraIDs,(e=>o(e)));const s={type:g.MediaMetadata,engine:f.MotionEye,metadata:{...r.size&&{days:r}},expiry:w(new Date,{seconds:d}),cached:!1};return(i?.useCache??1)&&this._requestCache.set(a,{...s,cached:!0},s.expiry),c.set(a,s),c}getCameraMetadata(e,t){return{...super.getCameraMetadata(e,t),engineLogo:"data:image/svg+xml,%3c%3fxml version='1.0' encoding='UTF-8' standalone='no'%3f%3e%3c!-- Created with Inkscape (http://www.inkscape.org/) --%3e%3csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' id='svg2' version='1.1' inkscape:version='0.91 r13725' width='64' height='64' xml:space='preserve' sodipodi:docname='motioneye-icon.svg' inkscape:export-filename='/home/ccrisan/projects/motioneye/static/img/motioneye-logo.png' inkscape:export-xdpi='960' inkscape:export-ydpi='960'%3e%3cmetadata id='metadata8'%3e%3crdf:RDF%3e%3ccc:Work rdf:about=''%3e%3cdc:format%3eimage/svg%2bxml%3c/dc:format%3e%3cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3e%3cdc:title /%3e%3c/cc:Work%3e%3c/rdf:RDF%3e%3c/metadata%3e%3cdefs id='defs6'%3e%3clinearGradient id='linearGradient4351' inkscape:collect='always'%3e%3cstop id='stop4353' offset='0' style='stop-color:%23737373%3bstop-opacity:1' /%3e%3cstop id='stop4355' offset='1' style='stop-color:%23585858%3bstop-opacity:1' /%3e%3c/linearGradient%3e%3clinearGradient inkscape:collect='always' id='linearGradient4205'%3e%3cstop style='stop-color:%234aa3e0%3bstop-opacity:1' offset='0' id='stop4207' /%3e%3cstop style='stop-color:%233096db%3bstop-opacity:1' offset='1' id='stop4209' /%3e%3c/linearGradient%3e%3clinearGradient inkscape:collect='always' id='linearGradient4197'%3e%3cstop style='stop-color:%23787878%3bstop-opacity:1' offset='0' id='stop4199' /%3e%3cstop style='stop-color:%23585858%3bstop-opacity:1' offset='1' id='stop4201' /%3e%3c/linearGradient%3e%3clinearGradient inkscape:collect='always' xlink:href='%23linearGradient4351' id='linearGradient4203' x1='26.445793' y1='47.517574' x2='26.445793' y2='3.8183768' gradientUnits='userSpaceOnUse' /%3e%3clinearGradient inkscape:collect='always' xlink:href='%23linearGradient4205' id='linearGradient4211' x1='26.602072' y1='43.034946' x2='26.602072' y2='29.466328' gradientUnits='userSpaceOnUse' gradientTransform='matrix(0.96428571%2c0%2c0%2c0.96428571%2c0.91428571%2c0.91428571)' /%3e%3cfilter style='color-interpolation-filters:sRGB%3b' inkscape:label='Drop Shadow' id='filter4285'%3e%3cfeFlood flood-opacity='0.588235' flood-color='rgb(0%2c0%2c0)' result='flood' id='feFlood4287' /%3e%3cfeComposite in='flood' in2='SourceGraphic' operator='in' result='composite1' id='feComposite4289' /%3e%3cfeGaussianBlur in='composite1' stdDeviation='0.6' result='blur' id='feGaussianBlur4291' /%3e%3cfeOffset dx='0' dy='-1' result='offset' id='feOffset4293' /%3e%3cfeComposite in='SourceGraphic' in2='offset' operator='over' result='composite2' id='feComposite4295' /%3e%3c/filter%3e%3clinearGradient inkscape:collect='always' xlink:href='%23linearGradient4197' id='linearGradient4309' gradientUnits='userSpaceOnUse' x1='26.445793' y1='47.517574' x2='26.445793' y2='3.8183768' /%3e%3clinearGradient inkscape:collect='always' xlink:href='%23linearGradient4197' id='linearGradient4311' gradientUnits='userSpaceOnUse' x1='26.445793' y1='47.517574' x2='26.445793' y2='3.8183768' /%3e%3clinearGradient inkscape:collect='always' xlink:href='%23linearGradient4197' id='linearGradient4313' gradientUnits='userSpaceOnUse' x1='26.445793' y1='47.517574' x2='26.445793' y2='3.8183768' /%3e%3cfilter style='color-interpolation-filters:sRGB%3b' inkscape:label='Drop Shadow' id='filter4315' x='-0.10000000000000001' y='-0.16000000000000003'%3e%3cfeFlood flood-opacity='0.588235' flood-color='rgb(0%2c0%2c0)' result='flood' id='feFlood4317' /%3e%3cfeComposite in='flood' in2='SourceGraphic' operator='in' result='composite1' id='feComposite4319' /%3e%3cfeGaussianBlur in='composite1' stdDeviation='0.6' result='blur' id='feGaussianBlur4321' /%3e%3cfeOffset dx='0' dy='-1' result='offset' id='feOffset4323' /%3e%3cfeComposite in='SourceGraphic' in2='offset' operator='over' result='composite2' id='feComposite4325' /%3e%3c/filter%3e%3cfilter style='color-interpolation-filters:sRGB%3b' inkscape:label='Drop Shadow' id='filter4327'%3e%3cfeFlood flood-opacity='0.588235' flood-color='rgb(0%2c0%2c0)' result='flood' id='feFlood4329' /%3e%3cfeComposite in='flood' in2='SourceGraphic' operator='in' result='composite1' id='feComposite4331' /%3e%3cfeGaussianBlur in='composite1' stdDeviation='0.6' result='blur' id='feGaussianBlur4333' /%3e%3cfeOffset dx='0' dy='-1' result='offset' id='feOffset4335' /%3e%3cfeComposite in='SourceGraphic' in2='offset' operator='over' result='composite2' id='feComposite4337' /%3e%3c/filter%3e%3cfilter style='color-interpolation-filters:sRGB%3b' inkscape:label='Drop Shadow' id='filter4339'%3e%3cfeFlood flood-opacity='0.588235' flood-color='rgb(0%2c0%2c0)' result='flood' id='feFlood4341' /%3e%3cfeComposite in='flood' in2='SourceGraphic' operator='in' result='composite1' id='feComposite4343' /%3e%3cfeGaussianBlur in='composite1' stdDeviation='0.2' result='blur' id='feGaussianBlur4345' /%3e%3cfeOffset dx='0' dy='-0.5' result='offset' id='feOffset4347' /%3e%3cfeComposite in='SourceGraphic' in2='offset' operator='over' result='composite2' id='feComposite4349' /%3e%3c/filter%3e%3c/defs%3e%3csodipodi:namedview pagecolor='white' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1025' id='namedview4' showgrid='false' inkscape:zoom='2' inkscape:cx='-94.597631' inkscape:cy='10.226517' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='g10' showguides='true' inkscape:guide-bbox='true' /%3e%3cg id='g10' inkscape:groupmode='layer' inkscape:label='ink_ext_XXXXXX' transform='matrix(1.25%2c0%2c0%2c-1.25%2c0%2c64)'%3e%3cg id='g4170' style='fill:url(%23linearGradient4203)%3bfill-opacity:1%3bfilter:url(%23filter4327)' transform='matrix(0.96428571%2c0%2c0%2c0.96428571%2c0.91428571%2c0.91428571)'%3e%3cpath id='path4244' d='M 8.9346154%2c40.515385 C 5.3647588%2c36.547307 3.2%2c31.357779 3.2%2c25.6 3.2%2c13.228821 13.228821%2c3.2 25.6%2c3.2 37.971179%2c3.2 48%2c13.228821 48%2c25.6 c 0%2c5.736682 -2.161128%2c10.952493 -5.707692%2c14.915385 -1.695935%2c-0.623286 -3.387833%2c-1.349065 -5.061539%2c-2.288462 3.2394%2c-0.937363 5.6%2c-3.937988 5.6%2c-7.457692 0%2c-4.260339 -3.469626%2c-7.753846 -7.753846%2c-7.753846 -3.633936%2c0 -6.690552%2c2.51055 -7.538461%2c5.869231 l -3.876924%2c0 c -0.840685%2c-3.360193 -3.903443%2c-5.869231 -7.538461%2c-5.869231 -4.284219%2c0 -7.7807693%2c3.493507 -7.7807693%2c7.753846 0%2c3.56112 2.4570323%2c6.5856 5.7615383%2c7.484616 -1.676267%2c0.912203 -3.404813%2c1.620556 -5.1692306%2c2.261538 z M 25.6%2c26.461538 c 0.532632%2c-1.981435 1.101793%2c-3.947553 3.446154%2c-5.16923 L 25.6%2c16.123077 22.153846%2c21.292308 c 2.053593%2c1.454966 3.000771%2c3.237758 3.446154%2c5.16923 z' style='fill:url(%23linearGradient4309)%3bfill-opacity:1%3bstroke:none' inkscape:connector-curvature='0' /%3e%3cpath id='path4242' d='m 16.123077%2c33.353847 c -1.427443%2c0 -2.584616%2c-1.157173 -2.584616%2c-2.584616 0%2c-1.427444 1.157173%2c-2.584615 2.584616%2c-2.584615 1.427444%2c0 2.584615%2c1.157171 2.584615%2c2.584615 0%2c1.427443 -1.157171%2c2.584616 -2.584615%2c2.584616 z' style='fill:url(%23linearGradient4311)%3bfill-opacity:1%3bstroke:none' inkscape:connector-curvature='0' /%3e%3cpath id='path4240' d='m 35.076923%2c33.353847 c -1.427443%2c0 -2.584615%2c-1.157173 -2.584615%2c-2.584616 0%2c-1.427444 1.157172%2c-2.584615 2.584615%2c-2.584615 1.427443%2c0 2.584616%2c1.157171 2.584616%2c2.584615 0%2c1.427443 -1.157173%2c2.584616 -2.584616%2c2.584616 z' style='fill:url(%23linearGradient4313)%3bfill-opacity:1%3bstroke:none' inkscape:connector-curvature='0' /%3e%3c/g%3e%3cpath inkscape:connector-curvature='0' style='fill:%23737373%3bfill-opacity:1%3bstroke:none%3bfilter:url(%23filter4339)' d='m 25.6%2c47.2 c -4.373944%2c0 -8.437159%2c-1.399808 -11.838461%2c-3.634616 3.677605%2c-0.394237 7.305921%2c-1.342945 11.423077%2c-3.375 4.166157%2c2.122533 8.434154%2c3.008875 12.279808%2c3.452886 C 34.057131%2c45.890032 29.986674%2c47.2 25.6%2c47.2 Z' id='path4248' /%3e%3cpath inkscape:connector-curvature='0' style='fill:url(%23linearGradient4211)%3bfill-opacity:1%3bstroke:none%3bfilter:url(%23filter4315)' d='M 39.723077%2c42.552884 C 35.394064%2c42.5242 29.479588%2c40.397223 25.184616%2c38.432418 20.668821%2c40.064102 16.035448%2c42.649343 10.801923%2c42.526924 10.453022%2c42.51873 10.118061%2c42.50105 9.7634616%2c42.475 L 5.6615384%2c42.1375 9.5557693%2c40.839424 c 5.3417977%2c-1.74056 10.0398397%2c-2.851302 14.1749997%2c-10.025963 0.959101%2c0 2.845924%2c-4.15e-4 3.738462%2c-4.15e-4 4.11884%2c7.134039 9.059296%2c8.324614 14.149039%2c10.026378 L 45.460577%2c42.085577 41.4625%2c42.475 c -0.544847%2c0.05181 -1.120992%2c0.08198 -1.739423%2c0.07788 z' id='path4246' sodipodi:nodetypes='cccccccccccc' /%3e%3c/g%3e%3c/svg%3e"}}getCameraEndpoints(e,t){const a=e.motioneye?.url?{endpoint:e.motioneye.url}:null;return{...a&&{ui:a}}}}export{z as MotionEyeCameraManagerEngine}; diff --git a/www/frigate-card/frigate-hass-card.js b/www/frigate-card/frigate-hass-card.js new file mode 100644 index 00000000..764fd995 --- /dev/null +++ b/www/frigate-card/frigate-hass-card.js @@ -0,0 +1 @@ +import"./card-555679fd.js"; diff --git a/www/frigate-card/gallery-6281c347.js b/www/frigate-card/gallery-6281c347.js new file mode 100644 index 00000000..72f28814 --- /dev/null +++ b/www/frigate-card/gallery-6281c347.js @@ -0,0 +1,165 @@ +import{d3 as e,bk as t,bl as r,cO as i,cP as o,c0 as n,d1 as a,y as s,cS as l,bj as c,s as d,cq as h,cp as g,bY as u,bZ as m,bn as p,l as f,ch as v,d4 as b,c6 as y,d5 as w,d6 as $,d7 as x,d8 as k,p as C,ce as _,cg as T,d9 as S,da as E,db as O,c2 as z,dc as M,bQ as L,dd as D,u as A,de as I,df as R,o as F,cU as H,bm as N}from"./card-555679fd.js";import{u as j}from"./uniqWith-12b3ff8a.js";import{s as W}from"./index-52dee8bb.js";import{p as B}from"./index-af8cf05c.js"; +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +function q(t){return class extends t{createRenderRoot(){const t=this.constructor,{registry:r,elementDefinitions:i,shadowRootOptions:o}=t;i&&!r&&(t.registry=new CustomElementRegistry,Object.entries(i).forEach((([e,r])=>t.registry.define(e,r))));const n=this.renderOptions.creationScope=this.attachShadow({...o,customElements:t.registry});return e(n,this.constructor.elementStyles),n}}}const P=!0,V=!0,K=!0,U=!1,Y=!1;let G,Q,X,Z=!1,J=!1,ee=!1,te=!1,re=null,ie=!1;const oe="http://www.w3.org/1999/xlink",ne={},ae=e=>"object"===(e=typeof e)||"function"===e;const se=(e,t,...r)=>{let i=null,o=null,n=null,a=!1,s=!1;const l=[],c=t=>{for(let r=0;re[t])).join(" "))}}if("function"==typeof e)return e(null===t?{}:t,l,de);const d=le(e,null);return d.$attrs$=t,l.length>0&&(d.$children$=l),d.$key$=o,d.$name$=n,d},le=(e,t)=>{const r={$flags$:0,$tag$:e,$text$:t,$elm$:null,$children$:null,$attrs$:null,$key$:null,$name$:null};return r},ce={},de={forEach:(e,t)=>e.map(he).forEach(t),map:(e,t)=>e.map(he).map(t).map(ge)},he=e=>({vattrs:e.$attrs$,vchildren:e.$children$,vkey:e.$key$,vname:e.$name$,vtag:e.$tag$,vtext:e.$text$}),ge=e=>{if("function"==typeof e.vtag){const t=Object.assign({},e.vattrs);return e.vkey&&(t.key=e.vkey),e.vname&&(t.name=e.vname),se(e.vtag,t,...e.vchildren||[])}const t=le(e.vtag,e.vtext);return t.$attrs$=e.vattrs,t.$children$=e.vchildren,t.$key$=e.vkey,t.$name$=e.vname,t},ue=(e,t,r)=>{const i=e;return{emit:e=>me(i,t,{bubbles:!!(4&r),composed:!!(2&r),cancelable:!!(1&r),detail:e})}},me=(e,t,r)=>{const i=lt.ce(t,r);return e.dispatchEvent(i),i},pe=new WeakMap,fe=e=>{const t=e.$cmpMeta$,r=e.$hostElement$,i=t.$flags$,o=(t.$tagName$,()=>{}),n=((e,t,r,i)=>{var o;let n=ve(t,r);const a=it.get(n);if(e=11===e.nodeType?e:at,a)if("string"==typeof a){e=e.head||e;let t,r=pe.get(e);if(r||pe.set(e,r=new Set),!r.has(n)){{t=at.createElement("style"),t.innerHTML=a;const r=null!==(o=lt.$nonce$)&&void 0!==o?o:function(e){var t,r,i;return null!==(i=null===(r=null===(t=e.head)||void 0===t?void 0:t.querySelector('meta[name="csp-nonce"]'))||void 0===r?void 0:r.getAttribute("content"))&&void 0!==i?i:void 0}(at);null!=r&&t.setAttribute("nonce",r),e.insertBefore(t,e.querySelector("link"))}r&&r.add(n)}}else e.adoptedStyleSheets.includes(a)||(e.adoptedStyleSheets=[...e.adoptedStyleSheets,a]);return n})(r.shadowRoot?r.shadowRoot:r.getRootNode(),t,e.$modeName$);10&i&&(r["s-sc"]=n,r.classList.add(n+"-h"),2&i&&r.classList.add(n+"-s")),o()},ve=(e,t)=>"sc-"+(t&&32&e.$flags$?e.$tagName$+"-"+t:e.$tagName$),be=(e,t,r,i,o,n)=>{if(r!==i){let a=tt(e,t),s=t.toLowerCase();if("class"===t){const t=e.classList,o=we(r),n=we(i);t.remove(...o.filter((e=>e&&!n.includes(e)))),t.add(...n.filter((e=>e&&!o.includes(e))))}else if("style"===t){for(const t in r)i&&null!=i[t]||(t.includes("-")?e.style.removeProperty(t):e.style[t]="");for(const t in i)r&&i[t]===r[t]||(t.includes("-")?e.style.setProperty(t,i[t]):e.style[t]=i[t])}else if("key"===t);else if("ref"===t)i&&i(e);else if(e.__lookupSetter__(t)||"o"!==t[0]||"n"!==t[1]){const l=ae(i);if((a||l&&null!==i)&&!o)try{if(e.tagName.includes("-"))e[t]=i;else{const o=null==i?"":i;"list"===t?a=!1:null!=r&&e[t]==o||(e[t]=o)}}catch(e){}let c=!1;s!==(s=s.replace(/^xlink\:?/,""))&&(t=s,c=!0),null==i||!1===i?!1===i&&""!==e.getAttribute(t)||(c?e.removeAttributeNS(oe,t):e.removeAttribute(t)):(!a||4&n||o)&&!l&&(i=!0===i?"":i,c?e.setAttributeNS(oe,t,i):e.setAttribute(t,i))}else t="-"===t[2]?t.slice(3):tt(nt,s)?s.slice(2):s[2]+t.slice(3),r&<.rel(e,t,r,!1),i&<.ael(e,t,i,!1)}},ye=/\s/,we=e=>e?e.split(ye):[],$e=(e,t,r,i)=>{const o=11===t.$elm$.nodeType&&t.$elm$.host?t.$elm$.host:t.$elm$,n=e&&e.$attrs$||ne,a=t.$attrs$||ne;for(i in n)i in a||be(o,i,n[i],void 0,r,t.$flags$);for(i in a)be(o,i,n[i],a[i],r,t.$flags$)},xe=(e,t,r,i)=>{const o=t.$children$[r];let n,a,s,l=0;if(Z||(ee=!0,"slot"===o.$tag$&&(G&&i.classList.add(G+"-s"),o.$flags$|=o.$children$?2:1)),null!==o.$text$)n=o.$elm$=at.createTextNode(o.$text$);else if(1&o.$flags$)n=o.$elm$=at.createTextNode("");else{if(te||(te="svg"===o.$tag$),n=o.$elm$=at.createElementNS(te?"http://www.w3.org/2000/svg":"http://www.w3.org/1999/xhtml",2&o.$flags$?"slot-fb":o.$tag$),te&&"foreignObject"===o.$tag$&&(te=!1),$e(null,o,te),null!=G&&n["s-si"]!==G&&n.classList.add(n["s-si"]=G),o.$children$)for(l=0;l{lt.$flags$|=1;const r=e.childNodes;for(let e=r.length-1;e>=0;e--){const i=r[e];i["s-hn"]!==X&&i["s-ol"]&&(Ee(i).insertBefore(i,Se(i)),i["s-ol"].remove(),i["s-ol"]=void 0,ee=!0),t&&ke(i,t)}lt.$flags$&=-2},Ce=(e,t,r,i,o,n)=>{let a,s=e["s-cr"]&&e["s-cr"].parentNode||e;for(s.shadowRoot&&s.tagName===X&&(s=s.shadowRoot);o<=n;++o)i[o]&&(a=xe(null,r,o,e),a&&(i[o].$elm$=a,s.insertBefore(a,Se(t))))},_e=(e,t,r,i,o)=>{for(;t<=r;++t)(i=e[t])&&(o=i.$elm$,Ae(i),J=!0,o["s-ol"]?o["s-ol"].remove():ke(o,!0),o.remove())},Te=(e,t)=>e.$tag$===t.$tag$&&("slot"===e.$tag$?e.$name$===t.$name$:e.$key$===t.$key$),Se=e=>e&&e["s-ol"]||e,Ee=e=>(e["s-ol"]?e["s-ol"]:e).parentNode,Oe=(e,t)=>{const r=t.$elm$=e.$elm$,i=e.$children$,o=t.$children$,n=t.$tag$,a=t.$text$;let s;null===a?(te="svg"===n||"foreignObject"!==n&&te,"slot"===n||$e(e,t,te),null!==i&&null!==o?((e,t,r,i)=>{let o,n,a=0,s=0,l=0,c=0,d=t.length-1,h=t[0],g=t[d],u=i.length-1,m=i[0],p=i[u];for(;a<=d&&s<=u;)if(null==h)h=t[++a];else if(null==g)g=t[--d];else if(null==m)m=i[++s];else if(null==p)p=i[--u];else if(Te(h,m))Oe(h,m),h=t[++a],m=i[++s];else if(Te(g,p))Oe(g,p),g=t[--d],p=i[--u];else if(Te(h,p))"slot"!==h.$tag$&&"slot"!==p.$tag$||ke(h.$elm$.parentNode,!1),Oe(h,p),e.insertBefore(h.$elm$,g.$elm$.nextSibling),h=t[++a],p=i[--u];else if(Te(g,m))"slot"!==h.$tag$&&"slot"!==p.$tag$||ke(g.$elm$.parentNode,!1),Oe(g,m),e.insertBefore(g.$elm$,h.$elm$),g=t[--d],m=i[++s];else{for(l=-1,c=a;c<=d;++c)if(t[c]&&null!==t[c].$key$&&t[c].$key$===m.$key$){l=c;break}l>=0?(n=t[l],n.$tag$!==m.$tag$?o=xe(t&&t[s],r,l,e):(Oe(n,m),t[l]=void 0,o=n.$elm$),m=i[++s]):(o=xe(t&&t[s],r,s,e),m=i[++s]),o&&Ee(h.$elm$).insertBefore(o,Se(h.$elm$))}a>d?Ce(e,null==i[u+1]?null:i[u+1].$elm$,r,i,s,u):s>u&&_e(t,a,d)})(r,i,t,o):null!==o?(null!==e.$text$&&(r.textContent=""),Ce(r,null,t,o,0,o.length-1)):null!==i&&_e(i,0,i.length-1),te&&"svg"===n&&(te=!1)):(s=r["s-cr"])?s.parentNode.textContent=a:e.$text$!==a&&(r.data=a)},ze=e=>{const t=e.childNodes;let r,i,o,n,a,s;for(i=0,o=t.length;i{let t,r,i,o,n,a,s=0;const l=e.childNodes,c=l.length;for(;s=0;a--)r=i[a],r["s-cn"]||r["s-nr"]||r["s-hn"]===t["s-hn"]||(De(r,o)?(n=Me.find((e=>e.$nodeToRelocate$===r)),J=!0,r["s-sn"]=r["s-sn"]||o,n?n.$slotRefNode$=t:Me.push({$slotRefNode$:t,$nodeToRelocate$:r}),r["s-sr"]&&Me.map((e=>{De(e.$nodeToRelocate$,r["s-sn"])&&(n=Me.find((e=>e.$nodeToRelocate$===r)),n&&!e.$slotRefNode$&&(e.$slotRefNode$=n.$slotRefNode$))}))):Me.some((e=>e.$nodeToRelocate$===r))||Me.push({$nodeToRelocate$:r}));1===t.nodeType&&Le(t)}},De=(e,t)=>1===e.nodeType?null===e.getAttribute("slot")&&""===t||e.getAttribute("slot")===t:e["s-sn"]===t||""===t,Ae=e=>{e.$attrs$&&e.$attrs$.ref&&e.$attrs$.ref(null),e.$children$&&e.$children$.map(Ae)},Ie=(e,t)=>{const r=e.$hostElement$,i=e.$cmpMeta$,o=e.$vnode$||le(null,null),n=(a=t)&&a.$tag$===ce?t:se(null,null,t);var a;if(X=r.tagName,i.$attrsToReflect$&&(n.$attrs$=n.$attrs$||{},i.$attrsToReflect$.map((([e,t])=>n.$attrs$[t]=r[e]))),n.$tag$=null,n.$flags$|=4,e.$vnode$=n,n.$elm$=o.$elm$=r.shadowRoot||r,G=r["s-sc"],Q=r["s-cr"],Z=0!=(1&i.$flags$),J=!1,Oe(o,n),lt.$flags$|=1,ee){let e,t,r,i,o,a;Le(n.$elm$);let s=0;for(;s{e.$flags$|=16,e.$ancestorComponent$;return vt((()=>Fe(e,t)))},Fe=(e,t)=>{const r=e.$hostElement$,i=(e.$cmpMeta$.$tagName$,()=>{}),o=r;let n;return n=We(o,t?"componentWillLoad":"componentWillUpdate"),n=Be(n,(()=>We(o,"componentWillRender"))),i(),Be(n,(()=>He(e,o,t)))},He=async(e,t,r)=>{const i=e.$hostElement$,o=(e.$cmpMeta$.$tagName$,()=>{});i["s-rc"],r&&fe(e);const n=(e.$cmpMeta$.$tagName$,()=>{});Ne(e,t,i),n(),o(),je(e)},Ne=(e,t,r)=>{try{re=t,t=t.render&&t.render(),e.$flags$&=-17,e.$flags$|=2,(P||V)&&(K||V)&&(U||Ie(e,t))}catch(t){rt(t,e.$hostElement$)}return re=null,null},je=e=>{e.$cmpMeta$.$tagName$;const t=()=>{},r=e.$hostElement$;e.$ancestorComponent$,We(r,"componentDidRender"),64&e.$flags$?(We(r,"componentDidUpdate"),t()):(e.$flags$|=64,We(r,"componentDidLoad"),t())},We=(e,t,r)=>{if(e&&e[t])try{return e[t](r)}catch(e){rt(e)}},Be=(e,t)=>e&&e.then?e.then(t):t(),qe=(e,t,r,i)=>{const o=Je(e),n=e,a=o.$instanceValues$.get(t),s=o.$flags$,l=n;var c,d;c=r,d=i.$members$[t][0],r=null==c||ae(c)?c:4&d?"false"!==c&&(""===c||!!c):2&d?parseFloat(c):1&d?String(c):c;const h=Number.isNaN(a)&&Number.isNaN(r);if(r!==a&&!h){if(o.$instanceValues$.set(t,r),i.$watchers$&&128&s){const e=i.$watchers$[t];e&&e.map((e=>{try{l[e](r,a,t)}catch(e){rt(e,n)}}))}if(2==(18&s)){if(l.componentShouldUpdate&&!1===l.componentShouldUpdate(r,a,t))return;Re(o,!1)}}},Pe=(e,t,r)=>{if(t.$members$){e.watchers&&(t.$watchers$=e.watchers);const r=Object.entries(t.$members$),i=e.prototype;r.map((([e,[r]])=>{(31&r||32&r)&&Object.defineProperty(i,e,{get(){return t=e,Je(this).$instanceValues$.get(t);var t},set(r){qe(this,e,r,t)},configurable:!0,enumerable:!0})}));{const o=new Map;i.attributeChangedCallback=function(e,t,r){lt.jmp((()=>{const t=o.get(e);if(this.hasOwnProperty(t))r=this[t],delete this[t];else if(i.hasOwnProperty(t)&&"number"==typeof this[t]&&this[t]==r)return;this[t]=(null!==r||"boolean"!=typeof this[t])&&r}))},e.observedAttributes=r.filter((([e,t])=>15&t[0])).map((([e,r])=>{const i=r[1]||e;return o.set(i,e),512&r[0]&&t.$attrsToReflect$.push([e,i]),i}))}}return e},Ve=async(e,t,r,i,o)=>{if(0==(32&t.$flags$)&&(o=e.constructor,t.$flags$|=32,customElements.whenDefined(r.$tagName$).then((()=>t.$flags$|=128)),o.style)){let i=o.style;"string"!=typeof i&&(i=i[t.$modeName$=(e=>ot.map((t=>t(e))).find((e=>!!e)))(e)]);const n=ve(r,t.$modeName$);if(!it.has(n)){const e=(r.$tagName$,()=>{});((e,t,r)=>{let i=it.get(e);dt&&r?(i=i||new CSSStyleSheet,"string"==typeof i?i=t:i.replaceSync(t)):i=t,it.set(e,i)})(n,i,!!(1&r.$flags$)),e()}}t.$ancestorComponent$;Re(t,!0)},Ke=e=>{const t=e["s-cr"]=at.createComment("");t["s-cn"]=!0,e.insertBefore(t,e.firstChild)},Ue=(e,t)=>{const r={$flags$:t[0],$tagName$:t[1]};r.$members$=t[2],r.$listeners$=t[3],r.$watchers$=e.$watchers$,r.$attrsToReflect$=[];const i=e.prototype.connectedCallback,o=e.prototype.disconnectedCallback;return Object.assign(e.prototype,{__registerHost(){et(this,r)},connectedCallback(){(e=>{if(0==(1<.$flags$)){const t=Je(e),r=t.$cmpMeta$,i=(r.$tagName$,()=>{});1&t.$flags$?(Ye(e,t,r.$listeners$),t.$lazyInstance$):(t.$flags$|=1,12&r.$flags$&&Ke(e),r.$members$&&Object.entries(r.$members$).map((([t,[r]])=>{if(31&r&&e.hasOwnProperty(t)){const r=e[t];delete e[t],e[t]=r}})),Ve(e,t,r)),i()}})(this),i&&i.call(this)},disconnectedCallback(){(e=>{if(0==(1<.$flags$)){const t=Je(e);t.$rmListeners$&&(t.$rmListeners$.map((e=>e())),t.$rmListeners$=void 0)}})(this),o&&o.call(this)},__attachShadow(){this.attachShadow({mode:"open",delegatesFocus:!!(16&r.$flags$)})}}),e.is=r.$tagName$,Pe(e,r)},Ye=(e,t,r,i)=>{r&&r.map((([r,i,o])=>{const n=Qe(e,r),a=Ge(t,o),s=Xe(r);lt.ael(n,i,a,s),(t.$rmListeners$=t.$rmListeners$||[]).push((()=>lt.rel(n,i,a,s)))}))},Ge=(e,t)=>r=>{try{Y||e.$hostElement$[t](r)}catch(e){rt(e)}},Qe=(e,t)=>4&t?at:8&t?nt:16&t?at.body:e,Xe=e=>ct?{passive:0!=(1&e),capture:0!=(2&e)}:0!=(2&e),Ze=new WeakMap,Je=e=>Ze.get(e),et=(e,t)=>{const r={$flags$:0,$hostElement$:e,$cmpMeta$:t,$instanceValues$:new Map};return Ye(e,r,t.$listeners$),Ze.set(e,r)},tt=(e,t)=>t in e,rt=(e,t)=>(0,console.error)(e,t),it=new Map,ot=[],nt="undefined"!=typeof window?window:{},at=nt.document||{head:{}},st=nt.HTMLElement||class{},lt={$flags$:0,$resourcesUrl$:"",jmp:e=>e(),raf:e=>requestAnimationFrame(e),ael:(e,t,r,i)=>e.addEventListener(t,r,i),rel:(e,t,r,i)=>e.removeEventListener(t,r,i),ce:(e,t)=>new CustomEvent(e,t)},ct=(()=>{let e=!1;try{at.addEventListener("e",null,Object.defineProperty({},"passive",{get(){e=!0}}))}catch(e){}return e})(),dt=(()=>{try{return new CSSStyleSheet,"function"==typeof(new CSSStyleSheet).replaceSync}catch(e){}return!1})(),ht=[],gt=[],ut=(e,t)=>r=>{e.push(r),ie||(ie=!0,t&&4<.$flags$?ft(pt):lt.raf(pt))},mt=e=>{for(let t=0;t{mt(ht),mt(gt),(ie=ht.length>0)&<.raf(pt)},ft=e=>{return Promise.resolve(t).then(e);var t},vt=ut(gt,!0),bt=(e,t)=>{const r=!!e.label||e.hasLabelSlot,i=!!e.helpText||e.hasHelpTextSlot,o=!!e.invalidText||e.hasInvalidTextSlot,n=!e.invalid,a=!!e.invalid;return se("div",{class:{"form-control":!0,[`form-control-${e.size}`]:!0,"form-control-has-label":r,"form-control-has-help-text":i,"form-control-has-invalid-text":o}},se("label",{id:e.labelId,class:"form-control-label",htmlFor:e.inputId,"aria-hidden":r?"false":"true",onClick:e.onLabelClick},se("slot",{name:"label"},e.label),e.requiredIndicator&&se("div",{class:"asterisk"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 1200 1200"},se("path",{fill:"currentColor",d:"M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z"})))),se("div",{class:"form-control-input"},t),n&&se("div",{id:e.helpTextId,class:"form-control-help-text","aria-hidden":i?"false":"true"},se("slot",{name:"help-text"},e.helpText)),a&&se("div",{id:e.invalidTextId,class:"form-control-invalid-text","aria-hidden":o?"false":"true"},se("div",{class:"icon"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Alert Circle"),se("path",{d:"M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,319.91a20,20,0,1,1,20-20A20,20,0,0,1,256,367.91Zm21.72-201.15-5.74,122a16,16,0,0,1-32,0l-5.74-121.94v-.05a21.74,21.74,0,1,1,43.44,0Z",fill:"currentColor"}))),se("div",{class:"text"},se("slot",{name:"invalid-text"},e.invalidText))))}; +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +function yt(e){const t=e?e.assignedNodes({flatten:!0}):[];let r="";return[...t].map((e=>{e.nodeType===Node.TEXT_NODE&&(r+=e.textContent)})),r}function wt(e,t){return t?null!==e.querySelector(`[slot="${t}"]`):[...e.childNodes].some((e=>{if(e.nodeType===e.TEXT_NODE&&""!==e.textContent.trim())return!0;if(e.nodeType===e.ELEMENT_NODE){if(!e.hasAttribute("slot"))return!0}return!1}))} +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */const $t=(e,t=[])=>{const r={};return t.forEach((t=>{if(e.hasAttribute(t)){null!==e.getAttribute(t)&&(r[t]=e.getAttribute(t)),e.removeAttribute(t)}})),r},xt=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow()}render(){return se("span",{class:"spinner","aria-busy":"true","aria-live":"polite"})}static get style(){return":host{--track-color:var(--gr-color-light-shade);--indicator-color:var(--gr-color-primary);--stroke-width:2px;display:inline-flex;box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}.spinner{display:inline-block;width:1em;height:1em;border-radius:50%;border:solid var(--stroke-width) var(--track-color);border-top-color:var(--indicator-color);border-right-color:var(--indicator-color);animation:1s linear infinite spin}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}"}},[1,"gr-spinner"]);function kt(){if("undefined"==typeof customElements)return;["gr-spinner"].forEach((e=>{if("gr-spinner"===e)customElements.get(e)||customElements.define(e,xt)}))}kt(); +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +const Ct=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.grFocus=ue(this,"gr-focus",7),this.grBlur=ue(this,"gr-blur",7),this.inheritedAttributes={},this.handleClick=e=>{if("button"!==this.type){const t=this.el.closest("form");if(t){e.preventDefault();const r=document.createElement("button");r.type=this.type,r.style.display="none",t.appendChild(r),r.click(),r.remove()}}},this.onFocus=()=>{this.grFocus.emit()},this.onBlur=()=>{this.grBlur.emit()},this.variant="default",this.disabled=!1,this.loading=!1,this.size="medium",this.caret=!1,this.pill=!1,this.expand=void 0,this.circle=!1,this.href=void 0,this.target=void 0,this.rel=void 0,this.type="button"}componentWillLoad(){this.inheritedAttributes=$t(this.el,["aria-label","tabindex","title"])}async setFocus(e){this.button.focus(e)}async removeFocus(){this.button.blur()}render(){const{rel:e,target:t,href:r,variant:i,size:o,expand:n,type:a,inheritedAttributes:s,disabled:l}=this,c=void 0===r?"button":"a",d="button"===c?{type:a}:{href:r,rel:e,target:t};return se(ce,{onClick:this.handleClick,"aria-disabled":l?"true":null,class:{[`button-${i}`]:!0,[`button-${o}`]:!0,[`button-${n}`]:void 0!==n,"button-caret":this.caret,"button-circle":this.circle,"button-pill":this.pill,"button-disabled":l,"button-loading":this.loading}},se(c,Object.assign({ref:e=>this.button=e},d,{class:"button-native",disabled:l,onFocus:this.onFocus,onBlur:this.onBlur},s),se("span",{class:"button-inner"},se("slot",{name:"icon-only"}),se("slot",{name:"start"}),se("slot",null),se("slot",{name:"end"}),this.caret&&se("span",{class:"caret"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Chevron Down"),se("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"48",d:"M112 184l144 144 144-144"})))),this.loading&&se("gr-spinner",null)))}get el(){return this}static get style(){return".gr-scroll-lock{overflow:hidden !important}:host{display:inline-block;width:auto;font-family:var(--gr-font-family);font-weight:var(--gr-font-weight-medium);font-size:var(--gr-form-element-font-size-medium);font-kerning:none;user-select:none;vertical-align:top;vertical-align:-webkit-baseline-middle;pointer-events:auto;--height:var(--gr-form-element-height-medium);--border-radius:var(--gr-form-element-border-radius-medium);--border-width:1px;--border-style:solid;--background:transparent;--background-hover:transparent;--background-focus:transparent;--color:var(--gr-color-dark-tint);--color-hover:var(--gr-color-dark-tint);--color-focus:var(--gr-color-dark-tint);--border-color:var(--gr-color-light-shade);--border-color-hover:var(--gr-color-medium);--border-color-focus:var(--gr-color-primary);--padding-top:0;--padding-start:var(--gr-spacing-medium);--padding-end:var(--gr-spacing-medium);--padding-bottom:0;--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-primary-rgb), 0.33);--shadow:none;--transition:background-color 150ms linear, opacity 150ms linear, border 150ms linear, color 150ms linear}:host(.button-disabled){pointer-events:none;opacity:0.5}:host(.button-primary){--border-color:var(--gr-color-primary);--background:var(--gr-color-primary);--color:var(--gr-color-primary-contrast);--border-color-hover:var(--gr-color-primary-shade);--background-hover:var(--gr-color-primary-shade);--color-hover:var(--gr-color-primary-contrast);--border-color-focus:var(--gr-color-primary);--background-focus:var(--gr-color-primary-shade);--color-focus:var(--gr-color-primary-contrast);--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-primary-rgb), 0.33)}:host(.button-secondary){--border-color:var(--gr-color-light-shade);--background:transparent;--color:var(--gr-color-primary);--border-color-hover:var(--gr-color-primary);--background-hover:transparent;--color-hover:var(--gr-color-primary);--border-color-focus:var(--gr-color-primary);--background-focus:transparent;--color-focus:var(--gr-color-primary);--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-primary-rgb), 0.33)}:host(.button-danger){--border-color:var(--gr-color-danger);--background:transparent;--color:var(--gr-color-danger);--border-color-hover:var(--gr-color-danger);--background-hover:var(--gr-color-danger);--color-hover:var(--gr-color-danger-contrast);--border-color-focus:var(--gr-color-danger);--background-focus:var(--gr-color-danger);--color-focus:var(--gr-color-danger-contrast);--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-danger-rgb), 0.33)}:host(.button-plain){--border-color:transparent;--background:transparent;--color:var(--gr-color-primary);--border-color-hover:transparent;--background-hover:transparent;--color-hover:var(--gr-color-primary-shade);--border-color-focus:transparent;--background-focus:transparent;--color-focus:var(--gr-color-primary-shade);--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-primary-rgb), 0.33)}:host(.button-small){--padding-start:var(--gr-spacing-small);--padding-end:var(--gr-spacing-small);--border-radius:var(--gr-form-element-border-radius-small);--height:var(--gr-form-element-height-small);font-size:var(--gr-form-element-font-size-small)}:host(.button-large){--padding-start:var(--gr-spacing-large);--padding-end:var(--gr-spacing-large);--border-radius:var(--gr-form-element-border-radius-large);--height:var(--gr-form-element-height-large);font-size:var(--gr-form-element-font-size-large)}:host(.button-block){display:block}:host(.button-block) .button-native{margin-left:0;margin-right:0;display:block;width:100%;clear:both;contain:content}:host(.button-block) .button-native::after{clear:both}:host(.button-full){display:block}:host(.button-full) .button-native{margin-left:0;margin-right:0;display:block;width:100%;contain:content;border-radius:0;border-right-width:0;border-left-width:0}.button-native{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;letter-spacing:inherit;text-decoration:inherit;text-indent:inherit;text-overflow:inherit;text-transform:inherit;text-align:inherit;white-space:inherit;color:inherit;display:block;position:relative;padding-top:var(--padding-top);padding-left:var(--padding-start);padding-right:var(--padding-end);padding-bottom:var(--padding-bottom);width:100%;height:var(--height);transition:var(--transition);border-radius:var(--border-radius);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);color:var(--color);box-shadow:var(--shadow);line-height:1;cursor:pointer;z-index:0;text-decoration:none;box-sizing:border-box}.button-native::-moz-focus-inner{border:0}.button-native:focus{outline:none;box-shadow:var(--focus-ring);border-color:var(--border-color-focus);background-color:var(--background-focus);color:var(--color-focus)}.button-native *,.button-native *:before,.button-native *:after{box-sizing:inherit}.button-inner{display:flex;position:relative;flex-flow:row nowrap;flex-shrink:0;align-items:center;justify-content:center;width:100%;height:100%;z-index:1}:host(.button-circle) .button-native{padding-top:0;padding-bottom:0;padding-left:0;padding-right:0;border-radius:50%;width:var(--height)}@media (any-hover: hover){.button-native:hover{color:var(--color-hover);background:var(--background-hover);border-color:var(--border-color-hover)}}:host(.button-caret) .caret{display:flex;align-items:center;margin-left:0.3em}:host(.button-caret) .caret svg{width:1em;height:1em}:host(.button-pill) .button-native{border-radius:var(--height)}::slotted(*){pointer-events:none}::slotted([slot=start]){margin-top:0;margin-left:-0.3em;margin-right:0.3em;margin-bottom:0}::slotted([slot=end]){margin-top:0;margin-left:0.3em;margin-right:-0.2em;margin-bottom:0}::slotted([slot=icon-only]){font-size:1.4em;pointer-events:none}:host(.button-loading){position:relative;pointer-events:none}:host(.button-loading) .caret{visibility:hidden}:host(.button-loading) slot[name=start],:host(.button-loading) slot[name=end],:host(.button-loading) slot[name=icon-only],:host(.button-loading) slot:not([name]){visibility:hidden}:host(.button-loading) gr-spinner{--indicator-color:currentColor;position:absolute;height:1em;width:1em;top:calc(50% - 0.5em);left:calc(50% - 0.5em)}@media not all and (min-resolution: 0.001dpcm){@supports (-webkit-appearance: none) and (stroke-color: transparent){:host([type=button]),:host([type=reset]),:host([type=submit]){-webkit-appearance:none !important}}}"}},[1,"gr-button",{variant:[513],disabled:[516],loading:[516],size:[513],caret:[4],pill:[516],expand:[513],circle:[516],href:[1],target:[1],rel:[1],type:[1],setFocus:[64],removeFocus:[64]}]);function _t(){if("undefined"==typeof customElements)return;["gr-button","gr-spinner"].forEach((e=>{switch(e){case"gr-button":customElements.get(e)||customElements.define(e,Ct);break;case"gr-spinner":customElements.get(e)||kt()}}))}function Tt(e,t,r="vertical",i="smooth"){const o= +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +function(e,t){return{top:Math.round(e.getBoundingClientRect().top-t.getBoundingClientRect().top),left:Math.round(e.getBoundingClientRect().left-t.getBoundingClientRect().left)}}(e,t),n=o.top+t.scrollTop,a=o.left+t.scrollLeft,s=t.scrollLeft,l=t.scrollLeft+t.offsetWidth,c=t.scrollTop,d=t.scrollTop+t.offsetHeight;"horizontal"!==r&&"both"!==r||(al&&t.scrollTo({left:a-t.offsetWidth+e.clientWidth,behavior:i})),"vertical"!==r&&"both"!==r||(nd&&t.scrollTo({top:n-t.offsetHeight+e.clientHeight,behavior:i}))} +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */function St(e){return e.tabIndex>-1}function Et(e){if(St(e))return e;if(e.shadowRoot){const t=[...e.shadowRoot.children].find(St);if(t)return t}return e.children?[...e.children].map(Et)[0]:null}_t();var Ot="top",zt="bottom",Mt="right",Lt="left",Dt="auto",At=[Ot,zt,Mt,Lt],It="start",Rt="end",Ft="clippingParents",Ht="viewport",Nt="popper",jt="reference",Wt=At.reduce((function(e,t){return e.concat([t+"-"+It,t+"-"+Rt])}),[]),Bt=[].concat(At,[Dt]).reduce((function(e,t){return e.concat([t,t+"-"+It,t+"-"+Rt])}),[]),qt=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function Pt(e){return e?(e.nodeName||"").toLowerCase():null}function Vt(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function Kt(e){return e instanceof Vt(e).Element||e instanceof Element}function Ut(e){return e instanceof Vt(e).HTMLElement||e instanceof HTMLElement}function Yt(e){return"undefined"!=typeof ShadowRoot&&(e instanceof Vt(e).ShadowRoot||e instanceof ShadowRoot)}const Gt={name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var t=e.state;Object.keys(t.elements).forEach((function(e){var r=t.styles[e]||{},i=t.attributes[e]||{},o=t.elements[e];Ut(o)&&Pt(o)&&(Object.assign(o.style,r),Object.keys(i).forEach((function(e){var t=i[e];!1===t?o.removeAttribute(e):o.setAttribute(e,!0===t?"":t)})))}))},effect:function(e){var t=e.state,r={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,r.popper),t.styles=r,t.elements.arrow&&Object.assign(t.elements.arrow.style,r.arrow),function(){Object.keys(t.elements).forEach((function(e){var i=t.elements[e],o=t.attributes[e]||{},n=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:r[e]).reduce((function(e,t){return e[t]="",e}),{});Ut(i)&&Pt(i)&&(Object.assign(i.style,n),Object.keys(o).forEach((function(e){i.removeAttribute(e)})))}))}},requires:["computeStyles"]};function Qt(e){return e.split("-")[0]}var Xt=Math.max,Zt=Math.min,Jt=Math.round;function er(e,t){void 0===t&&(t=!1);var r=e.getBoundingClientRect(),i=1,o=1;if(Ut(e)&&t){var n=e.offsetHeight,a=e.offsetWidth;a>0&&(i=Jt(r.width)/a||1),n>0&&(o=Jt(r.height)/n||1)}return{width:r.width/i,height:r.height/o,top:r.top/o,right:r.right/i,bottom:r.bottom/o,left:r.left/i,x:r.left/i,y:r.top/o}}function tr(e){var t=er(e),r=e.offsetWidth,i=e.offsetHeight;return Math.abs(t.width-r)<=1&&(r=t.width),Math.abs(t.height-i)<=1&&(i=t.height),{x:e.offsetLeft,y:e.offsetTop,width:r,height:i}}function rr(e,t){var r=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(r&&Yt(r)){var i=t;do{if(i&&e.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function ir(e){return Vt(e).getComputedStyle(e)}function or(e){return["table","td","th"].indexOf(Pt(e))>=0}function nr(e){return((Kt(e)?e.ownerDocument:e.document)||window.document).documentElement}function ar(e){return"html"===Pt(e)?e:e.assignedSlot||e.parentNode||(Yt(e)?e.host:null)||nr(e)}function sr(e){return Ut(e)&&"fixed"!==ir(e).position?e.offsetParent:null}function lr(e){for(var t=Vt(e),r=sr(e);r&&or(r)&&"static"===ir(r).position;)r=sr(r);return r&&("html"===Pt(r)||"body"===Pt(r)&&"static"===ir(r).position)?t:r||function(e){var t=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&Ut(e)&&"fixed"===ir(e).position)return null;var r=ar(e);for(Yt(r)&&(r=r.host);Ut(r)&&["html","body"].indexOf(Pt(r))<0;){var i=ir(r);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return r;r=r.parentNode}return null}(e)||t}function cr(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function dr(e,t,r){return Xt(e,Zt(t,r))}function hr(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function gr(e,t){return t.reduce((function(t,r){return t[r]=e,t}),{})}var ur=function(e,t){return hr("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:gr(e,At))};const mr={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,r=e.state,i=e.name,o=e.options,n=r.elements.arrow,a=r.modifiersData.popperOffsets,s=Qt(r.placement),l=cr(s),c=[Lt,Mt].indexOf(s)>=0?"height":"width";if(n&&a){var d=ur(o.padding,r),h=tr(n),g="y"===l?Ot:Lt,u="y"===l?zt:Mt,m=r.rects.reference[c]+r.rects.reference[l]-a[l]-r.rects.popper[c],p=a[l]-r.rects.reference[l],f=lr(n),v=f?"y"===l?f.clientHeight||0:f.clientWidth||0:0,b=m/2-p/2,y=d[g],w=v-h[c]-d[u],$=v/2-h[c]/2+b,x=dr(y,$,w),k=l;r.modifiersData[i]=((t={})[k]=x,t.centerOffset=x-$,t)}},effect:function(e){var t=e.state,r=e.options.element,i=void 0===r?"[data-popper-arrow]":r;null!=i&&("string"!=typeof i||(i=t.elements.popper.querySelector(i)))&&rr(t.elements.popper,i)&&(t.elements.arrow=i)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function pr(e){return e.split("-")[1]}var fr={top:"auto",right:"auto",bottom:"auto",left:"auto"};function vr(e){var t,r=e.popper,i=e.popperRect,o=e.placement,n=e.variation,a=e.offsets,s=e.position,l=e.gpuAcceleration,c=e.adaptive,d=e.roundOffsets,h=e.isFixed,g=a.x,u=void 0===g?0:g,m=a.y,p=void 0===m?0:m,f="function"==typeof d?d({x:u,y:p}):{x:u,y:p};u=f.x,p=f.y;var v=a.hasOwnProperty("x"),b=a.hasOwnProperty("y"),y=Lt,w=Ot,$=window;if(c){var x=lr(r),k="clientHeight",C="clientWidth";if(x===Vt(r)&&"static"!==ir(x=nr(r)).position&&"absolute"===s&&(k="scrollHeight",C="scrollWidth"),o===Ot||(o===Lt||o===Mt)&&n===Rt)w=zt,p-=(h&&x===$&&$.visualViewport?$.visualViewport.height:x[k])-i.height,p*=l?1:-1;if(o===Lt||(o===Ot||o===zt)&&n===Rt)y=Mt,u-=(h&&x===$&&$.visualViewport?$.visualViewport.width:x[C])-i.width,u*=l?1:-1}var _,T=Object.assign({position:s},c&&fr),S=!0===d?function(e){var t=e.x,r=e.y,i=window.devicePixelRatio||1;return{x:Jt(t*i)/i||0,y:Jt(r*i)/i||0}}({x:u,y:p}):{x:u,y:p};return u=S.x,p=S.y,l?Object.assign({},T,((_={})[w]=b?"0":"",_[y]=v?"0":"",_.transform=($.devicePixelRatio||1)<=1?"translate("+u+"px, "+p+"px)":"translate3d("+u+"px, "+p+"px, 0)",_)):Object.assign({},T,((t={})[w]=b?p+"px":"",t[y]=v?u+"px":"",t.transform="",t))}var br={passive:!0};var yr={left:"right",right:"left",bottom:"top",top:"bottom"};function wr(e){return e.replace(/left|right|bottom|top/g,(function(e){return yr[e]}))}var $r={start:"end",end:"start"};function xr(e){return e.replace(/start|end/g,(function(e){return $r[e]}))}function kr(e){var t=Vt(e);return{scrollLeft:t.pageXOffset,scrollTop:t.pageYOffset}}function Cr(e){return er(nr(e)).left+kr(e).scrollLeft}function _r(e){var t=ir(e),r=t.overflow,i=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(r+o+i)}function Tr(e){return["html","body","#document"].indexOf(Pt(e))>=0?e.ownerDocument.body:Ut(e)&&_r(e)?e:Tr(ar(e))}function Sr(e,t){var r;void 0===t&&(t=[]);var i=Tr(e),o=i===(null==(r=e.ownerDocument)?void 0:r.body),n=Vt(i),a=o?[n].concat(n.visualViewport||[],_r(i)?i:[]):i,s=t.concat(a);return o?s:s.concat(Sr(ar(a)))}function Er(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function Or(e,t){return t===Ht?Er(function(e){var t=Vt(e),r=nr(e),i=t.visualViewport,o=r.clientWidth,n=r.clientHeight,a=0,s=0;return i&&(o=i.width,n=i.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(a=i.offsetLeft,s=i.offsetTop)),{width:o,height:n,x:a+Cr(e),y:s}}(e)):Kt(t)?function(e){var t=er(e);return t.top=t.top+e.clientTop,t.left=t.left+e.clientLeft,t.bottom=t.top+e.clientHeight,t.right=t.left+e.clientWidth,t.width=e.clientWidth,t.height=e.clientHeight,t.x=t.left,t.y=t.top,t}(t):Er(function(e){var t,r=nr(e),i=kr(e),o=null==(t=e.ownerDocument)?void 0:t.body,n=Xt(r.scrollWidth,r.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),a=Xt(r.scrollHeight,r.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),s=-i.scrollLeft+Cr(e),l=-i.scrollTop;return"rtl"===ir(o||r).direction&&(s+=Xt(r.clientWidth,o?o.clientWidth:0)-n),{width:n,height:a,x:s,y:l}}(nr(e)))}function zr(e,t,r){var i="clippingParents"===t?function(e){var t=Sr(ar(e)),r=["absolute","fixed"].indexOf(ir(e).position)>=0&&Ut(e)?lr(e):e;return Kt(r)?t.filter((function(e){return Kt(e)&&rr(e,r)&&"body"!==Pt(e)})):[]}(e):[].concat(t),o=[].concat(i,[r]),n=o[0],a=o.reduce((function(t,r){var i=Or(e,r);return t.top=Xt(i.top,t.top),t.right=Zt(i.right,t.right),t.bottom=Zt(i.bottom,t.bottom),t.left=Xt(i.left,t.left),t}),Or(e,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function Mr(e){var t,r=e.reference,i=e.element,o=e.placement,n=o?Qt(o):null,a=o?pr(o):null,s=r.x+r.width/2-i.width/2,l=r.y+r.height/2-i.height/2;switch(n){case Ot:t={x:s,y:r.y-i.height};break;case zt:t={x:s,y:r.y+r.height};break;case Mt:t={x:r.x+r.width,y:l};break;case Lt:t={x:r.x-i.width,y:l};break;default:t={x:r.x,y:r.y}}var c=n?cr(n):null;if(null!=c){var d="y"===c?"height":"width";switch(a){case It:t[c]=t[c]-(r[d]/2-i[d]/2);break;case Rt:t[c]=t[c]+(r[d]/2-i[d]/2)}}return t}function Lr(e,t){void 0===t&&(t={});var r=t,i=r.placement,o=void 0===i?e.placement:i,n=r.boundary,a=void 0===n?Ft:n,s=r.rootBoundary,l=void 0===s?Ht:s,c=r.elementContext,d=void 0===c?Nt:c,h=r.altBoundary,g=void 0!==h&&h,u=r.padding,m=void 0===u?0:u,p=hr("number"!=typeof m?m:gr(m,At)),f=d===Nt?jt:Nt,v=e.rects.popper,b=e.elements[g?f:d],y=zr(Kt(b)?b:b.contextElement||nr(e.elements.popper),a,l),w=er(e.elements.reference),$=Mr({reference:w,element:v,strategy:"absolute",placement:o}),x=Er(Object.assign({},v,$)),k=d===Nt?x:w,C={top:y.top-k.top+p.top,bottom:k.bottom-y.bottom+p.bottom,left:y.left-k.left+p.left,right:k.right-y.right+p.right},_=e.modifiersData.offset;if(d===Nt&&_){var T=_[o];Object.keys(C).forEach((function(e){var t=[Mt,zt].indexOf(e)>=0?1:-1,r=[Ot,zt].indexOf(e)>=0?"y":"x";C[e]+=T[r]*t}))}return C}function Dr(e,t){void 0===t&&(t={});var r=t,i=r.placement,o=r.boundary,n=r.rootBoundary,a=r.padding,s=r.flipVariations,l=r.allowedAutoPlacements,c=void 0===l?Bt:l,d=pr(i),h=d?s?Wt:Wt.filter((function(e){return pr(e)===d})):At,g=h.filter((function(e){return c.indexOf(e)>=0}));0===g.length&&(g=h);var u=g.reduce((function(t,r){return t[r]=Lr(e,{placement:r,boundary:o,rootBoundary:n,padding:a})[Qt(r)],t}),{});return Object.keys(u).sort((function(e,t){return u[e]-u[t]}))}const Ar={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,r=e.options,i=e.name;if(!t.modifiersData[i]._skip){for(var o=r.mainAxis,n=void 0===o||o,a=r.altAxis,s=void 0===a||a,l=r.fallbackPlacements,c=r.padding,d=r.boundary,h=r.rootBoundary,g=r.altBoundary,u=r.flipVariations,m=void 0===u||u,p=r.allowedAutoPlacements,f=t.options.placement,v=Qt(f),b=l||(v===f||!m?[wr(f)]:function(e){if(Qt(e)===Dt)return[];var t=wr(e);return[xr(e),t,xr(t)]}(f)),y=[f].concat(b).reduce((function(e,r){return e.concat(Qt(r)===Dt?Dr(t,{placement:r,boundary:d,rootBoundary:h,padding:c,flipVariations:m,allowedAutoPlacements:p}):r)}),[]),w=t.rects.reference,$=t.rects.popper,x=new Map,k=!0,C=y[0],_=0;_=0,z=O?"width":"height",M=Lr(t,{placement:T,boundary:d,rootBoundary:h,altBoundary:g,padding:c}),L=O?E?Mt:Lt:E?zt:Ot;w[z]>$[z]&&(L=wr(L));var D=wr(L),A=[];if(n&&A.push(M[S]<=0),s&&A.push(M[L]<=0,M[D]<=0),A.every((function(e){return e}))){C=T,k=!1;break}x.set(T,A)}if(k)for(var I=function(e){var t=y.find((function(t){var r=x.get(t);if(r)return r.slice(0,e).every((function(e){return e}))}));if(t)return C=t,"break"},R=m?3:1;R>0;R--){if("break"===I(R))break}t.placement!==C&&(t.modifiersData[i]._skip=!0,t.placement=C,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Ir(e,t,r){return void 0===r&&(r={x:0,y:0}),{top:e.top-t.height-r.y,right:e.right-t.width+r.x,bottom:e.bottom-t.height+r.y,left:e.left-t.width-r.x}}function Rr(e){return[Ot,Mt,zt,Lt].some((function(t){return e[t]>=0}))}const Fr={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var t=e.state,r=e.options,i=e.name,o=r.offset,n=void 0===o?[0,0]:o,a=Bt.reduce((function(e,r){return e[r]=function(e,t,r){var i=Qt(e),o=[Lt,Ot].indexOf(i)>=0?-1:1,n="function"==typeof r?r(Object.assign({},t,{placement:e})):r,a=n[0],s=n[1];return a=a||0,s=(s||0)*o,[Lt,Mt].indexOf(i)>=0?{x:s,y:a}:{x:a,y:s}}(r,t.rects,n),e}),{}),s=a[t.placement],l=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=l,t.modifiersData.popperOffsets.y+=c),t.modifiersData[i]=a}};const Hr={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,r=e.options,i=e.name,o=r.mainAxis,n=void 0===o||o,a=r.altAxis,s=void 0!==a&&a,l=r.boundary,c=r.rootBoundary,d=r.altBoundary,h=r.padding,g=r.tether,u=void 0===g||g,m=r.tetherOffset,p=void 0===m?0:m,f=Lr(t,{boundary:l,rootBoundary:c,padding:h,altBoundary:d}),v=Qt(t.placement),b=pr(t.placement),y=!b,w=cr(v),$="x"===w?"y":"x",x=t.modifiersData.popperOffsets,k=t.rects.reference,C=t.rects.popper,_="function"==typeof p?p(Object.assign({},t.rects,{placement:t.placement})):p,T="number"==typeof _?{mainAxis:_,altAxis:_}:Object.assign({mainAxis:0,altAxis:0},_),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,E={x:0,y:0};if(x){if(n){var O,z="y"===w?Ot:Lt,M="y"===w?zt:Mt,L="y"===w?"height":"width",D=x[w],A=D+f[z],I=D-f[M],R=u?-C[L]/2:0,F=b===It?k[L]:C[L],H=b===It?-C[L]:-k[L],N=t.elements.arrow,j=u&&N?tr(N):{width:0,height:0},W=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},B=W[z],q=W[M],P=dr(0,k[L],j[L]),V=y?k[L]/2-R-P-B-T.mainAxis:F-P-B-T.mainAxis,K=y?-k[L]/2+R+P+q+T.mainAxis:H+P+q+T.mainAxis,U=t.elements.arrow&&lr(t.elements.arrow),Y=U?"y"===w?U.clientTop||0:U.clientLeft||0:0,G=null!=(O=null==S?void 0:S[w])?O:0,Q=D+K-G,X=dr(u?Zt(A,D+V-G-Y):A,D,u?Xt(I,Q):I);x[w]=X,E[w]=X-D}if(s){var Z,J="x"===w?Ot:Lt,ee="x"===w?zt:Mt,te=x[$],re="y"===$?"height":"width",ie=te+f[J],oe=te-f[ee],ne=-1!==[Ot,Lt].indexOf(v),ae=null!=(Z=null==S?void 0:S[$])?Z:0,se=ne?ie:te-k[re]-C[re]-ae+T.altAxis,le=ne?te+k[re]+C[re]-ae-T.altAxis:oe,ce=u&&ne?function(e,t,r){var i=dr(e,t,r);return i>r?r:i}(se,te,le):dr(u?se:ie,te,u?le:oe);x[$]=ce,E[$]=ce-te}t.modifiersData[i]=E}},requiresIfExists:["offset"]};function Nr(e,t,r){void 0===r&&(r=!1);var i,o,n=Ut(t),a=Ut(t)&&function(e){var t=e.getBoundingClientRect(),r=Jt(t.width)/e.offsetWidth||1,i=Jt(t.height)/e.offsetHeight||1;return 1!==r||1!==i}(t),s=nr(t),l=er(e,a),c={scrollLeft:0,scrollTop:0},d={x:0,y:0};return(n||!n&&!r)&&(("body"!==Pt(t)||_r(s))&&(c=(i=t)!==Vt(i)&&Ut(i)?{scrollLeft:(o=i).scrollLeft,scrollTop:o.scrollTop}:kr(i)),Ut(t)?((d=er(t,!0)).x+=t.clientLeft,d.y+=t.clientTop):s&&(d.x=Cr(s))),{x:l.left+c.scrollLeft-d.x,y:l.top+c.scrollTop-d.y,width:l.width,height:l.height}}function jr(e){var t=new Map,r=new Set,i=[];function o(e){r.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!r.has(e)){var i=t.get(e);i&&o(i)}})),i.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){r.has(e.name)||o(e)})),i}var Wr={placement:"bottom",modifiers:[],strategy:"absolute"};function Br(){for(var e=arguments.length,t=new Array(e),r=0;r{},onAfterHide:()=>{},onTransitionEnd:()=>{}},r),this.isVisible=!1,this.popover.hidden=!0,this.popover.classList.remove(this.options.visibleClass),this.popover.addEventListener("transitionend",this.handleTransitionEnd)}handleTransitionEnd(e){e.target===this.options.transitionElement&&(this.options.onTransitionEnd.call(this,e),this.isVisible||this.popover.hidden||(this.popover.hidden=!0,this.popover.classList.remove(this.options.visibleClass),this.options.onAfterHide.call(this)))}destroy(){this.popover.removeEventListener("transitionend",this.handleTransitionEnd),this.popper&&(this.popper.destroy(),this.popper=null)}show(){this.isVisible=!0,this.popover.hidden=!1,requestAnimationFrame((()=>this.popover.classList.add(this.options.visibleClass))),this.popper&&this.popper.destroy(),this.popper=Pr(this.anchor,this.popover,{placement:this.options.placement,strategy:this.options.strategy,modifiers:[{name:"flip",options:{boundary:"viewport"}},{name:"offset",options:{offset:[this.options.skidding,this.options.distance]}}]}),this.popover.addEventListener("transitionend",(()=>this.options.onAfterShow.call(this)),{once:!0}),requestAnimationFrame((()=>this.popper.update()))}hide(){this.isVisible=!1,this.popover.classList.remove(this.options.visibleClass)}setOptions(e){this.options=Object.assign(this.options,e),this.isVisible?this.popover.classList.add(this.options.visibleClass):this.popover.classList.remove(this.options.visibleClass),this.popper&&(this.popper.setOptions({placement:this.options.placement,strategy:this.options.strategy}),requestAnimationFrame((()=>this.popper.update())))}}let Kr=0;const Ur=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.grShow=ue(this,"gr-show",7),this.grAfterShow=ue(this,"gr-after-show",7),this.grHide=ue(this,"gr-hide",7),this.grAfterHide=ue(this,"gr-after-hide",7),this.componentId="dropdown-"+ ++Kr,this.isVisible=!1,this.open=!1,this.placement="bottom-start",this.closeOnSelect=!0,this.containingElement=void 0,this.distance=2,this.skidding=0,this.hoist=!1}handleOpenChange(){this.open?this.show():this.hide(),this.updateAccessibleTrigger()}handlePopoverOptionsChange(){this.popoverElement.setOptions({strategy:this.hoist?"fixed":"absolute",placement:this.placement,distance:this.distance,skidding:this.skidding})}connectedCallback(){this.containingElement||(this.containingElement=this.el),this.handleDocumentKeyDown=this.handleDocumentKeyDown.bind(this),this.handleDocumentMouseDown=this.handleDocumentMouseDown.bind(this),this.handleMenuItemActivate=this.handleMenuItemActivate.bind(this),this.handlePanelSelect=this.handlePanelSelect.bind(this),this.handleTriggerClick=this.handleTriggerClick.bind(this),this.handleTriggerKeyDown=this.handleTriggerKeyDown.bind(this),this.handleTriggerKeyUp=this.handleTriggerKeyUp.bind(this),this.handleTriggerSlotChange=this.handleTriggerSlotChange.bind(this)}componentDidLoad(){this.popoverElement=new Vr(this.trigger,this.positioner,{strategy:this.hoist?"fixed":"absolute",placement:this.placement,distance:this.distance,skidding:this.skidding,transitionElement:this.panel,onAfterHide:()=>this.grAfterHide.emit(),onAfterShow:()=>this.grAfterShow.emit(),onTransitionEnd:()=>{this.open||(this.panel.scrollTop=0)}}),this.open&&this.show()}disconnectedCallback(){this.hide(),this.popoverElement.destroy()}async show(){if(this.isVisible)return;this.grShow.emit().defaultPrevented?this.open=!1:(this.panel.addEventListener("gr-activate",this.handleMenuItemActivate),this.panel.addEventListener("gr-select",this.handlePanelSelect),document.addEventListener("keydown",this.handleDocumentKeyDown),document.addEventListener("mousedown",this.handleDocumentMouseDown),this.isVisible=!0,this.open=!0,this.popoverElement.show())}async hide(){if(!this.isVisible)return;this.grHide.emit().defaultPrevented?this.open=!0:(this.panel.removeEventListener("gr-activate",this.handleMenuItemActivate),this.panel.removeEventListener("gr-select",this.handlePanelSelect),document.addEventListener("keydown",this.handleDocumentKeyDown),document.removeEventListener("mousedown",this.handleDocumentMouseDown),this.isVisible=!1,this.open=!1,this.popoverElement.hide())}async focusOnTrigger(){const e=this.trigger.querySelector("slot").assignedElements({flatten:!0})[0];e&&("function"==typeof e.setFocus?e.setFocus():"function"==typeof e.focus&&e.focus())}getMenu(){return this.panel.querySelector("slot").assignedElements({flatten:!0}).filter((e=>"gr-menu"===e.tagName.toLowerCase()))[0]}handleDocumentKeyDown(e){var t;if("Escape"===e.key)return this.hide(),void this.focusOnTrigger();if("Tab"===e.key){if(this.open&&"gr-menu-item"===(null===(t=document.activeElement)||void 0===t?void 0:t.tagName.toLowerCase()))return e.preventDefault(),this.hide(),void this.focusOnTrigger();setTimeout((()=>{var e;const t=this.containingElement.getRootNode()instanceof ShadowRoot?null===(e=document.activeElement.shadowRoot)||void 0===e?void 0:e.activeElement:document.activeElement;(null==t?void 0:t.closest(this.containingElement.tagName.toLowerCase()))===this.containingElement||this.hide()}))}}handleDocumentMouseDown(e){e.composedPath().includes(this.containingElement)||this.hide()}handleMenuItemActivate(e){Tt(e.target,this.panel)}handlePanelSelect(e){const t=e.target;this.closeOnSelect&&"gr-menu"===t.tagName.toLowerCase()&&(this.hide(),this.focusOnTrigger())}handleTriggerClick(){this.open?this.hide():this.show()}handleTriggerKeyDown(e){const t=this.getMenu(),r=t?[...t.querySelectorAll("gr-menu-item")]:null,i=r[0],o=r[r.length-1];if("Escape"===e.key)return this.focusOnTrigger(),void this.hide();if([" ","Enter"].includes(e.key))return e.preventDefault(),void(this.open?this.hide():this.show());if(["ArrowDown","ArrowUp"].includes(e.key)){if(e.preventDefault(),this.open||this.show(),"ArrowDown"===e.key&&i)return void i.setFocus();if("ArrowUp"===e.key&&o)return void o.setFocus()}this.open&&t&&!["Tab","Shift","Meta","Ctrl","Alt"].includes(e.key)&&t.typeToSelect(e.key)}handleTriggerKeyUp(e){" "===e.key&&e.preventDefault()}handleTriggerSlotChange(){this.updateAccessibleTrigger()}updateAccessibleTrigger(){const e=this.trigger.querySelector("slot").assignedElements({flatten:!0}).map(Et)[0];e&&(e.setAttribute("aria-haspopup","true"),e.setAttribute("aria-expanded",this.open?"true":"false"))}render(){return se(ce,{id:this.componentId,class:{"dropdown-open":this.open}},se("span",{class:"dropdown-trigger",ref:e=>this.trigger=e,onClick:this.handleTriggerClick,onKeyDown:this.handleTriggerKeyDown,onKeyUp:this.handleTriggerKeyUp},se("slot",{name:"trigger",onSlotchange:this.handleTriggerSlotChange})),se("div",{ref:e=>this.positioner=e,class:"dropdown-positioner"},se("div",{ref:e=>this.panel=e,class:"dropdown-panel",role:"menu","aria-hidden":this.open?"false":"true","aria-labelledby":this.componentId},se("slot",null))))}get el(){return this}static get watchers(){return{open:["handleOpenChange"],distance:["handlePopoverOptionsChange"],hoist:["handlePopoverOptionsChange"],placement:["handlePopoverOptionsChange"],skidding:["handlePopoverOptionsChange"]}}static get style(){return".gr-scroll-lock{overflow:hidden !important}:host{--panel-background-color:var(--gr-color-white);--panel-border-radius:var(--gr-border-radius-medium);--panel-border-color:var(--gr-panel-border-color);--panel-box-shadow:var(--gr-shadow-large);--transition:150ms opacity, 150ms transform;display:inline-block;position:relative;box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}.dropdown-trigger{display:block}.dropdown-positioner{position:absolute;z-index:var(--gr-z-index-dropdown)}.dropdown-panel{max-height:50vh;font-family:var(--gr-font-family);font-size:var(--gr-font-size-medium);font-weight:var(--gr-font-weight-normal);background-color:var(--panel-background-color);border:solid 1px var(--panel-border-color);border-radius:var(--panel-border-radius);box-shadow:var(--panel-box-shadow);opacity:0;overflow:auto;overscroll-behavior:none;pointer-events:none;transform:scale(0.9);transition:var(--transition)}.dropdown-positioner[data-popper-placement^=top] .dropdown-panel{transform-origin:bottom}.dropdown-positioner[data-popper-placement^=bottom] .dropdown-panel{transform-origin:top}.dropdown-positioner[data-popper-placement^=left] .dropdown-panel{transform-origin:right}.dropdown-positioner[data-popper-placement^=right] .dropdown-panel{transform-origin:left}.dropdown-positioner.popover-visible .dropdown-panel{opacity:1;transform:none;pointer-events:all}"}},[1,"gr-dropdown",{open:[1540],placement:[1],closeOnSelect:[4,"close-on-select"],containingElement:[1040],distance:[2],skidding:[2],hoist:[4],show:[64],hide:[64],focusOnTrigger:[64]}]);function Yr(){if("undefined"==typeof customElements)return;["gr-dropdown"].forEach((e=>{if("gr-dropdown"===e)customElements.get(e)||customElements.define(e,Ur)}))}Yr(); +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +const Gr=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.grSelect=ue(this,"gr-select",7),this.typeToSelectString=""}connectedCallback(){this.handleClick=this.handleClick.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}async typeToSelect(e){clearTimeout(this.typeToSelectTimeout),this.typeToSelectTimeout=setTimeout((()=>this.typeToSelectString=""),750),this.typeToSelectString+=e.toLowerCase();const t=this.getItems();for(const e of t){if(yt(e.shadowRoot.querySelector("slot:not([name])")).toLowerCase().trim().substring(0,this.typeToSelectString.length)===this.typeToSelectString){e.setFocus();break}}}getItems(){return[...this.menu.querySelector("slot").assignedElements({flatten:!0})].filter((e=>"gr-menu-item"===e.tagName.toLowerCase()&&!e.disabled))}getActiveItem(){return this.getItems().find((e=>e===document.activeElement))}setActiveItem(e){e.setFocus()}handleClick(e){const t=e.target.closest("gr-menu-item");t&&!t.disabled&&this.grSelect.emit({item:t})}handleKeyDown(e){if("Enter"===e.key){const t=this.getActiveItem();e.preventDefault(),t&&this.grSelect.emit({item:t})}if(" "===e.key&&e.preventDefault(),["ArrowDown","ArrowUp","Home","End"].includes(e.key)){const t=this.getItems(),r=this.getActiveItem();let i=t.indexOf(r);if(t.length)return e.preventDefault(),"ArrowDown"===e.key?i++:"ArrowUp"===e.key?i--:"Home"===e.key?i=0:"End"===e.key&&(i=t.length-1),i<0&&(i=0),i>t.length-1&&(i=t.length-1),void this.setActiveItem(t[i])}this.typeToSelect(e.key)}render(){return se("div",{ref:e=>this.menu=e,class:"menu",role:"menu",onClick:this.handleClick,onKeyDown:this.handleKeyDown},se("slot",null))}static get style(){return".gr-scroll-lock{overflow:hidden !important}:host{--padding-top:var(--gr-spacing-x-small);--padding-bottom:var(--gr-spacing-x-small);display:block;padding-top:var(--padding-top);padding-left:0;padding-right:0;padding-bottom:var(--padding-bottom);box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}:host:focus{outline:none}"}},[1,"gr-menu",{typeToSelect:[64]}]);function Qr(){if("undefined"==typeof customElements)return;["gr-menu"].forEach((e=>{if("gr-menu"===e)customElements.get(e)||customElements.define(e,Gr)}))}Qr(); +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +const Xr=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.grClear=ue(this,"gr-clear",7),this.type="primary",this.size="medium",this.pill=!1,this.clearable=!1}connectedCallback(){this.handleClearClick=this.handleClearClick.bind(this)}handleClearClick(){this.grClear.emit()}render(){return se(ce,{class:{[`tag-${this.type}`]:!0,[`tag-${this.size}`]:!0,"tag-pill":this.pill,"tag-clearable":this.clearable}},se("span",{class:"tag"},se("slot",null),this.clearable&&se("gr-button",{variant:"plain",size:this.size,class:"tag-clear","aria-label":"clear",onClick:this.handleClearClick},se("svg",{slot:"icon-only",role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Close"),se("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"32",d:"M368 368L144 144M368 144L144 368"})))))}static get style(){return":host{--height:calc(var(--gr-form-element-height-medium) * 0.8);--line-height:calc(var(--gr-form-element-height-medium) - 1px * 2);--border-radius:var(--gr-form-element-border-radius-medium);--border-width:1px;--border-style:solid;--padding-top:0;--padding-start:var(--gr-spacing-small);--padding-end:var(--gr-spacing-small);--padding-bottom:0;--font-size:var(--gr-form-element-font-size-medium);--background-color:rgba(var(--gr-color-primary-rgb), 0.05);--border-color:rgba(var(--gr-color-primary-rgb), 0.2);--color:var(--gr-color-primary-shade);--clear-color:var(--gr-color-primary);--clear-color-hover:var(--gr-color-primary-shade);--clear-margin-left:var(--gr-spacing-xx-small);--clear-margin-right:calc(-1 * var(--gr-spacing-xxx-small));display:inline-block;box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}:host(.tag-success){--background-color:rgba(var(--gr-color-success-rgb), 0.05);--border-color:rgba(var(--gr-color-success-rgb), 0.2);--color:var(--gr-color-success-shade);--clear-color:var(--gr-color-success);--clear-color-hover:var(--gr-color-success-shade)}:host(.tag-info){--background-color:rgba(var(--gr-color-medium-rgb), 0.05);--border-color:rgba(var(--gr-color-medium-rgb), 0.2);--color:var(--gr-color-medium-shade);--clear-color:var(--gr-color-medium);--clear-color-hover:var(--gr-color-medium-shade)}:host(.tag-warning){--background-color:rgba(var(--gr-color-warning-rgb), 0.05);--border-color:rgba(var(--gr-color-warning-rgb), 0.2);--color:var(--gr-color-warning-shade);--clear-color:var(--gr-color-warning);--clear-color-hover:var(--gr-color-warning-shade)}:host(.tag-danger){--background-color:rgba(var(--gr-color-danger-rgb), 0.05);--border-color:rgba(var(--gr-color-danger-rgb), 0.2);--color:var(--gr-color-danger-shade);--clear-color:var(--gr-color-danger);--clear-color-hover:var(--gr-color-danger-shade)}:host(.tag-small){--font-size:var(--gr-form-element-font-size-small);--height:calc(var(--gr-form-element-height-small) * 0.8);--line-height:calc(var(--gr-form-element-height-small) - 1px * 2);--border-radius:var(--gr-form-element-border-radius-small);--padding-start:var(--gr-spacing-x-small);--padding-end:var(--gr-spacing-x-small);--clear-margin-left:var(--gr-spacing-xx-small);--clear-margin-right:calc(-1 * var(--gr-spacing-xxx-small))}:host(.tag-large){--font-size:var(--gr-form-element-font-size-large);--height:calc(var(--gr-form-element-height-large) * 0.8);--line-height:calc(var(--gr-form-element-height-large) - 1px * 2);--border-radius:var(--gr-form-element-border-radius-large);--padding:0 var(--gr-spacing-medium);--clear-margin-left:var(--gr-spacing-xx-small);--clear-margin-right:calc(-1 * var(--gr-spacing-x-small))}.tag{display:flex;align-items:center;border-style:var(--border-style);border-width:var(--border-width);border-radius:var(--border-radius);white-space:nowrap;user-select:none;cursor:default;font-family:var(--gr-font-family);font-size:var(--font-size);font-weight:var(--gr-font-weight-normal);height:var(--height);line-height:var(--line-height);padding-top:var(--padding-top);padding-left:var(--padding-start);padding-right:var(--padding-end);padding-bottom:var(--padding-bottom);background-color:var(--background-color);border-color:var(--border-color);color:var(--color)}.tag-clear{--color:var(--clear-color);--color-hover:var(--clear-color-hover);--padding-start:0;--padding-end:0;margin-left:var(--clear-margin-left);margin-right:var(--clear-margin-right);--height:1em}.tag-clear svg{font-size:0.7em}.tag-clear svg{width:1.1em;height:1.1em}:host(.tag-pill) .tag{border-radius:var(--height)}"}},[1,"gr-tag",{type:[513],size:[513],pill:[516],clearable:[516]}]);function Zr(){if("undefined"==typeof customElements)return;["gr-tag","gr-button","gr-spinner"].forEach((e=>{switch(e){case"gr-tag":customElements.get(e)||customElements.define(e,Xr);break;case"gr-button":customElements.get(e)||_t();break;case"gr-spinner":customElements.get(e)||kt()}}))}Zr(); +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */ +const Jr="undefined"!=typeof HTMLElement?HTMLElement:class{};let ei=0;const ti=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.grChange=ue(this,"gr-change",7),this.grFocus=ue(this,"gr-focus",7),this.grBlur=ue(this,"gr-blur",7),this.inputId="select-"+ ++ei,this.labelId=`select-label-${ei}`,this.helpTextId=`select-help-text-${ei}`,this.invalidTextId=`select-invalid-text-${ei}`,this.inheritedAttributes={},this.handleBlur=()=>{this.isOpen||(this.hasFocus=!1,this.grBlur.emit())},this.handleFocus=()=>{this.hasFocus||(this.hasFocus=!0,this.grFocus.emit())},this.hasFocus=!1,this.hasHelpTextSlot=!1,this.hasInvalidTextSlot=!1,this.hasLabelSlot=!1,this.isOpen=!1,this.items=[],this.displayLabel="",this.displayTags=[],this.multiple=!1,this.maxTagsVisible=3,this.disabled=!1,this.name="",this.placeholder="",this.size="medium",this.hoist=!1,this.value="",this.pill=!1,this.label="",this.requiredIndicator=!1,this.helpText="",this.invalidText="",this.invalid=!1,this.clearable=!1}handleDisabledChange(){this.disabled&&this.isOpen&&this.dropdown.hide()}handleLabelChange(){this.handleSlotChange()}handleMultipleChange(){const e=this.getValueAsArray();this.value=this.multiple?e:e[0]||"",this.syncItemsFromValue()}handleValueChange(){this.syncItemsFromValue(),this.grChange.emit()}connectedCallback(){this.handleClearClick=this.handleClearClick.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleLabelClick=this.handleLabelClick.bind(this),this.handleMenuHide=this.handleMenuHide.bind(this),this.handleMenuShow=this.handleMenuShow.bind(this),this.handleMenuSelect=this.handleMenuSelect.bind(this),this.handleSlotChange=this.handleSlotChange.bind(this),this.handleTagInteraction=this.handleTagInteraction.bind(this),this.el.shadowRoot.addEventListener("slotchange",this.handleSlotChange)}componentWillLoad(){this.handleSlotChange(),this.inheritedAttributes=$t(this.el,["aria-label"])}componentDidLoad(){this.resizeObserver=new ResizeObserver((()=>this.resizeMenu())),this.reportDuplicateItemValues(),requestAnimationFrame((()=>this.syncItemsFromValue()))}disconnectedCallback(){this.el.shadowRoot.removeEventListener("slotchange",this.handleSlotChange)}async setFocus(){this.hasFocus=!0,this.grFocus.emit(),this.dropdown.focusOnTrigger()}getItemLabel(e){return yt(e.shadowRoot.querySelector("slot:not([name])"))}getItems(){return[...this.el.querySelectorAll("gr-menu-item")]}getValueAsArray(){return Array.isArray(this.value)?this.value:[this.value]}handleClearClick(e){e.stopPropagation(),this.value=this.multiple?[]:"",this.syncItemsFromValue()}handleKeyDown(e){const t=e.target,r=this.getItems(),i=r[0],o=r[r.length-1];if("gr-tag"!==t.tagName.toLowerCase())if("Tab"!==e.key){if(["ArrowDown","ArrowUp"].includes(e.key)){if(e.preventDefault(),this.isOpen||this.dropdown.show(),"ArrowDown"===e.key&&i)return void i.setFocus();if("ArrowUp"===e.key&&o)return void o.setFocus()}this.isOpen||1!==e.key.length||(e.stopPropagation(),e.preventDefault(),this.dropdown.show(),this.menu.typeToSelect(e.key))}else this.isOpen&&this.dropdown.hide()}handleLabelClick(){this.box.focus()}handleMenuSelect(e){const t=e.detail.item;this.multiple?this.value=this.value.includes(t.value)?this.value.filter((e=>e!==t.value)):[...this.value,t.value]:this.value=t.value,this.syncItemsFromValue()}handleMenuShow(e){this.disabled?e.preventDefault():(this.resizeMenu(),this.resizeObserver.observe(this.el),this.isOpen=!0)}handleMenuHide(){this.resizeObserver.unobserve(this.el),this.isOpen=!1,this.box.focus()}handleSlotChange(){this.hasHelpTextSlot=wt(this.el,"help-text"),this.hasInvalidTextSlot=wt(this.el,"invalid-text"),this.hasLabelSlot=wt(this.el,"label"),this.syncItemsFromValue(),this.reportDuplicateItemValues()}handleTagInteraction(e){e.composedPath().find((e=>{if(e instanceof Jr)return e.classList.contains("tag-clear")}))&&e.stopPropagation()}reportDuplicateItemValues(){const e=this.getItems().map((e=>e.value)).filter(((e,t,r)=>r.indexOf(e)!==t));if(e.length)throw new Error('Duplicate value found on in : "'+e.join('", "')+'"')}resizeMenu(){this.menu.style.width=`${this.box.clientWidth}px`}syncItemsFromValue(){const e=this.getItems(),t=this.getValueAsArray();if(e.map((e=>e.checked=t.includes(e.value))),this.multiple){const r=[];if(t.map((t=>e.map((e=>e.value===t?r.push(e):null)))),this.displayTags=r.map((e=>se("gr-tag",{type:"info",size:this.size,pill:this.pill,clearable:!0,onClick:this.handleTagInteraction,onKeyDown:this.handleTagInteraction,"onGr-clear":t=>{t.stopPropagation(),this.disabled||(e.checked=!1,this.syncValueFromItems())}},this.getItemLabel(e)))),this.maxTagsVisible>0&&this.displayTags.length>this.maxTagsVisible){const e=this.displayTags.length;this.displayLabel="",this.displayTags=this.displayTags.slice(0,this.maxTagsVisible),this.displayTags.push(se("gr-tag",{type:"info",size:this.size,pill:this.pill},"+",e-this.maxTagsVisible))}}else{const r=e.filter((e=>e.value===t[0]))[0];this.displayLabel=r?this.getItemLabel(r):"",this.displayTags=[]}}syncValueFromItems(){const e=this.getItems().filter((e=>e.checked)).map((e=>e.value));this.multiple?this.value=this.value.filter((t=>e.includes(t))):this.value=e.length>0?e[0]:""}render(){var e;const t=this.multiple?this.value.length>0:""!==this.value,r=this.inheritedAttributes["aria-label"]?{"aria-label":this.inheritedAttributes["aria-label"]}:{"aria-labelledby":this.labelId};return((e,t,r,i)=>{let o=e.querySelector("input.aux-input");o||(o=e.ownerDocument.createElement("input"),o.type="hidden",o.classList.add("aux-input"),e.appendChild(o)),o.disabled=i,o.name=t,o.value=r||""})(this.el,this.name,ri(this.value),this.disabled),se(bt,{inputId:this.inputId,label:this.label,labelId:this.labelId,hasLabelSlot:this.hasLabelSlot,helpTextId:this.helpTextId,helpText:this.helpText,hasHelpTextSlot:this.hasHelpTextSlot,invalidTextId:this.invalidTextId,invalidText:this.invalidText,invalid:this.invalid,hasInvalidTextSlot:this.hasInvalidTextSlot,size:this.size,onLabelClick:this.handleLabelClick,requiredIndicator:this.requiredIndicator},se("gr-dropdown",{ref:e=>this.dropdown=e,hoist:this.hoist,closeOnSelect:!this.multiple,containingElement:this.el,class:{select:!0,"select-open":this.isOpen,"select-empty":0===(null===(e=this.value)||void 0===e?void 0:e.length),"select-focused":this.hasFocus,"select-clearable":this.clearable,"select-disabled":this.disabled,"select-multiple":this.multiple,"select-has-tags":this.multiple&&t,"select-placeholder-visible":""===this.displayLabel,[`select-${this.size}`]:!0,"select-pill":this.pill,"select-invalid":this.invalid},"onGr-show":this.handleMenuShow,"onGr-hide":this.handleMenuHide},se("div",Object.assign({slot:"trigger",ref:e=>this.box=e,id:this.inputId,class:"select-box",role:"combobox"},r,{"aria-describedby":this.invalid?this.invalidTextId:this.helpTextId,"aria-haspopup":"true","aria-expanded":this.isOpen?"true":"false","aria-invalid":this.invalid?"true":"false","aria-required":this.requiredIndicator?"true":"false",tabIndex:this.disabled?-1:0,onBlur:this.handleBlur,onFocus:this.handleFocus,onKeyDown:this.handleKeyDown}),se("div",{class:"select-label"},this.displayTags.length?se("span",{class:"select-tags"},this.displayTags):this.displayLabel||this.placeholder),this.clearable&&t&&se("button",{class:"select-clear",type:"button",onClick:this.handleClearClick,"aria-label":"clear",tabindex:"-1"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Close Circle"),se("path",{d:"M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z",fill:"none",stroke:"currentColor","stroke-miterlimit":"10","stroke-width":"32"}),se("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"32",d:"M320 320L192 192M192 320l128-128"}))),se("span",{class:"caret"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Chevron Down"),se("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"48",d:"M112 184l144 144 144-144"}))),se("input",{class:"select-hidden-select","aria-hidden":"true",value:t?"1":"",tabIndex:-1})),se("gr-menu",{ref:e=>this.menu=e,class:"select-menu","onGr-select":this.handleMenuSelect},se("slot",{onSlotchange:this.handleSlotChange}))))}get el(){return this}static get watchers(){return{disabled:["handleDisabledChange"],helpText:["handleLabelChange"],invalidText:["handleLabelChange"],label:["handleLabelChange"],multiple:["handleMultipleChange"],value:["handleValueChange"]}}static get style(){return".form-control .form-control-label{display:none}.form-control .form-control-help-text{display:none}.form-control .form-control-invalid-text{display:none}.form-control-has-label .form-control-label{display:flex;line-height:var(--gr-line-height-normal);color:var(--gr-form-element-label-color);margin-bottom:var(--gr-spacing-xxx-small)}.form-control-has-label.form-control-small .form-control-label{font-size:var(--gr-form-element-label-font-size-small)}.form-control-has-label.form-control-medium .form-control-label{font-size:var(--gr-form-element-label-font-size-medium)}.form-control-has-label.form-control-large .form-control-label{font-size:var(--gr-form-element-label-font-size-large)}.form-control-has-label .form-control-label .asterisk{margin-left:var(--gr-spacing-x-small);color:var(--gr-color-medium)}.form-control-has-label .form-control-label .asterisk svg{width:0.6em;height:0.6em;margin-bottom:var(--gr-spacing-xxx-small)}.form-control-has-help-text .form-control-help-text{display:block;line-height:var(--gr-line-height-normal);color:var(--gr-form-element-help-text-color);margin-top:var(--gr-spacing-xxx-small)}.form-control-has-help-text.form-control-small .form-control-help-text{font-size:var(--gr-form-element-help-text-font-size-small);min-height:1.625rem}.form-control-has-help-text.form-control-medium .form-control-help-text{font-size:var(--gr-form-element-help-text-font-size-medium);min-height:1.875rem}.form-control-has-help-text.form-control-large .form-control-help-text{font-size:var(--gr-form-element-help-text-font-size-large);min-height:2.125rem}.form-control-has-invalid-text .form-control-invalid-text{display:flex;margin-left:-2px;line-height:var(--gr-line-height-normal);color:var(--gr-form-element-invalid-text-color);margin-top:var(--gr-spacing-xxx-small)}.form-control-has-invalid-text .form-control-invalid-text .icon{margin-top:var(--gr-spacing-xxx-small);margin-right:var(--gr-spacing-xx-small)}.form-control-has-invalid-text .form-control-invalid-text .icon svg{width:1.4em;height:1.4em}.form-control-has-invalid-text.form-control-small .form-control-invalid-text{font-size:var(--gr-form-element-invalid-text-font-size-small);min-height:1.625rem}.form-control-has-invalid-text.form-control-medium .form-control-invalid-text{font-size:var(--gr-form-element-invalid-text-font-size-medium);min-height:1.875rem}.form-control-has-invalid-text.form-control-large .form-control-invalid-text{font-size:var(--gr-form-element-invalid-text-font-size-large);min-height:2.125rem}.gr-scroll-lock{overflow:hidden !important}:host{--font-size:var(--gr-form-element-font-size-medium);--font-weight:var(--gr-font-weight-normal);--background-color:var(--gr-color-white);--background-color-hover:var(--gr-color-white);--background-color-focus:var(--gr-color-white);--background-color-invalid:var(--gr-color-white);--background-color-invalid-hover:var(--gr-color-white);--border-radius:var(--gr-form-element-border-radius-small);--border-color:var(--gr-color-light-shade);--border-color-hover:var(--gr-color-medium);--border-color-focus:var(--gr-color-primary);--border-color-invalid:var(--gr-color-danger);--border-color-invalid-hover:var(--gr-color-danger-shade);--color:var(--gr-color-dark-tint);--placeholder-color:var(--gr-color-medium-tint);--min-height:var(--gr-form-element-height-medium);--label-margin-start:var(--gr-spacing-medium);--label-margin-end:var(--gr-spacing-medium);--clear-icon-margin-end:var(--gr-spacing-medium);--caret-margin-end:var(--gr-spacing-medium);--tags-padding-top:3px;--tags-padding-bottom:3px;--tags-margin-end:var(--gr-spacing-xx-small);--focus-ring:0 0 0 var(--gr-focus-ring-width) rgb(var(--gr-color-primary-rgb), 0.33);display:block;box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}.select-small{--font-size:var(--gr-form-element-font-size-small);--min-height:var(--gr-form-element-height-small);--label-margin-start:var(--gr-spacing-small);--label-margin-end:var(--gr-spacing-small);--clear-icon-margin-end:var(--gr-spacing-small);--caret-margin-end:var(--gr-spacing-small);--tags-padding-top:2px;--tags-padding-bottom:2px}.select-large{--font-size:var(--gr-form-element-font-size-large);--min-height:var(--gr-form-element-height-large);--label-margin-start:var(--gr-spacing-large);--label-margin-end:var(--gr-spacing-large);--clear-icon-margin-end:var(--gr-spacing-large);--caret-margin-end:var(--gr-spacing-large);--tags-padding-top:4px;--tags-padding-bottom:4px}.select{display:block}.select-box{display:inline-flex;align-items:center;justify-content:start;position:relative;width:100%;font-family:var(--gr-font-family);font-size:var(--font-size);font-weight:var(--font-weight);letter-spacing:normal;background-color:var(--background-color);border:solid 1px var(--border-color);border-radius:var(--border-radius);min-height:var(--min-height);color:var(--color);vertical-align:middle;overflow:hidden;transition:150ms color, 150ms border, 150ms box-shadow;cursor:pointer}.select.select-invalid:not(.select-disabled) .select-box{background-color:var(--background-color-invalid);border-color:var(--border-color-invalid)}.select.select-invalid:not(.select-disabled):not(.select-focused) .select-box:hover{background-color:var(--background-color-invalid-hover);border-color:var(--border-color-invalid-hover)}.select.select-invalid:not(.select-disabled) .select-box{background-color:var(--background-color-invalid);border-color:var(--border-color-invalid)}.select:not(.select-disabled) .select-box:hover{background-color:var(--background-color-hover);border-color:var(--border-color-hover)}.select.select-focused:not(.select-disabled) .select-box{outline:none;box-shadow:var(--focus-ring);border-color:var(--border-color-focus);background-color:var(--background-color-focus)}.select-disabled .select-box{opacity:0.5;cursor:not-allowed;outline:none}.select-disabled .select-tags,.select-disabled .select-clear{pointer-events:none}.select-label{flex:1 1 auto;display:flex;align-items:center;user-select:none;margin-top:0;margin-left:var(--label-margin-start);margin-right:var(--label-margin-end);margin-bottom:0;scrollbar-width:none;-ms-overflow-style:none;overflow-x:auto;overflow-y:hidden;white-space:nowrap}.select-label::-webkit-scrollbar{width:0;height:0}.select-has-tags .select-label{margin-left:0}.select-clear{display:inline-flex;align-items:center;font-size:inherit;color:var(--gr-color-medium);border:none;background:none;padding:0;transition:150ms color;cursor:pointer;margin-right:var(--clear-icon-margin-end)}.select-clear:hover{color:var(--gr-color-dark)}.select-clear:focus{outline:none}.select-clear svg{width:1.2em;height:1.2em;font-size:var(--font-size)}.caret{flex:0 0 auto;display:inline-flex;transition:250ms transform ease;margin-right:var(--caret-margin-end)}.caret svg{width:1em;height:1em;font-size:var(--font-size)}.select-open .caret{transform:rotate(-180deg)}.select-placeholder-visible .select-label{color:var(--placeholder-color)}.select-tags{display:inline-flex;align-items:center;flex-wrap:wrap;justify-content:left;margin-left:var(--gr-spacing-xx-small);padding-bottom:var(--tags-padding-bottom)}.select-tags gr-tag{padding-top:var(--tags-padding-top)}.select-tags gr-tag:not(:last-of-type){margin-right:var(--tags-margin-end)}.select-hidden-select{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px;position:absolute;top:0;left:0;width:100%;height:100%}.select-pill .select-box{border-radius:var(--min-height)}"}},[1,"gr-select",{multiple:[4],maxTagsVisible:[2,"max-tags-visible"],disabled:[4],name:[1],placeholder:[1],size:[1],hoist:[4],value:[1025],pill:[4],label:[1],requiredIndicator:[4,"required-indicator"],helpText:[1,"help-text"],invalidText:[1,"invalid-text"],invalid:[516],clearable:[4],hasFocus:[32],hasHelpTextSlot:[32],hasInvalidTextSlot:[32],hasLabelSlot:[32],isOpen:[32],items:[32],displayLabel:[32],displayTags:[32],setFocus:[64]}]),ri=e=>{if(null!=e)return Array.isArray(e)?e.join(","):e.toString()};!function(){if("undefined"==typeof customElements)return;["gr-select","gr-button","gr-dropdown","gr-menu","gr-spinner","gr-tag"].forEach((e=>{switch(e){case"gr-select":customElements.get(e)||customElements.define(e,ti);break;case"gr-button":customElements.get(e)||_t();break;case"gr-dropdown":customElements.get(e)||Yr();break;case"gr-menu":customElements.get(e)||Qr();break;case"gr-spinner":customElements.get(e)||kt();break;case"gr-tag":customElements.get(e)||Zr()}}))}();const ii=ti,oi=Ue(class extends st{constructor(){super(),this.__registerHost(),this.__attachShadow(),this.hasFocus=!1,this.checked=!1,this.value="",this.disabled=!1}connectedCallback(){this.handleBlur=this.handleBlur.bind(this),this.handleFocus=this.handleFocus.bind(this),this.handleMouseEnter=this.handleMouseEnter.bind(this),this.handleMouseLeave=this.handleMouseLeave.bind(this)}async setFocus(e){this.el.focus(e)}async removeFocus(){this.el.blur()}handleBlur(){this.hasFocus=!1}handleFocus(){this.hasFocus=!0}handleMouseEnter(){this.setFocus()}handleMouseLeave(){this.removeFocus()}render(){return se(ce,{class:{"menu-item-checked":this.checked,"menu-item-disabled":this.disabled,"menu-item-focused":this.hasFocus},role:"menuitem","aria-disabled":this.disabled?"true":"false","aria-checked":this.checked?"true":"false",tabIndex:this.disabled?null:0,onFocus:this.handleFocus,onBlur:this.handleBlur,onMouseEnter:this.handleMouseEnter,onMouseLeave:this.handleMouseLeave},se("span",{class:"checkmark"},se("svg",{role:"img","aria-hidden":"true",viewBox:"0 0 512 512"},se("title",null,"Checkmark"),se("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"32",d:"M416 128L192 384l-96-96"}))),se("span",{class:"start"},se("slot",{name:"start"})),se("span",{class:"label"},se("slot",null)),se("span",{class:"end"},se("slot",{name:"end"})))}get el(){return this}static get style(){return".gr-scroll-lock{overflow:hidden !important}:host{--line-height:var(--gr-line-height-normal);--background-color:transparent;--background-color-focused:var(--gr-color-primary);--color:var(--gr-color-dark);--color-focused:var(--gr-color-primary-contrast);--color-disabled:var(--gr-color-medium);--padding-top:var(--gr-spacing-xx-small);--padding-start:var(--gr-spacing-x-large);--padding-end:var(--gr-spacing-x-large);--padding-bottom:var(--gr-spacing-xx-small);--transition:background-color 150ms linear, color 150ms linear;position:relative;display:flex;align-items:stretch;font-family:var(--gr-font-family);font-size:var(--gr-font-size-medium);font-weight:var(--gr-font-weight-normal);line-height:var(--line-height);letter-spacing:var(--gr-letter-spacing-normal);text-align:left;background-color:var(--background-color);color:var(--color);padding-top:var(--padding-top);padding-left:var(--padding-start);padding-right:var(--padding-end);padding-bottom:var(--padding-bottom);transition:var(--transition);user-select:none;white-space:nowrap;cursor:pointer;box-sizing:border-box}:host *,:host *:before,:host *:after{box-sizing:inherit}:host(.menu-item-focused:not(.menu-item-disabled)){outline:none;background-color:var(--background-color-focused);color:var(--color-focused)}:host(.menu-item-disabled){outline:none;color:var(--color-disabled);cursor:not-allowed}.checkmark{display:flex;position:absolute;left:0.5em;top:calc(50% - 0.5em);visibility:hidden;align-items:center;font-size:inherit}.checkmark svg{display:inline-block;width:1.1em;height:1.1em;contain:strict;fill:currentcolor;box-sizing:content-box !important}:host(.menu-item-checked) .checkmark{visibility:visible}.label{flex:1 1 auto}.start{flex:0 0 auto;display:flex;align-items:center}.start ::slotted(:last-child){margin-right:0.5em}.end{flex:0 0 auto;display:flex;align-items:center}.end ::slotted(:first-child){margin-left:0.5em}"}},[1,"gr-menu-item",{checked:[516],value:[513],disabled:[516],hasFocus:[32],setFocus:[64],removeFocus:[64]}]); +/*! + * (C) PAQT.com B.V. https://paqt.com - MIT License + */!function(){if("undefined"==typeof customElements)return;["gr-menu-item"].forEach((e=>{if("gr-menu-item"===e)customElements.get(e)||customElements.define(e,oi)}))}();const ni={"gr-select":ii,"gr-menu-item":oi};class ai extends(q(d)){constructor(){super(...arguments),this.multiple=!1,this.clearable=!1,this._refSelect=o()}_valueChangedHandler(e){const t=this._refSelect.value?.value;void 0===t||n(this.value,t)||(this.value=t,a(this,"select:change",t))}render(){return s` + ${this.options?.map((e=>s`${e.label}`))} + `}static get styles(){return c(':root, :host {\n --gr-color-primary: #1079b2;\n --gr-color-primary-rgb: 16, 121, 178;\n --gr-color-primary-contrast: #ffffff;\n --gr-color-primary-contrast-rgb: 255, 255, 255;\n --gr-color-primary-shade: #0d6696;\n --gr-color-primary-tint: #1499e1;\n --gr-color-secondary: #051f2c;\n --gr-color-secondary-rgb: 5, 31, 44;\n --gr-color-secondary-contrast: #ffffff;\n --gr-color-secondary-contrast-rgb: 255, 255, 255;\n --gr-color-secondary-shade: #000000;\n --gr-color-secondary-tint: #0a415c;\n --gr-color-tertiary: #0c4a6e;\n --gr-color-tertiary-rgb: 12, 74, 110;\n --gr-color-tertiary-contrast: #ffffff;\n --gr-color-tertiary-contrast-rgb: 255, 255, 255;\n --gr-color-tertiary-shade: #083249;\n --gr-color-tertiary-tint: #106393;\n --gr-color-success: #0fbe78;\n --gr-color-success-rgb: 15, 190, 120;\n --gr-color-success-contrast: #000000;\n --gr-color-success-contrast-rgb: 0, 0, 0;\n --gr-color-success-shade: #057f4e;\n --gr-color-success-tint: #12e28f;\n --gr-color-warning: #fbbc4e;\n --gr-color-warning-rgb: 251, 188, 78;\n --gr-color-warning-contrast: #051f2c;\n --gr-color-warning-contrast-rgb: 5, 31, 44;\n --gr-color-warning-shade: #9e6400;\n --gr-color-warning-tint: #fdd187;\n --gr-color-danger: #e60017;\n --gr-color-danger-rgb: 230, 0, 23;\n --gr-color-danger-contrast: #ffffff;\n --gr-color-danger-contrast-rgb: 255, 255, 255;\n --gr-color-danger-shade: #cc0014;\n --gr-color-danger-tint: #ff1f35;\n --gr-color-light: #f4f5f8;\n --gr-color-light-rgb: 244, 245, 248;\n --gr-color-light-contrast: #051f2c;\n --gr-color-light-contrast-rgb: 5, 31, 44;\n --gr-color-light-shade: #d7d8da;\n --gr-color-light-tint: #f9fafb;\n --gr-color-medium: #5e6c78;\n --gr-color-medium-rgb: 94, 108, 120;\n --gr-color-medium-contrast: #ffffff;\n --gr-color-medium-contrast-rgb: 255, 255, 255;\n --gr-color-medium-shade: #48535b;\n --gr-color-medium-tint: #81909c;\n --gr-color-dark: #02131b;\n --gr-color-dark-rgb: 2, 19, 27;\n --gr-color-dark-contrast: #ffffff;\n --gr-color-dark-contrast-rgb: 255, 255, 255;\n --gr-color-dark-shade: #000000;\n --gr-color-dark-tint: #222428;\n --gr-color-white: #ffffff;\n --gr-color-black: #000000;\n --gr-border-radius-small: 0.125rem;\n --gr-border-radius-medium: 0.25rem;\n --gr-border-radius-large: 0.5rem;\n --gr-border-radius-x-large: 1rem;\n --gr-border-width-small: 0.063rem;\n --gr-border-width-medium: 0.125rem;\n --gr-border-width-large: 0.188rem;\n --gr-shadow-x-small: 0 1px 0 #0d131e0d;\n --gr-shadow-small: 0 1px 2px #0d131e1a;\n --gr-shadow-medium: 0 2px 4px #0d131e1a;\n --gr-shadow-large: 0 2px 8px #0d131e1a;\n --gr-shadow-x-large: 0 4px 16px #0d131e1a;\n --gr-spacing-xxx-small: 0.125rem;\n --gr-spacing-xx-small: 0.25rem;\n --gr-spacing-x-small: 0.5rem;\n --gr-spacing-small: 0.75rem;\n --gr-spacing-medium: 1rem;\n --gr-spacing-large: 1.25rem;\n --gr-spacing-x-large: 1.75rem;\n --gr-spacing-xx-large: 2.25rem;\n --gr-spacing-xxx-large: 3rem;\n --gr-spacing-xxxx-large: 4.5rem;\n --gr-font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,\n sans-serif;\n --gr-letter-spacing-dense: -0.015em;\n --gr-letter-spacing-normal: normal;\n --gr-letter-spacing-loose: 0.075em;\n --gr-line-height-dense: 1.4;\n --gr-line-height-normal: 1.8;\n --gr-line-height-loose: 2.2;\n --gr-font-size-xx-small: 0.625rem;\n --gr-font-size-x-small: 0.75rem;\n --gr-font-size-small: 0.875rem;\n --gr-font-size-medium: 1rem;\n --gr-font-size-large: 1.25rem;\n --gr-font-size-x-large: 1.5rem;\n --gr-font-size-xx-large: 2.25rem;\n --gr-font-size-xxx-large: 3rem;\n --gr-font-size-xxxx-large: 4.5rem;\n --gr-font-weight-thin: 100;\n --gr-font-weight-extra-light: 200;\n --gr-font-weight-light: 300;\n --gr-font-weight-normal: 400;\n --gr-font-weight-medium: 500;\n --gr-font-weight-semi-bold: 600;\n --gr-font-weight-bold: 700;\n --gr-font-weight-extra-bold: 800;\n --gr-font-weight-black: 900;\n --gr-form-element-font-size-x-small: var(--gr-font-size-x-small);\n --gr-form-element-font-size-small: var(--gr-font-size-small);\n --gr-form-element-font-size-medium: var(--gr-font-size-medium);\n --gr-form-element-font-size-large: var(--gr-font-size-large);\n --gr-form-element-height-small: 2.188rem;\n --gr-form-element-height-medium: 3.125rem;\n --gr-form-element-height-large: 4.063rem;\n --gr-form-element-border-radius-small: var(--gr-border-radius-medium);\n --gr-form-element-border-radius-medium: var(--gr-border-radius-medium);\n --gr-form-element-border-radius-large: var(--gr-border-radius-medium);\n --gr-focus-ring-width: 2px;\n --gr-form-element-label-font-size-small: var(--gr-font-size-small);\n --gr-form-element-label-font-size-medium: var(--gr-font-size-medium);\n --gr-form-element-label-font-size-large: var(--gr-font-size-large);\n --gr-form-element-label-color: inherit;\n --gr-form-element-help-text-font-size-small: var(--gr-font-size-x-small);\n --gr-form-element-help-text-font-size-medium: var(--gr-font-size-small);\n --gr-form-element-help-text-font-size-large: var(--gr-font-size-medium);\n --gr-form-element-help-text-color: var(--gr-color-medium);\n --gr-form-element-invalid-text-font-size-small: var(--gr-font-size-x-small);\n --gr-form-element-invalid-text-font-size-medium: var(--gr-font-size-small);\n --gr-form-element-invalid-text-font-size-large: var(--gr-font-size-medium);\n --gr-form-element-invalid-text-color: var(--gr-color-danger);\n --gr-toggle-size: 1rem;\n --gr-panel-border-color: var(--gr-color-light-shade);\n --gr-z-index-dropdown: 900;\n}\n\n')}}function si(){var e=new Date,t=e.getFullYear(),r=e.getMonth(),i=e.getDate(),o=new Date(0);return o.setFullYear(t,r,i-1),o.setHours(23,59,59,999),o}function li(){var e=new Date,t=e.getFullYear(),r=e.getMonth(),i=e.getDate(),o=new Date(0);return o.setFullYear(t,r,i-1),o.setHours(0,0,0,0),o}function ci(e){u(1,arguments);var t=m(e),r=t.getMonth();return t.setFullYear(t.getFullYear(),r+1,0),t.setHours(23,59,59,999),t}var di,hi,gi;ai.elementDefinitions={...ni},t([r({attribute:!1,hasChanged:i})],ai.prototype,"options",void 0),t([r({attribute:!1,hasChanged:i})],ai.prototype,"value",void 0),t([r({attribute:!0})],ai.prototype,"label",void 0),t([r({attribute:!0})],ai.prototype,"placeholder",void 0),t([r({attribute:!0,type:Boolean})],ai.prototype,"multiple",void 0),t([r({attribute:!0,type:Boolean})],ai.prototype,"clearable",void 0),function(e){e.Favorite="favorite",e.NotFavorite="not-favorite"}(di||(di={})),function(e){e.Today="today",e.Yesterday="yesterday",e.PastWeek="past-week",e.PastMonth="past-month"}(hi||(hi={})),function(e){e.Clips="clips",e.Snapshots="snapshots",e.Recordings="recordings"}(gi||(gi={}));let ui=class extends(q(d)){constructor(){super(),this._defaults=null,this._refMediaType=o(),this._refCamera=o(),this._refWhen=o(),this._refWhat=o(),this._refWhere=o(),this._refFavorite=o(),this._refTags=o(),this._favoriteOptions=[{value:di.Favorite,label:f("media_filter.favorite")},{value:di.NotFavorite,label:f("media_filter.not_favorite")}],this._mediaTypeOptions=[{value:gi.Clips,label:f("media_filter.media_types.clips")},{value:gi.Snapshots,label:f("media_filter.media_types.snapshots")},{value:gi.Recordings,label:f("media_filter.media_types.recordings")}]}_stringToDateRange(e){const t=e.split(",");return{start:B(t[0],"yyyy-MM-dd",new Date),end:B(t[1],"yyyy-MM-dd",new Date)}}_dateRangeToString(e){return`${v(e.start)},${v(e.end)}`}_getWhen(){const e=this._refWhen.value?.value;if(!e||Array.isArray(e))return null;const t=new Date;switch(e){case hi.Today:return{start:g(Date.now()),end:h(Date.now())};case hi.Yesterday:return{start:li(),end:si()};case hi.PastWeek:return{start:g(W(t,{days:7})),end:h(t)};case hi.PastMonth:return{start:g(W(t,{months:1})),end:h(t)};default:return this._stringToDateRange(e)}}async _valueChangedHandler(e){const t=this.cameraManager?.getStore().getVisibleCameras();if(!(this.hass&&t&&this.cameraManager&&this.view))return;const r=e=>e&&Array.isArray(e)&&e.length&&!e.includes("")?new Set([...e]):null,i=r(this._refCamera.value?.value)??new Set(t.keys()),o=this._refMediaType.value?.value,n=this._getWhen(),a=this._refFavorite.value?.value?this._refFavorite.value.value===di.Favorite:null,s=this.cardWideConfig?.performance?.features.media_chunk_size;if(o===gi.Clips||o===gi.Snapshots){const e=r(this._refWhere.value?.value),t=r(this._refWhat.value?.value),l=r(this._refTags.value?.value),c=new b([{type:y.Event,cameraIDs:i,...l&&{tags:l},...t&&{what:t},...e&&{where:e},...null!==a&&{favorite:a},...n&&{start:n.start,end:n.end},...s&&{limit:s},...o===gi.Clips&&{hasClip:!0},...o===gi.Snapshots&&{hasSnapshot:!0}}]);(await w(this,this.hass,this.cameraManager,this.view,c,{...1===i.size&&{targetCameraID:[...i][0]},targetView:o===gi.Clips?"clips":"snapshots"}))?.dispatchChangeEvent(this)}else if(o===gi.Recordings){const e=new $([{type:y.Recording,cameraIDs:i,...s&&{limit:s},...n&&{start:n.start,end:n.end}}]);(await w(this,this.hass,this.cameraManager,this.view,e,{...1===i.size&&{targetCameraID:[...i][0]},targetView:"recordings"}))?.dispatchChangeEvent(this)}}willUpdate(e){if(e.has("cameraManager")){const e=this.cameraManager?.getStore().getVisibleCameras();e&&(this._cameraOptions=Array.from(e.keys()).map((e=>({value:e,label:this.hass?this.cameraManager?.getCameraMetadata(this.hass,e)?.title??"":""}))))}if(e.has("cameraManager")&&this.hass&&this.cameraManager&&(this._mediaMetadataController=new mi(this,this.hass,this.cameraManager)),this._whenOptions=[{value:hi.Today,label:f("media_filter.whens.today")},{value:hi.Yesterday,label:f("media_filter.whens.yesterday")},{value:hi.PastWeek,label:f("media_filter.whens.past_week")},{value:hi.PastMonth,label:f("media_filter.whens.past_month")},...this._mediaMetadataController?.whenOptions??[]],e.has("view")){const e=this._getDefaultsFromView();n(e,this._defaults)||(this._defaults=e)}}_getDefaultsFromView(){const e=this.view?.query?.getQueries(),t=this.cameraManager?.getStore().getVisibleCameras();if(!this.view||!e||!t)return null;let r,i,o,a,s,l;1!==j(e.map((e=>e.cameraIDs)),n).length||n(e[0].cameraIDs,t)||(i=[...e[0].cameraIDs]);if(1===j(e.map((e=>e.favorite)),n).length&&void 0!==e[0].favorite&&(s=e[0].favorite?di.Favorite:di.NotFavorite),x.areEventQueries(this.view.query)){const e=this.view.query.getQueries();if(!e)return null;const t=j(e.map((e=>e.hasClip)),n),i=j(e.map((e=>e.hasSnapshot)),n);1===t.length&&1===i.length&&(r=t[0]?gi.Clips:i[0]?gi.Snapshots:void 0);1===j(e.map((e=>e.what)),n).length&&e[0].what?.size&&(o=[...e[0].what]);1===j(e.map((e=>e.where)),n).length&&e[0].where?.size&&(a=[...e[0].where]);1===j(e.map((e=>e.tags)),n).length&&e[0].tags?.size&&(l=[...e[0].tags])}else x.areRecordingQueries(this.view.query)&&(r=gi.Recordings);return{...r&&{mediaType:r},...i&&{cameraIDs:i},...o&&{what:o},...a&&{where:a},...void 0!==s&&{favorite:s},...l&&{tags:l}}}render(){if(!this._mediaMetadataController)return;const e=!(!this.view?.query||!x.areEventQueries(this.view.query)),t=!(!this.view?.query||!x.areRecordingQueries(this.view.query)),r=this.cameraManager?.getAggregateCameraCapabilities(),i=e?!!r?.canFavoriteEvents:!!t&&!!r?.canFavoriteRecordings;return s` + + + + + + ${e&&this._mediaMetadataController.whatOptions.length?s` + `:""} + ${e&&this._mediaMetadataController.tagsOptions.length?s` + `:""} + ${e&&this._mediaMetadataController.whereOptions.length?s` + `:""} + ${i?s` + + + `:""}`}static get styles(){return c(":host {\n display: flex;\n flex-direction: column;\n overflow: auto;\n scrollbar-width: none;\n -ms-overflow-style: none;\n height: 100%;\n width: 300px;\n margin: 5px;\n}\n\n/* Hide scrollbar for Chrome, Safari and Opera */\n:host::-webkit-scrollbar {\n display: none;\n}\n\nfrigate-card-select {\n padding: 5px;\n}")}};ui.elementDefinitions={"frigate-card-select":ai},t([r({attribute:!1})],ui.prototype,"hass",void 0),t([r({attribute:!1})],ui.prototype,"cameraManager",void 0),t([r({attribute:!1})],ui.prototype,"view",void 0),t([r({attribute:!1})],ui.prototype,"cardWideConfig",void 0),ui=t([p("frigate-card-media-filter")],ui);class mi{constructor(e,t,r){this.tagsOptions=[],this.whenOptions=[],this.whatOptions=[],this.whereOptions=[],this._host=e,this._hass=t,this._cameraManager=r,e.addController(this)}_dateRangeToString(e){return`${v(e.start)},${v(e.end)}`}async hostConnected(){let e;try{e=await this._cameraManager.getMediaMetadata(this._hass)}catch(e){return void k(e)}if(e){if(e.what&&(this.whatOptions=[...e.what].sort().map((e=>({value:e,label:C(e)})))),e.where&&(this.whereOptions=[...e.where].sort().map((e=>({value:e,label:C(e)})))),e.tags&&(this.tagsOptions=[...e.tags].sort().map((e=>({value:e,label:C(e)})))),e.days){const t=new Set;[...e.days].forEach((e=>{t.add(e.substring(0,7))}));const r=[];t.forEach((e=>{r.push(B(e,"yyyy-MM",new Date))})),this.whenOptions=_(r,(e=>e.getTime()),"desc").map((e=>({label:T(e,"MMMM yyyy"),value:this._dateRangeToString({start:e,end:ci(e)})})))}this._host.requestUpdate()}}}const pi={closed:"mdi:filter-cog-outline",open:"mdi:filter-cog"};let fi=class extends d{render(){if(this.hass&&this.view&&this.view.isGalleryView()&&this.cameraManager&&this.cardWideConfig){if(!this.view.query){if(this.view.is("recordings"))S(this,this.hass,this.cameraManager,this.cardWideConfig,this.view);else{const e=this.view.is("snapshots")?"snapshots":this.view.is("clips")?"clips":null;E(this,this.hass,this.cameraManager,this.cardWideConfig,this.view,{...e&&{mediaType:e}})}return O({cardWideConfig:this.cardWideConfig})}return s` + + ${this.galleryConfig&&"none"!==this.galleryConfig.controls.filter.mode?s` + `:""} + + + + `}}static get styles(){return c(":host {\n width: 100%;\n height: 100%;\n display: block;\n}\n\nfrigate-card-surround-basic {\n max-height: var(--frigate-card-max-height);\n}")}};t([r({attribute:!1})],fi.prototype,"hass",void 0),t([r({attribute:!1})],fi.prototype,"view",void 0),t([r({attribute:!1})],fi.prototype,"galleryConfig",void 0),t([r({attribute:!1})],fi.prototype,"cameraManager",void 0),t([r({attribute:!1})],fi.prototype,"cardWideConfig",void 0),fi=t([p("frigate-card-gallery")],fi);let vi=class extends d{constructor(){super(),this._refLoaderBottom=o(),this._refSelected=o(),this._showLoaderBottom=!0,this._showLoaderTop=!1,this._boundWheelHandler=this._wheelHandler.bind(this),this._boundTouchStartHandler=this._touchStartHandler.bind(this),this._boundTouchEndHandler=this._touchEndHandler.bind(this),this._throttleExtendGalleryLater=z(this._extendGallery.bind(this),500,{leading:!0,trailing:!1}),this._touchScrollYPosition=null,this._resizeObserver=new ResizeObserver(this._resizeHandler.bind(this)),this._intersectionObserver=new IntersectionObserver(this._intersectionHandler.bind(this))}_touchStartHandler(e){1===e.touches.length?this._touchScrollYPosition=e.touches[0].screenY:this._touchScrollYPosition=null}async _touchEndHandler(e){!this.scrollTop&&1===e.changedTouches.length&&this._touchScrollYPosition&&e.changedTouches[0].screenY>this._touchScrollYPosition&&await this._extendLater(),this._touchScrollYPosition=null}async _wheelHandler(e){!this.scrollTop&&e.deltaY<0&&await this._extendLater()}async _extendLater(){const e=new Date;this._showLoaderTop=!0,await this._throttleExtendGalleryLater("later",!1);const t=(new Date).getTime()-e.getTime();t<500&&await M(.5-t/1e3),this._showLoaderTop=!1}connectedCallback(){super.connectedCallback(),this._resizeObserver.observe(this),this.addEventListener("wheel",this._boundWheelHandler,{passive:!0}),this.addEventListener("touchstart",this._boundTouchStartHandler,{passive:!0}),this.addEventListener("touchend",this._boundTouchEndHandler),this.requestUpdate()}disconnectedCallback(){this.removeEventListener("wheel",this._boundWheelHandler),this.removeEventListener("touchstart",this._boundTouchStartHandler),this.removeEventListener("touchend",this._boundTouchEndHandler),this._resizeObserver.disconnect(),this._intersectionObserver.disconnect(),super.disconnectedCallback()}_setColumnCount(){const e=this.galleryConfig?.controls.thumbnails.size??L.media_gallery.controls.thumbnails.size,t=this.galleryConfig?.controls.thumbnails.show_details?Math.max(1,Math.floor(this.clientWidth/D)):Math.max(1,Math.ceil(this.clientWidth/A),Math.ceil(this.clientWidth/e));this.style.setProperty("--frigate-card-gallery-columns",String(t))}_resizeHandler(){this._setColumnCount()}async _intersectionHandler(e){e.every((e=>!e.isIntersecting))||(this._showLoaderBottom=!1,await this._extendGallery("earlier"))}async _extendGallery(e,t=!0){if(!this.cameraManager||!this.hass||!this.view)return;const r=this.view?.query,i=r?.getQueries()??null,o=this.view.queryResults?.getResults();if(!r||!i||!o)return;let n;try{n=await this.cameraManager.extendMediaQueries(this.hass,i,o,e,{useCache:t})}catch(e){return void k(e)}if(n){const e=x.areEventQueries(r)?new b(n.queries):x.areRecordingQueries(r)?new $(n.queries):null;e&&this.view?.evolve({query:e,queryResults:new I(n.results).selectResultIfFound((e=>e===this.view?.queryResults?.getSelectedResult()))}).dispatchChangeEvent(this)}}willUpdate(e){if(e.has("galleryConfig")&&(this.galleryConfig?.controls.thumbnails.show_details?this.setAttribute("details",""):this.removeAttribute("details"),this._setColumnCount(),this.galleryConfig?.controls.thumbnails.size&&this.style.setProperty("--frigate-card-thumbnail-size",`${this.galleryConfig.controls.thumbnails.size}px`)),e.has("view")){this._showLoaderBottom=!0;e.get("view")?.queryResults?.getResults()!==this.view?.queryResults?.getResults()&&(this._media=[...this.view?.queryResults?.getResults()??[]].reverse())}}render(){if(!(this._media&&this.hass&&this.view&&this.view.isGalleryView()))return s``;if(0===(this.view?.queryResults?.getResultsCount()??0))return R({type:"info",message:f("common.no_media"),icon:"mdi:multimedia"});const e=this.view?.queryResults?.getSelectedResult();return s`
+ ${this._showLoaderTop?s`${O({cardWideConfig:this.cardWideConfig,classes:{top:!0},size:"small"})}`:""} + ${this._media.map(((t,r)=>s`{this.view&&this._media&&this.view.evolve({view:"media",queryResults:this.view.queryResults?.clone().selectResult(this._media.length-r-1)}).dispatchChangeEvent(this),H(e)}} + > + `))} + ${this._showLoaderBottom?s`${O({cardWideConfig:this.cardWideConfig,componentRef:this._refLoaderBottom})}`:""} +
`}updated(e){this._refLoaderBottom.value&&(this._intersectionObserver.disconnect(),this._intersectionObserver.observe(this._refLoaderBottom.value)),this.updateComplete.then((()=>{e.has("view")&&!e.get("view")&&this._refSelected.value&&this._refSelected.value.scrollIntoView()}))}static get styles(){return c(":host {\n width: 100%;\n height: 100%;\n display: block;\n overflow: auto;\n -ms-overflow-style: none;\n scrollbar-width: none;\n --frigate-card-gallery-gap: 3px;\n --frigate-card-gallery-columns: 4;\n}\n\n.grid {\n display: grid;\n grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr));\n grid-auto-rows: min-content;\n gap: var(--frigate-card-gallery-gap);\n}\n\n:host::-webkit-scrollbar {\n display: none;\n}\n\nfrigate-card-thumbnail {\n height: 100%;\n max-height: var(--frigate-card-thumbnail-size);\n}\n\nfrigate-card-thumbnail:not([details]) {\n width: 100%;\n}\n\nfrigate-card-thumbnail.selected {\n border: 4px solid var(--accent-color);\n border-radius: calc(var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px)) + 4px);\n}\n\nfrigate-card-progress-indicator.top {\n grid-column: 1/-1;\n}")}};t([r({attribute:!1})],vi.prototype,"hass",void 0),t([r({attribute:!1})],vi.prototype,"view",void 0),t([r({attribute:!1})],vi.prototype,"galleryConfig",void 0),t([r({attribute:!1})],vi.prototype,"cameraManager",void 0),t([r({attribute:!1})],vi.prototype,"cardWideConfig",void 0),t([N()],vi.prototype,"_showLoaderBottom",void 0),t([N()],vi.prototype,"_showLoaderTop",void 0),vi=t([p("frigate-card-gallery-core")],vi);export{fi as FrigateCardGallery,vi as FrigateCardGalleryCore}; diff --git a/www/frigate-card/ha-hls-player-aef987da.js b/www/frigate-card/ha-hls-player-aef987da.js new file mode 100644 index 00000000..748c12a0 --- /dev/null +++ b/www/frigate-card/ha-hls-player-aef987da.js @@ -0,0 +1,33 @@ +import{di as e,cL as t,y as i,dj as s,dk as o,dl as r,dm as a,bj as n,dn as d,bk as l,bn as h}from"./card-555679fd.js";import{m as u}from"./audio-557099cb.js";import{h as c,s as v,M as y}from"./lazyload-c2d6254a.js"; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const p=({finisher:e,descriptor:t})=>(i,s)=>{var o;if(void 0===s){const s=null!==(o=i.originalKey)&&void 0!==o?o:i.key,r=null!=t?{kind:"method",placement:"prototype",key:s,descriptor:t(i.key)}:{...i,key:s};return null!=e&&(r.finisher=function(t){e(t,s)}),r}{const o=i.constructor;void 0!==t&&Object.defineProperty(i,s,t(s)),null==e||e(o,s)}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;function m(e,t){return p({descriptor:i=>{const s={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;s.get=function(){var i,s;return void 0===this[t]&&(this[t]=null!==(s=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(e))&&void 0!==s?s:null),this[t]}}return s}})}var _="img, video {\n object-fit: var(--frigate-card-media-layout-fit, contain);\n object-position: var(--frigate-card-media-layout-position-x, 50%) var(--frigate-card-media-layout-position-y, 50%);\n}";customElements.whenDefined("ha-hls-player").then((()=>{let p=class extends(customElements.get("ha-hls-player")){async play(){return this._video?.play()}async pause(){this._video?.pause()}async mute(){this._video&&(this._video.muted=!0)}async unmute(){this._video&&(this._video.muted=!1)}isMuted(){return this._video?.muted??!0}async seek(e){this._video&&(c(this._video),this._video.currentTime=e)}async setControls(e){this._video&&v(this._video,e??this.controls)}isPaused(){return this._video?.paused??!0}async getScreenshotURL(){return this._video?e(this._video):null}render(){if(this._error){if(this._errorIsFatal)return t(this,this._error);console.error(this._error)}return i` + + `}static get styles(){return[super.styles,n(_),d` + :host { + width: 100%; + height: 100%; + } + video { + width: 100%; + height: 100%; + } + `]}};l([m("#video")],p.prototype,"_video",void 0),p=l([h("frigate-card-ha-hls-player")],p)}));export{_ as c,m as i}; diff --git a/www/frigate-card/image-0b99ab11.js b/www/frigate-card/image-0b99ab11.js new file mode 100644 index 00000000..17614ba8 --- /dev/null +++ b/www/frigate-card/image-0b99ab11.js @@ -0,0 +1,11 @@ +import{cH as A,cI as e,dp as t,dq as i,cW as a,cJ as r,cK as o,cX as s,s as h,cP as c,dr as l,dl as g,dm as E,y as n,cS as C,ds as Q,c0 as B,cN as u,cL as m,l as w,bj as d,bk as I,bl as f,cO as p,bn as b}from"./card-555679fd.js";import{u as U}from"./media-layout-8e0c974f.js"; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const k=A(class extends e{constructor(A){if(super(A),A.type!==t.PROPERTY&&A.type!==t.ATTRIBUTE&&A.type!==t.BOOLEAN_ATTRIBUTE)throw Error("The `live` directive is not allowed on child or event bindings");if(!i(A))throw Error("`live` bindings can only contain a single expression")}render(A){return A}update(A,[e]){if(e===a||e===r)return e;const i=A.element,s=A.name;if(A.type===t.PROPERTY){if(e===i[s])return a}else if(A.type===t.BOOLEAN_ATTRIBUTE){if(!!e===i.hasAttribute(s))return a}else if(A.type===t.ATTRIBUTE&&i.getAttribute(s)===e+"")return a;return o(A),e}});class H{constructor(A,e,t,i,a){this._timer=new s,this._timerSeconds=e,this._callback=t,this._timerStartCallback=i,this._timerStopCallback=a,(this._host=A).addController(this)}removeController(){this.stopTimer(),this._host.removeController(this)}get value(){return this._value}updateValue(){this._value=this._callback()}clearValue(){this._value=void 0}stopTimer(){this._timer.isRunning()&&(this._timer.stop(),this._timerStopCallback?.())}startTimer(){this.stopTimer(),this._timerSeconds>0&&(this._timerStartCallback?.(),this._timer.startRepeated(this._timerSeconds,(()=>{this.updateValue(),this._host.requestUpdate()})))}hasTimer(){return this._timer.isRunning()}hostConnected(){this.updateValue(),this.startTimer(),this._host.requestUpdate()}hostDisconnected(){this.clearValue(),this.stopTimer()}}var L="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wgARCAEVAewDAREAAhEBAxEB/8QAGwAAAwEAAwEAAAAAAAAAAAAAAAECAwQFBgf/xAAaAQEBAQEBAQEAAAAAAAAAAAAAAQIDBAUG/9oADAMBAAIQAxAAAAHu/wBF+fKuKWoua2zblcOpTDeJqLJsVgAACCiNQEKJRCmEoMAhy1FzThq4VrCHDWbHCoilBWQKqzaa2xeTz3Uumd+Z+h4Sxy0VGmdbzV5AybMdZjUz1kEFA4AAEAAdEFAAEVKBDVxc1UtSgiaY1cSI4nPfl/N6eZrHp/Rw11mLJq83XO9c2ovO+Ry6ea+j4FY1uKl1zrSAAIucd5mlYUoBggA1ACiRiVgiCiW81w6JQqWpazQVTQCo6fz9vH+L2ec83o937PJ7P6HgsBAaZ1vjemdJNcb5HLr536PgACrioqFU2RZFkWLcAgAAokdoEhRKwgoHCCgrKpWrlQ1ctQpVU0jovN38P4fZ1HDta+y9HD3f1Pm2jEBrm78+muNXNVnW2N6Z15z6fz7zWSlDEkVnrMWKyaVgOCkA6AkBipwKBDhgIa1AC3KoaiuJtmPP+b0eM8Ps6Pl01lprsenL6V9T5vadMUJEa41y+XXXGqmrzrTOrzamvNfU+brnThgRrOdk2Z6yqSKxBTAQAgojBBRQAHDhhDViRLc1cpKhV5rzd/nnz/dw+PbkIqheXrH036nz+578LipVY42xvl8u14uk1pjeuNXNB5f6vzNc6uERc56znokjUEBBYAFACGAIAAAAKDyoqaEJRWVNOOn5dfnXz/b5rz9he1zutRGtfSvo/O731+aoYwjTOuRz6b893nW2N6Z3ebpnTXyf1/laZ0xM5azGpFioQAVgCiAAKmgAAoCAhgpDHLctZoNUvh/H6vnfzvfjHJl56taNNT6L9L5voPT59s1UAVLti8nl2rN1zrbn00zq860zoPJ/Y+SDM7nPUmxWIBiQoAKABAAAAAFEQ1BIBFTV5uktTXnOXX4/8n6euNbnYzWVjXkdMfR/qfL7X0cNM63xqpagKl5PLrrjdy643rjeuN641cpL4n73xUFmdzNCAACFAAFAAgAAogACCiQCiUlqWoqa0zfkXzvo+Z8fq7XnvmVx9Zmu27cvon1Pmcrvwcu+N6S689a51pnWmdb8+m/PppjeuNbZ3ea5QZ4H9D8JWISKhAKIApgAAgAK0SCiiACCwCEooOWocvTcuvx74v1+RjfZmk1jrPp/Z4/cfU+a9QHHIxupeRz3tjXJ5dXLvz6crl11570zqpXRDVnz39F8BwqlDUSCgwABgMIVORqkLEoAAJAFQAqsakOX5T8j6nQ+P19xNTc8nHXk9uP1L7fxI3hUK5d8a2xrk8+muNcnj25HPptjfI59NM6AHDVqHz79D+fCbFokAoHAAwAcEFEMWghKgALEgoAhaCiPLznm9Py3431+djXay8TU7vty919L5/YejzTYhDl3xrl8+nJ5dd+XTkc+nJ5ddcavOnKqCotoA+efovz9RGorEFMIYQUDQlYBAAtQAIKIVgoqRiqbAcuWNfH/AIv2ev4du3zrSqj6R9L5/fezxRYqRUC65vL5dOTx7crl15PLrtjd5tTRBTilagHzr9H+eZNgAwpwAADgGAQUAKwUQAQArjLG+s83p7P3+BhnXy/5H1vN+T1b5vPa1s9r34+++h87PWc9RwE2TV5cjn05vDvz/P6N+e9MbuWlBqAAQHzn9N+ecKwgGFjlBhBRDAIACkMSClEi0UOXTNpfN+L3ec+D+k7z6Xx+r8/o8FvlWddkbHP6Y+vfT+VyZqUmzTOtcbZnrNTXK49eVy67c9643rnVNAAAAAR84/TfnmgADgGADgAKcFEIBolKAEiRWhWbyMbivi3wf0EcuvKmuH38mWNdrbon036fy+768dsbcumd6Y1rjfL5dalBy0ry1zvXG6UAAAAA+cfpPzrAdAAMIcEFAACFoAISlCJSxQrUjl1zry3l9Pyz5P1MY1O72wxruK9F6vL7j6PzNc6359N8dORy6a43rje+NuAnRDjTGts7uaAAAAAPnH6T886IaFAQwGOEAAFAgAAAAAQ5Vm+D8Pt8J873xLKJeyrntdpL9I+n8vse/m1zvTOtM65vn9G/PpvjdQE1lrKRrUuuNbY3pNgAAAB85/S/niAdhBRDGAQUIKIgBSkEACpwEYvmPH7fCeH2dVx2mmVLFNnvLruq+o/R+ZyOvCprTO2c3z+jmce2uNMipsz1nPeHNa41rne3PWudtQAAAPnH6T86DAB0Q6IcFFJCBSwhKCBBQ43PfQ+fv5zyerzfl9XGzYRy6LCCiKucdxqfZPs/Fcu+OnJ5dUmmd87z+jkc9hOk2QznuXm6Z1pnWuN6501AEJGofOf0v51wAADGAQWCiClAgEEB13Lr82+T9TpOHaEUOXlzVmlaDlzlmzg3N12J9p+x8blaztjfJ5dtcbqORz68nl1cKgmxI2rmqy1xrSbYEiZais+c/pfzzQgAdOAAAAAKQQgpRwuXX5N8X6/X8+hHLObNaLnZRUFVL3HLp6P0eXzPSddJ0fPp9G4693+g+Lvm8nl15fHtU1rjV5rWpqoBDVxebpndytUJEjVqz5x+k/PMEFKcjAIKEFAAQhBWeNfJ/ifY6Xh25kvPWrMiluXVexzPQ/Q8HoPf4OT24llZvH8vp+X/ABPt9fqfSvT5fX/R8PL49eXx7OUVoS1LUuudENai860ztqwAEFAPnf6P88QtFRIU4YQUAOFaCRUiZfmHxvreb8vp5Evay1YWcatJe+9Pn9f7/ndp6fPGiSpRKl359PM/N9/kvN6uQfSvX5O81ORy63mxqTrNZ1rnTl0zqoqW86qaqVq4KAAAPnX6P8+IrFTHKIwhUwHCpWkIVnhPmfQ8N876HNl3rQYrnkdM/QfpfM7f1+VWCkXLedXCNcbvN+a/K+p6j0+fpPL6vqnq82+NXnUazNjNcbvOrlcC1LedXm0rUCAAoPm/6X88SA6ZWaBQhDBUipFZpb53y+r538f6txnWpVmusfTfqfL7D0cTWVQhDmtZrTN0zrTLfn00zrjY38u8fs+gduPF59PadMXmgE2VLctTSRq5by1zu86pQMgKAPm36b86wh2seQjUokqUEToi83qePb5b8b6153QrGdv25fRvq/K5PTDSLA0zqpXDW5dcXk8+nI5ddM6cvTZ347n07BPb9McrNqVjlcquW05QqXXFvOtJpqBAEAHzf9P+dACCql0zXEWMagkjSs3jc+nyf4/1sOfTSWyLO87+f6N9X5fJ1EBNjipdJalqWjbnvlce2/PprndZoeXzvgmlnsrnTN0zsVWRc3NXmuXTOtM6pQAAAAI+bfpfzgNUMoZWarCWgWbIoxr5b8f6/V8O2hcvYaz636Hz/Sezyck0zpWJLlZFgJbl0zeTy6czj25HLrU040zpnQY6d9rFDilCUVaY1rnVzTUAAAAAAPmv6T86wBGrCiAocriNJTwHyfq+U8Xr2NK9j6fN7j6Pz63ijk8+jgBBdsaqWUw6Yqa2xrXG+Xx7b8+ly1nVy6Z1WaCqNZVjzWVLpndKAAAAAAAB81/SfnC1hIDtBhDHKQjy/k9fgPk/V7beO69HD1Hp8/e+jz6ZrSbNM60zdM6i5z3nbn05fHtpnXH6c1W2N6Y1tjemdXnWmbedaZ2xIiNZjWQrOtc6poAAAAAAAAPmf6X845XRIwV2uAqLzoTi8uvnvH7O16c+768uRrNDluHGeo0vOtsbjWYueRz68nl10zus1rebct51UrlqNM7uVqIEmW+cay5rTGrmqUAAAAAAAD5l+l/OMYQwKtcripdc6uJ8nr7a5DXG9M6x3zy3loLFyJlvJLUa53yeXXbHTfnu86ZedVm6TQCOW5qpWoAJnc5ayy5qs22gAAAAAAAPmX6b825QcOiKtakmmdVLpm9t5PTyOfSprfnu86z1ni9uMazGs5bzFzjvIuubyuPXkcuu+OmmNXLeNbY1U01STqVLebTQAAJJQGtAoEFAAAAAAf/EACkQAAEEAgIBBAIDAQEBAAAAAAEAAgMEBREQIBITIUBQFCIGMDFBFSP/2gAIAQEAAQUC6hDofrwE1D+gIdDwfpNqxZggbNnAXQQ5Kwom+DOQgEBwEFrjXGkOpKJ+kuZKrWVvMWJUXeTsDVsMQ6gIDjSa1ALS8VpaWlrqfo7uUrVlcytqwhG8rwaFim0GzjqAgFpaQCA50tLS19Pcy9SBXMpYsrW0PZHax1J1ySjViqxjo0JoQHAC0tcBD6MdcjloKqu5GzaMMZI1pa4q1ZrMmMpMpxBDoxDkIchDk/R38hXptv5izYTimAl2zx4uKirSyyYim6pBpa6BNHAQCCHIQ5P0GuM1mPTT3lxUUJcgGtQCLXKNs7zhq81eu3s0IcBDgdifov5Db/GpOO1FE6VRQ/s4+y0CoK80rsZXdWqhBDjS0g1NatIBBqAQCAWlrja388chfyC3+TeVD/5Sy68/bijSltGnB+NXQQ4bwAgE0IBBqAQC19PtZu3+LRJ2q0fm4f7M0sQCxWMc6VrWsbwE3hqCAWkwIBAIfVZy3+TbYNuDWta9VnhyoVX2LX/OjUE1BNQag1AfV565+PVcdqkP3sBoR9xg6vqZCrTirSdAmhAJoTQmhBD6ueVkMN+y6zZHuWjxa8pgWDpehWIWuNLS0moJqaEAh119FJ5+Fa4JJOJHMijy+RdbkKrj9h/p91hcc90WkQtLS8V48NCaFGE0ID6cLSzNYPhNyz6dHMQyr+T+qXJo2QxrQG6VCo+1aiaGMCKAQCDV6a9JNjTWINQQ+nBQKc0OEkz6tya5t8V10jJmaMTSSxvtolYmr+LWCHDWrxTAgF4rxWuQh9O1ZvKNga874YdKPUsNc6k0Y34Wn52EE1BNCAQCHYIfTPLWMymb2nHyK1xVk0+WLb2bLMZCIqAagEE1NTUOu1tAoFD6OSRkTbmcgjV29YtO6FN9jE/yZjx52arDFXQWk1MTUOh5CCCHz5bEESsZupGrGctPUs75XE8BBjii0hNBJWlDoNx0xFkoIJqITE0odCtIBBBBD5t+3FUiu5OxYJcSt8AEoV3kCshGGoex9t+jomPyJBaWHRpvNexDI2aJqCaeAE1DrrkIfNu2Y6sF61Jan4ZXcQxjGD9db415BjduLC12Mr2JF/48L68uJsvnyGKsVZfJYa/tjSgmhNHA433CHzJZGRR5O263YUcBeoowxErSA9gAS6N8clenudmHb6sUEER2toFWofWiyNdvlVc6E4e2HhqYh12h1HzCQ1uYyBtSbUTNljkTobBBPu1U8XPMq9CrAieoQWaq+TcYIzas1pcbYxdgWa7Qh0HA/v38L+Q3/f3Ka1qPuWeyemD9Y60srocZTiPTSAXivFNavFZqia0uMljymOpSSYvJNIIHQcDqPl5e+K0etqb3LRzHDPIG1YparWhjOw4DUGoNVqsyxAPWxOTuwR36n8es7a3+sIIfIyVxlOB73zSjpjqL7hqwsrwddLS0gEAmBAIBaWVoMu18FZfTtZmF9O1XlbNDzrkchDgf3BHuFbnjrQXLEluf/gQTP2diKTprUUbI2865HRqaEBzpZrGC2zFWhbhwfq1rKCA6BaWkAtfB33c5rGZW665OOB/lWnNPHg6H47WMa0NatLS0vFeK1yEE1NQ5AWlexcdicMG9LXTSAWlr5Wcv+q5vuf8AuiXQYSQsjYGN0mhALS0tcNC8EYkWJrUGprUwIcaQWlpa7AIfLzdwwsmjeySWg+vRw2JLlSxsFZ4WlrgIcHhiHDmoNQagEAgEOB1PIQ+XcmbWrYyjM5UMXHA51dj3+C8V4rXGuBweGBDjS8UAtIDkIdSjyEPjjhn+zUhYm9NeCaxemnM77W0AmoIdNf1noPjjiP8A2M+yCHDgiODw7lqagghwAgOw+R//xAAsEQABBAEEAQQCAQQDAAAAAAABAAIDERAEEiAhMRMwQFAiQQUUQlFhIzIz/9oACAEDAQE/AfaPwB7Y+KPZHC0flj3GsLvCbpT/AHFF8bP+oRO43yGBms19WyB7kzSsHlCgtTM0jbypDLQgMUq+rZp3O89JkLWeFRVKXdR7rkEFSpUgM19VHpnO89JkLWeM0VI8Mbaklc/zxAQCrACr3L+gjgc/tRxtZ4QGaT3BnZU05k6/XIIZCH0tYGWROf4UcLWYGaTjtFlaicSdDm0YCpDA+mrEEO7s5rAVFF+3yVqJmyHpDgMAIYCGAPqImb3VgC0ETik+RrPJWolEh6QwOACa1UgEAgEAqVKs39EFpWU2/wDOGdFHMkzY/Kmm9U3zAQCAVIBAKlXC/pImb3bUEO1aIpV/lTztaKae0ST55BAIBUmhAIBV9VpY9rbP7yVGQfxKmf6Te047jfMBAJqpBqA+r00W92Go4lJjbam1DpBR5BAIBAIBDA+qaCTQUUQjbWbVLUybvx5hBNTQgPqGP2m1JpdzfUi7GAgLWn0+zs+cDMuo2t2DzwpUqxSATQgEFX09L+P1f9PJ+XgrV6COvUYEP42GZ/4Gv9KT+Pbpm7m8XvDBaJs2UEcAINXpr00I01iDcD6cZ0WtliYKUkrd++Ppf1Bcwschwnl3u4Bq2poQC2raqyEPpwoIPU7Pji3Gpk2NrAQQQCAQHMIfTBqi0v7egONILU9yFUqQQQQ52rVoH6NoLugo9I7+5MibH45hF+3tTyB77GaQTUPaH0DY3u8BN0bz56TNHGPPaaA3ocKVcSL6wEEFSaU08qwMD50URkNBRQNj8cdq2qs0qzSlidG6iggmnACbxpVwHzYozIaCYwMFDO3F8aUUrIzUnhanWxRvqMWjqYnC7UUgkvb+sSxCdlftVRo4AQCpDF+wPmMaXGgoYhGKxWL4EV5RLWi7Tte2ugn6iR/kqycBQSmJ+4I04bgmmlrIb/5AgE1DN5HEfMAtaeD0x/vgeEuqYwf5T9XI798KVYC0k1fgVKCOx5UMweFLF6bqQCHAfR6WGvyPEoJzwAn6p540qVKkAgoJN4o+VKwxutq/9WIIYpUhgcLQPy9LBvNnwq4lwCM7g40UST2eIQzSDVtTbabCNSNUTzE5TMo7ghwrA4hBD5EMRkKaABQ4yTNj8qSQvNn2qTRgBUmHaVIyxuCjdY2leOFKsDgAq+OExheaCYwMFDjPN6YTnuebPCuYQQGaTHbU9ldhOF94CrgMUgFXwb5tF9BQxemOBeB0VqZ+6aiS42UAqzSrgFSamhAZAVJppUq5AKlXytLBtG48HaloBTnF3lUqzWQFsXpotQC2oNQGKzSpVypAfL0sO47iimyBxoKXUbemqXUl/XIYOGoYIQag1AKlWQhwrgPlxx7zSfIB+A8KScu6b4Qc4CgqVKlXAK8tCGKVKlSriOJ+WMBR/iOltW1Bi2Itr2QE3AwM17RVKvkjDUzAQwQqwcHg1BDgAq9yvg//xAAsEQABBAEDBAICAgEFAAAAAAABAAIDERAEEiATITFQMEAiQTJhIxQVM0JR/9oACAECAQE/Afmr1h9q6RrPKdq/00JrJH/yKa0NFDiVeDwv4q9G/UMYn6qR3hdytLp3A7ieRKOS5Xi1fq5NSxvhSTverxCRY7WeRKJV4tHjfEZH3z2Umqa3wpJ3yec2omdR21RxNjHbiSiVeCVav1cuoaxSTOf5V8GMc801QacR9/3yKPrpJms8qTUOfxtNaXGgtNp+l+R88iUTgq/hHotRPt/EInjaazd4C00Do7JR5HJRzav00r9jC5WiawMWmRuf4WnhMY78bVolFytErcrVq1avNeiK1Ulu2/8AmHiwhmKB0nhQQiIVxOLVolWiVav4qVfelk6bd2CqQN40+nc424dkAB45FFEq04olWr9Vq5N7qH6yE8V+QUTOqaCY3aK5FFEpy3IuRPq9TL02/wB4Kbf7xC0SOpRQNjNjkUSiUSiUfkHoHODRZU0vUdeAqxpo9o3IHkUU5Eon46+5akZvb2UWrp3Tl7HJdS1M/U7Dxxi0+5284BVq1avBKJTiiUT6i1rtJ12WPIUGrk/gSv8AcJYmfl3TNc7UdjxYwyGggK7YCtWi5b11EZEXon1NZ1WnYZCms/Ha/uhEA4EJ2AqUMWxvAuVpxVrcr4H1BU8/T8eUTwKC08e43koolEon1xKm1f6Yib4DOmoMV5KKPCsUqVIhH6dfQc4N8qTWNH8VJM5/lXzDd3ZQsLGUVWSnchwKPoHSsb5KdrWDwn6158IuLvPxAoDBRQKciOVq8n70szYhZUuofJxtXmlavIKhlbK2wjgjFoo+mlkEbbKkkMhs5vFcbUkTpB+HlaXSyyMuTsv9PI00pWdKt37xDIdLLf8A1KsEWMOKJ+M/dc4NFlTSmR14vkDY7IbnnaGlN0Dv2UzTMZ4C2qlS1MDZmFpTCR+LvITm7gtDMW/4nIpyOaxXoiaWon6h/rgAqzFpXv8A6TNJG39IDhaONZp7/wAjfIUTgVNCWd1DN1GIlHgVfotZPf4DiMNYXJmkYONq1atEolTRdN1jwo3CRu0r/hfg4CtHnX2gtVqNg2jzya0lDTtLRaDQOwzfA4tbluTqcKKFxuUrBKxQusbSj6WaURNRcXGzxiidL4UcYY2hzvJKcVatWnjcFG7aaKe2nbgvPC/QPeGCypJDI6zx08XUd3TWBgocL5lEonNp4tMffYpprt8Fq1f0aVc3Gu5U8vUPANJFhaaDtbk1oaKCvF4tblfAopyvNq0RatWrzatEq/sjGr1G47RwZpX2E1oaKCtE4tWrwSt6EiDkXLci5OObVq1avA4X9zVT7BtHnDoy0WVDpy/u5Q6YR9z3Ob4jDkcAouRcrV/CD9+SQMbaZETbz5UUAb3d3KLWk2VuW5WrxfAYccWrW5WrV/EPulSfke63rei9b0Hc6wSjg5tX8YVq/tuT8FHDSgcDAyUUUckq8j7f/8QANxAAAQMBBQYEAwcFAQAAAAAAAQACAxEQEiExUQQTIjJBUCAjUmFxgaEwM0JikcHRBRRAYOFy/9oACAEBAAY/Av8Ab6zSNZ8Vc2SF0rtSr207RuG+iPNBoJNNTXvNHPvO9LVSOkDfqqm9I7Ulb2QNjachdxPeaV3j9Gq7euM0asli5MZGH7RL6rtGt7xRrt6/Rqu1uM0bZgsUQ0igzJVyMfE692LG+bJoMgvMfw+kZIuPguxtJVBi88x7rxuq/wBAzRAO7Z6W2YW4BXGNLj7Ihxq52Y07qdn2V3F+J6JJqbKnAKjRZkrsd4nQIia7U9Kd1ow0fJgLKMxOiN/CnS3NUiaXfBXH3b1endnXTwM4W2NkcMETbWMcIzJQiDr3dnUPG/BtlTkLcRgVvNoj8rQ9VdY0NGg7uSORuDVRUbYYZOU5exW4HKMSfZU7vum/eSfQWXim3MqY2M3gq0NvJ7468Xd3SyHhanSu62AIJt4G6TRCVxq+Rv6DuZ3brruhojBKN3M3NuvwtL5DdaOqutwiGQsrpbG+cNMRN673UzjB8Yr8kDxPLciM/wDqEZY/eIUfWMdLAFQWCMYDq5NY3JooO60OSk3D+AONEJIQY304qap7ZiX3lhlZihQVOSunmdi7ux2eE1lOZ9Pgu9eixWozRnrwRnD37qXvcGtHUoxbJgPWqnw32mlV+ZqiAFCRePc70j2tHuUWwN3h16KsrzTTp4/ZMiOF40qmRuIJaKdw8yVjfiV5YdIf0C8u7GPbNVke5x9zbgsrKWkE5j9FE8dHdvvyHHo3Vc11vpCxNuS6LFypZQ5K+OVUVFUYJklA6hyTZG5Htpkk+Q1Rlk+Q0swVTgsqqlLKqqp1V0ggoncl8XVB7b8cujslcMVPforoG8bdvVFjb3wcqjLtZkeaNCLzyjlFlcgsvn4ANVdkBb8QmhkjHh2gKBkc271DaqscQHgoOcYtRNKFVxwOaEROB5f47UXE0AV1hpE3L3sqVSytnWhR3g3TNXDFNoy84dXfYGdo/wDf8p2ybR93Nh8D0KDXGrDyuCvfjGDu0/2kRwHOf2sGGPgocNE3ynmvUBAhhcR6j9neZyHlP7Iwz4vbg7+VupuXI+7dVUZdn3bD5rvosVgKKtt9kbnNrSoChZtELS5jQgxoo0ZD7R0UgwP0V4jLP8wTZYqF1KsP7J2yP5o+WunZrxxeeUIySGpPhdQ3WjqUImZD6/b3cpBylHYNoq1pOFfwlM/qMHq4wmysPC4VHZDLIcB9UZH/ACGngwQMjDuhmrkbQ1un+DvI8J25fmT9g2webSmPX/qm/p0lSG8Ubuxl7jRozKwwjHKPBI9jcGDFGaVvG7IHorrGho0H+GzaGvMUrfxAKtMexnZ4j5beY6myiutFSozI+7XnGio0U6943EX3r/oFuSDeGaEsho954WdaaoTbU3h6N1V8cT9T07y6Z3T6p+3TU378Yw7ot7J5sxxLig9zAXNy73GZMY2Y3dT/ALh//8QAKhABAAICAQMCBgIDAQAAAAAAAQARECExQVFhIHEwQFCBkbGh8MHR4fH/2gAIAQEAAT8hhBgwyGCcYxl/LkIes9JlIkPQFrBxgwQxGLl4PywlSoQhCVhyPoUqnlt+0v1GjQfjlle9ehX5dIJpdTp+7hlQemI1GKhAQgQl4vIX8oQxUrJlzcuW1H8jLNvB3+cLdDmLsspP5nYYmQzVTli0emiCKwxYvliEPSYvCy4YnzLj3Zci14vyzd2PLHizFp5391bggZM2QZMEIZBFYWOH0vx6lQMkMVlZdCug5Yg/iR+Zd7bxfmdyOtVv23L+b92CH7ekPacvTz85HF8HBWIgipywqVGLFjFl/JmD1glRxxxep+RjO06NYmhA4mrRGK7XLzzq9D3ilv7Ug9AMOMIYCVK9BcUWLF+XIZqVCDh545f5dps1+pz7vWKtwkWgCbX3lzrCBlXpFociHEEkVWAhxkOAYBKgyWKLH5kYMMVhUuahoOng/wBxKycq8xblDb305gmhLtavywUCbl3GJvbsAs93rhWSBiGoZwggSsDLwLF+cIMGER7quOh1f73jKNIHkbYPUnuBQBQaAn2nSG+zLDLw1CW5s048X1yqBfw+vKmBci/QBhRLP+4zrHvVuzxEhg28957Iu+bIuCJ2ZCRKK3VZHUMCWyj1DKRWbjh+eJcIsFX/AGmMoHlCfhigapLHvHEdXqF1gLNN4XIeAojlwhDDNU5eggegJX0WwN/f/Mr+FzoN5i0NQ/b2vrK6B1dx4SqFlorcckUMGoYfQUGLwel+OfJEy6a/YzrJXS0R1arR2ZfbApLjOicfublHf0O2KlSvWoUwQl5IZJcX58lCAL9/ETXlo7HaDggkcnMVeype8hTNUSdPNWRUMGmLm+aECBKlQgPQfOVAg03UH3IFnSs15RzAYI0Nqm07fe8su7snLsKSaFXFSEKduqIaayC+LCYGiZgIECVK+gk2jPXsh3EDxj2Svnp+3mcaZ1KP4mySeLxZ/wCxIbjVtXHSXfLLVvEurDdGgg8UT2CcIZZKZdg+EXGRBiQPoxcRAAopGGmpi7KuEvrwnwja4FK9bgXcmyaiS8vVAJazQ6wj2dK48fbBSr9ABqUlIQQ+lU7YKP8ArcuXi9LHo7jg2uEnA9APiE7Ude7n+IOQzRlx6CEUUH6HUCcuIEoJWv8AA9b7doqIq4YNMo91A5OXUp0Sv8kEoH5VyGkW5xgghGLF9ITh9AvCTuHVL0u/1FqLoOh9pd4Emq1BHCoF7L7Mu5YfvTd2AOJeHCaMeRixwuEH0Jfid2whPBoWIPuv5Med4LpbLvmWWoMW+odaaeJWERumKnfRHXRLmFLhi3iYLJTFTmMfSA9avB8vtcXFyo3t9PSR60fvG2NaJl9/aM6ofaeYd5fR/wCwLDtwr2dF1C6a+GXF0i+wIGqIvf4l4dN08nhwWILwGo9RcVKwCGQ+Meo+AuXjqLtEo/QO0Xe5VtRx49nmDqs6qS7SR50QcbdOScPz1lBVVoo5Y4G++o5dpq8L48w6rWdZf5luthrrUXzFztAXT2igO5N6LQB3Dr7n6hIIKLE6zfFolYjgMH0GQ9R8qAstqy/A1dsjuDbaNMf700o3EUzls9sjcrV13drzHdAB0/d0QCeHuH5iRGlXy/zChRqXiRr0Nq9+z4eIu7Zfceo+YNNCD9cZvX/I+8ME4xIS4ODhgIEEPTXoD0nxTBCtXpGLOF/ZluI/AhCbbq9R93R1KAHMp8pd6cLRe5UHDf6Ihibra9+3ESLjmVFHqDpr+H96f+QQa3XqfB+vuxau/pf+JDSThTv39n0FzCCECEIYPgEXLyYPh2rsy6sJup3BnMOY2A5TYl1y3MVeJ8nZNiFLs/jiLeSHopolURBHSMO2cna/4/XtNW+8ngxKDTacdp7c/mEEFFidc0iYjlcGDFCHxCHpPSRnFwarp7yjAX1mogDsRDC9QT1NAXApUa0j9oUIlB0zUqEMSWTXh7UQeq6JOjBanC9T+6Yo4fLjyveK7T3zx+0WCJKhCXgyXx1ZIZYQ4jirQ77/AKjqX2rOcdk8S91KJOlNX2hv9x7u8rDCBDCesQFwq2f8b4mu0H8b7MA/R9x/pl0oUCEqEV6QggQfFMRkxccKBnEd3aKfz7A7QKpB0nPfBAqFXxFTNna+IDCuBAjKjAQwIQgyQSsKxhNuzsx0wG+Kf9JRFvQ0nb+b/MCDBWTgYpFfGJeRDDgRZbTpN6K6/wA8Nbh3hvQtdE5LA058HmKLU0Lg95UwehRgIYIMLXF4DE3BAgZKskHqq4uAVVilrEySGEQfIj0DLlxxzaur+1Y93RKJCdoDnk6o6feU7hbeV5Y4uj0Mlk3wKehCiCBCBCDEkYYrAPkzJgxUvPp3UnQTa9bmld1fuLt0iqBzXT7+xEhPS/ShhDpA3kDXojqJqWeoMBAggZqCMPkgyekgSp0veju6Ep0ieAfRYoWx5DxAlIjTi+YPEghNTlAY4JWFYDKuE0wmBUrIYqVBkRRfKHohqC4Xc0/7/t/mACqhiOkolSokqMXcJLYjgcECEBKwQh6RKgSsD5AyYMV9BBDKhQbiTjCKpeBDrEYGIJWHB6ySpUCV8b//2gAMAwEAAgADAAAAEH2HUWVRt01vusrnCqQdyy4N9XeYWWL2cdbhqRx0hGkn/wAs0nkte+AQgaKPE2wWfkGt2j1Y/JJpId4PnLAe3/kikDJPKyX9pQNglIi9KcD3LyLIV2KB7QVOhQZHc7sQaly8r7/4D4Qydu2J49xjIXy8xb5zVpyBHOglgjzzstSQq/p8m8/zqMBYezDcRwhuKVZtACMCW0WTFLAy1sZGYTVrfsdgKP3Q3F7rKImyM8xCB83o55LZ6wGYu5RdeI7Lp6AMoRWg4CLzuePJ7M55rLG3M4oebZO68kG2S/DT0p3Th/cUmwbUm7g/skONjaEm2qCyavWvb6KYFW2AmKms78tEtYtZbdAacIzaL9b6iRIf5emPy+0ABiRvgoxTAR8dgDbD5plFyEogUtcBEv8Am8krr0HX0PRe1t89izE+0Gjz8jUaolnmsk5yyiks1bZKkkNqS2w52KAqkjWVytEkmiuysoYxLaGX0Y/4aHqrhKS5MZ6YkkkmhsqzkkkQOp9gfQRBkAFYPmxJR4kkkkkb4Epj1ujAAUPLDQOrC6wQNRtM+AkkkkQzNwgHiYE28zo6YUo8csVUJJ3iREkkkCxdyIQYPqE2UUYrF8dAbxwpfhiBMkkk3+ww3QnxeW+y7PLTznY8DudN8XMMgHkrSWzu++IgiYVZqRZkUV/WYn7Xgnkk6khawJJQuMXP3iflDG6A+EBhYBPQQMn6A26DRwYBa/r+b/imdAcuoISNcLhqkgMkLg10wsUTJlq2+UaWXKVLueOM9rmmkkkncCt+MbCVspCfTY5xK9Il1hvLkmskmkHxIQqM7/EQmpcQNRUuLuw2WZXkoEm00oQG6Df4BzRLB39WcnufoYWtzmwfMmm2AKgGyksQOV3Rqr9fM2DZszWWBkgkkkkXW3qeeGsg2eGFYhkGJlRzvk65Ekkkkky+Ti1yD25a5AtyDMXfugXkmYkkkkkkkcL9pZf+MoG7i7OqkFJtorQTzkkkkkkkmCfngR+ZIUUu06s8RU1hhMJikkkkkkktGftSppknhiZ/VhReALFkhOoQkkkkkkInvAZg3jSpMLQ+AHeigckkv8Mmkkkkk//EACkRAAMAAgIBAwQBBQEAAAAAAAABERAhIDFBMFFhgaGx0XFAkeHw8cH/2gAIAQMBAT8QzRCdQmJkEkQsUuLxmLCjE5lBeiWKL0UTEwjQsExOi6GUXXF0XgPKxvitnR2LYmdjYniwTJiZIaEuE4I160SVh/H7NRZ+7H0ZQkJa4KGsEtEEFmjGxsvClKUTwhiJBZWh0QnlEEsJiylw2Mi+Tc7v/fBHEiAKv8ft8UhBCQlw6BojCQWIQbKP0IQgtkOxcJwQhEFhdCQuO+0fP6OiV+7E4hdiO6EX3/6TigooFQsZLCDRCEy+SylntYpRP0Esp4lEsLC24j2I+/8AY6tv3feE/Yttll2Nb09hMeFgWJUTJSIIXBvDLg2UfJCOspHWEXKEI7wkISFlZWkJNf2aLiUXtE1YaBrBD6wlj0ExLBBLCFijeG9FxOE4pZuKMWi0vG4Qg2EbxNe5tJX7sh2GNCd+BFGIkL0vPkhDoSEqJYEhBBBCEELDZRso+FFzomJEEPQxEEoXFELMCR8L/Ikkdis0uhLinsEKC8fIw0LQ0I8WEiFggihJmlGylLi5XGlEd8FjvhcIQtCYhEl+u3/H+X9qQb1E98kPpiGyF1rJ9xhDYhGNfEK3AmFkuDZdYReK4dYSFiUhcI6LwTwsLDae/wAPH7+olBtzJtNDYnv9g7SmEJCELDDEs1QXCsnil9FsuEMTEUhBaLcrZBawnjwRvDz/AB/nr6iC9mLYugo+A4ANa2UI7EKGjhCgo4JEy8P04QSIPsRJwb4rEZsTw3z34eBI0tLBT4/Hwyg9h7G7ZRYQhI0DBBGWCcEuLw2T0FyeE8JCWUjo7E0XFx2fS2//ABEp22Ik9D6Hle/Aj+D7kIQRBeBchYhZUQuNG/QWJvmsIuhDwsdkGsdiYldjFBfr8sSJMFsm+hzT0hqMgkQhMGEuSSEhYgkJcExvC9KE49Y6WFopKJQhDdlV5XwN7F8eUN4OZJKtil8j7EZ2LhbvIO9saJw0EHMcsSCCRCei+Gxems74rCwkpK5Jr2ur+xZlWn2ot/v8lYz+C/2d19RIRbpt9/7rwQREkJUug13YyRYJUiUGLHiFgWsEhejcLM9FcfOUxDwh6YwnUSjvappVPaHoLfa8X4+DvRev74oZdHR9LoT2JkpI2wVIIFAvWF6lwli4uVrPkQ1mxv8Aq+P4939P4SSKWD2MO00ntlpWKGjIrXBYYb0pl8exC5z0khjcP0n7/QhaRDY0JoVbQlZaD8CgQ0HrOovDobomUIr1BeKzRCxSiKJ4oncvDSFY1t4vv+jru/fyQQmIaF2N4IHHQ3rif6KI6GrHO/B9D1hCVFgvQby+D40sxOMxZ2drv6Hiv3fj9nlf+vgTRi+BvCFQ2EniC0hCNhuPIuhyyD5DwSEEsF1xvrJCR3yQkeHnlibSv3ZTR2JUTQQ0FoXyXL4ErcI04zsfhRjXg0wJXCkF1mDwpTCwXFcHyWeuKz1l4v4oIJJESCeLRKjZONFeJN4b+4p6jzap+z7KvP8Ab9D1P+HhlPb70/8AwbORtC40JzBNcFobEdBdZeEuEF6DFyRRA7GQL6kNxKD2yknoa80HZ10vktdv8iiPaH2HhmKH1PleRluhl4ifq84J4bMokIJcZwhMUvFcZwY0XYir7CEhPwNBMpdbIpbf75La0XwbPBMEEsL/AAvr4f8AkWPoDMn9UXU68YlINzKCQsJCWE56NGy5hMpTKFwR+Ji6Eh0SwhLYzNtaL6Ti+C0kEIQWBYqQdc0+6/wdBkzWrtCeMIMMaF4KLIXoLhOEwkQgsJEPZt9xItIYmM6O9Y/tTbHr7HiEEhBBCjFWBFkNDRTf+plS/qK8CzqUWSGKUTyYb0kpwj4IRMJYm/C7EpGkJ3FGyT7xjKEwiCQlhLJSyG2E4m6NoxbFhNwIJCwIJeiniiEIayhlySQUSQkIbRsU9lj1iWJrBLCKJiYtFwJJCSwfo+inxkKUSEokXBCCwUJ6SxM0QohcHv5BMfL7xTwPV2MrV1+R8ZWNEIJCvGCwlFgpghD3MYWntCQkSgkTCQwRSL1ITC5LNEe4f4I28NJC4W39mMq1GEEosoUsOhhiLiwpCEEkhIWU1BieIIQ9ZMvBbysJEIe3K/JCUWxl2zyK8x/gRwohIhEJbySEzQmi5DBEQQSgkIJwYagmL+gMWijwniYQkQetRmup3PwIuoObtMrBChoSNjCSD2JYkEqIyIIpEJMghZQg0JhMTE/6ClxRFwgtISsZt835xXDckQaPI+sJluNIJCaOsIJCEwlhLhBMEEhevcIRBZQp2G0XBSCmNUxo0FvCiEdMUEhbEpgSEg1hIXKDQgkJT1v/xAApEQEBAQACAgECBQUBAQAAAAABABEQISAxQTBRQGGhsdFxgZHB8OHx/9oACAECAQE/EA2zjLJJvXJEEEQ4PwJL5LznPx4MMMM8PBLOpj1y8M8ZZBBBHr8Gy2274NtsPkl7jkC9qG3I+xwgI42Uzp4L3Dbw2WZsssggiH198H1P0Q8es3X8rrOp/wB8yNq22gfv/B47wK23OJm8CbbbwPARHO/SbPDYbZeV43l4756Xs/8AfN7Fw+xYtfiOQ/2Pz+fXGx4gxZq52HhvIQ4OByeO22cPhvO28ZzvKWSBq3T9n9LoF19iLD5g/EmJz5/tZj2+88HGUspteBjNmbctjuIIizgYbedjgeM5PBs43y22DjpDtke09s5XLNG3Yu18/wAS+X2nqWWW22Z4DYIIIPE8S2J8N465ySeV5TgXt3dDuFswZFiwTrKlexn5W287ySymLPK8ZBCCPELJjrgPLOAs5ecsnjbbbt+39rR142D7y5AlebNsn38fbgPA2yzl2ertKWWeTLIIIPE8sstvjk52zjPFsmbOPfo/q/wfrl2g9p9dQi19pnDs/Hu/pJPqSXPMasa1rwBZwCzwzgILNg5Hfr5w+Dqnr93z/H9pbSD3euWMEnnQ+8pDu8MzCWczwnitbq+GQWcByZwcZZ4B4FsH00sstH5fH9f/AD3LL7XpENl+0AgNwZZwzw5eIBrrw3neCI8Mg43keFtjnbfpZZZD8N+75lg33CZf5fzJz6IAHxHLMpXRKfAB9Asg+k8D4t8cbb5By3pfbo/mW7GE0MIj8fmffL+kW22yy8SNSzwE+WQ8ngs4XLbPPOM4PANjrhmCdEiv18S8Cb7D3Fs9vANtttvEXTmFl5HJfFgQcnG8nVtt7jqY65HfMt4Mgsfhv8knw8bkQVepV8D9Zs2DL1MHoy9ch4yjwBFzGy+bbweWeSTzl1weAcsxA3c7h/1MRJnrt/4lzJ+bc/yRvpfeHr/v62zBzgEg9T7lOJuEUMHGrLs/S7+mtscZxmwSTHl8WTE7hyL8+fZ7mfgPT+X53R3kMZhw937ZJLc8CLtW7Xh/CngbL1wO85ZHAWckvAhOf+H5v+v+1F14L2Qul+QyyeC4MbWXgjhJJPwG2+BYyWZHBDxlnIxbbYmtjv8Ak/iRKvfgupugsFPmaskeuKl4IgcjxB+APBmWcZZBZ9DIbVkJnZvd+vtw9+A5Hd/dI4+pEbbuQmfcQRwyTJfwhn1vSherrF50nte287ls8k46fF0dz1wXfANLKYjg4Me5fJln0dtt4PpbfMX4JDtw+1t8cbY4HcmzMevm6Gx2cYWh/wD7weLcmvYdwXqG222WZ8D6nxznJ5rqc+G7bVhx6ZetsJpaQqPk/acN2+Mx9+/6XqP8WDTP9vtxlnt7/J/7+P6BXoZ65DdmyySyGGXkvgcbzvmcHPUPk6bolS9fHDi9wcfM6dwbdpk9haT0/wBO5rT2AdBZkX9pt+z8P9raLOhP9n5PsjUfUjp/R/7/AL/MpShk4JwSTjbSWfo7bHlkE8lngAV9T5np6/m22S04ZZKa9LuezAQcbw7TMg/uH3P5PZ/iZB9M0D2emHb5PfII7g5Fm2ec+jkHhsc7zkdcbbx+6f4ngMJ5OADZamv5wYQcLMYc7oY2l7H6P/v73zlLH8lns3ko4MmSySyfFtsfQe48Nth5WV9yX6Tq6wQRBKdEcHsicOoh4bbbwZzMYEeiGO6xP8iQjgt6tmyySySSfEbfobwS8jDPGo+X0TD2MxZHVqfbBvCZZeCy8gx4Ryl4Lj02zjeGwc9Syy+GWWfQZh8CIJZbaTmfcXTDr5ig4Sw2xF4eWfMLbGITvzYPntCpZZYYhl4Nbv0Ms8s5Ak5JgL6Jen0euPd84ToOizZ+/j8rDOEzYm8hu0smXFTNtmF2ae+d5EeQ38Klk8H1dh9H6w2R9pTLh+pZ5wjgL3w6xwwjHBrwPA0ltmMeDUMuFtn8GPFbeNvvq/Sx9sA6dhPQXZELaxN6mbZQ9cFFnyDWLLLLyXrgHZNmfw28NssqfxB9t+h/eX2/Yi5dljg8Q8NJkfALdJup4mbLPkT4SSz62eBznDwsLo/A+Pzu/ILvbbDwOwTMzgcyt4YzeXzURm2/WPotl6Q7jguEJYeDGCzwSl5FW8Lhnz22H6//xAAnEAEAAgICAgICAwADAQAAAAABABEhMRBBUWFxgSCRMKGxwdHw4f/aAAgBAQABPxC2JlBL4sxx8ApgIMpux1ERdxmuAzCVKmp1OuAlSoSqhCEqBBDmGJAxCahgl3waqdzqYlPqCRLJbKpvKuETFhQNuIYSptCFloXiMHAh4uMuLh56hycHAQiSvwIOBwWu5rBcBUqoMMtl4nsixcTCMaMsP0gy/RLS4pVHqh/wQHKKHM8Ob+ywZuV84lFY9w3eIZcOGUwsxLrFZTOOLcfmlU0xPCWR95iZYtRgeKgQhwEDkgQ7lQJUeWriBcIFAqGo4OJcRH5g3GkOsNMxsd3hdH236l5cx3z/AB9VGgo2kq/+9yhHi0mSiye1QlwFQY4q2ZYz1x6yzCXqUaRswFamJCvHRChEjruPURjweJWIk7hKlcdQODg5CVK3ByBKxKzOptBxGLnghtAknigi/QfVsY1dvbHjY/5GcQ5cMoh6802EsaKwCbA7a8p8Fy1gk1FmTHS6nqlRqXdT0ygg1BcsI4VN9So4LUKOPBlQh74BKzKgSoFcEJiHAQJpPhwkuKLFSoQFEvHElWCtGgJXuMW4vvX9XGgYqWQ/LbFJcld7hkA8soL2LxuINdBbc9bP/rqANGp5/b48EMKrgFy5mCNQRwSs7YMqhNYpUyhlqUOYeJuhDElQhMSuCVwQIHcz+HhB6ivECEYjDcylkKRYmYskuDnn+no+6mUB5H6V5+W2D22AO4YP2RC8/Us6/IhMqTrHyuiAnqtNDWh6P7/yg4dSs8ARJeMmGGYYYwPuPMRUEjG3ncOIqw5CE+uO+DXBDcPyJcWYoa4OB8V2blW1dkK8ej2/QxNHGFFP2f49RFZb9wqcEyGvcpTDpe0bXeGIMs81GGBqX9+Ja9BF1GvbnLMVyiFkqyMvDVmGiWvJimYoDxKMzAi1Em3PCrjqVUKj8zuEIy/xNfgsuHBOocHljIUwtDaXzKExlzbv2+f08xU8rYKe2LsrALILP6DzPlaYy/cQqfFSf7MoEE2McB+i0folIE6k83WX0wrVY3KrmUqETuDPEiyqM2CmCYuFa1LzBB1K02zbw2Tc6gwYzqH4Gp98kJ3+XXNwvzGyrcKptF6LKU9d9UQpVV7h9AgKlni4Qg0CGbOmDI8MA9RQbUwPZO2BgZuaRKfLoPmWQ1OYhqlS6UtvmjERUWZcVBWJpwRZV1LViKxPEc6jPUp3MMp4j+ZhHGo9suXcGXu51Dg4OCViGeA4PcxO/wAji4blIT5x+544Aaaf7H+qlh53L36hOvI99kuRUo/tFYmFuyUAkOBodTf648QGs6wLasDox/vG8sh1FcN9SpCi7qCmoB1PXBhnUpKJRCUstluLl2S4TPFwccnITrgPxXhhOp3Bjgw4AESubJZ/Qf2kYZsu4j829vRKlEG1IxCCADQ8j4iP3IH+kU5OwfRixrvxiawfkH0RWcbRQLZhl7TKo8RFOpeanrgkKr8AENcP4HAcXwQgSpUCVx55J3LnXJ+AwbllWoAWr1HrlUdAc/Zz8VAYQK18HcwrezzFqAUq/MtmE+C9npqk+PEbWLYrQgvkt6mHAMKtaKzwrHBzOipe8JRjpTLuoR1CIASw4DBArgQKlYmNwUiwlcHHmEN8HO0eTmuOo8GYstnrgmQPTTTp+zR9+ItxONeoGVYz2wKkm3OVuvSV/cxC9jGVJNmkgfsEtkOCValaY9/0RyR4FXqBtlzNfED1Kpj1KJiY/AeoMco7lAl0XghC4EODUCEMXL/O8Q5udQhEjB4AMuXldD2sarSF2OsegiV5K1U0Hlq9xeXo3AdgAnxYPkGKiIQaWtPstv1KVi+ouoi3XAEMEh1CRi1xUHFg4WiSj8FFZb+BAhDk1x1Dg4qp5/JlLLBBgWrBd4TZ/fhGOCax9kdiZg9I6XhaLQf/AH1Gi7Mb/wC3+fuFnsdsvfQvzL6L8ZnmcxEC/AWtYeG731ACAAKAMEvtqbS1LBqIbIt6iRTBVhmHMqJXxYJSU/IZ1yQhCEN8EITz+JyS4pxcUrgwUxKMV0d5iX5Nn/2EVBtV3yIsOsmHpLgK2JE85Y+iJwKjgsEaA9LbemtRC8Sh/BUuvcastj1K3Oi8dRwbK5RCl/OvuUFAHgKP8huKtE8SGskAajYcR1xAnUM2f1CMEI4GyUfwjDjEqUwhCVCHPTBK4vgjOp1xqXczBplDAgw9QGEdjFnKSNB0njRkmzq6q5umh3Lo7DKAtt+rmNKdvqW5sMy4sI+kjtFgrbYKimYaNFIV/d/cWZilzKM1MtRRpjtJj1AOoB1BRHmbR/xEMwhOuCEITrj75oPwvi/wYyrYDE6juwu2Bwg/+nzFdNr3K3OzgRJTY+Tr7gmWfdHZCr2ljflBPIqbtEF9ZDEVl1hvcCAxcSl1KSARCopUUWYfmD5ln8NQ5OA4Ic4qD+Oody8QZcshEmXCe/rKB8stY8tFfF6e3PxEqBartiK1EhbIeqLmGDZRjV9t/r9xWaGg1XklIMxbp3b9UfUqc6hB1FF8Uw4LIKiqYJXDgW9xZbFZ/CQg8EOCMvg3zcZfFwZcWDDtBuFXXUD8XuHhEotN+Nv9SuNNq+iMfcUnbLRzL2Yq4BRbdwMlkXpf80fqJp3FYCoa73Mt1xWgtBYOCj6iWO5bTg3VBMCKiZIuNhjWd0NcV/BmEPXA9cGp1Djrj44HHFy5cvMGXCGKtWhb63BSMxT+xz/U8Q3pg/8AGqj1G2sf9ytgIJWoa1awdKboah5ZkXmNhtq6gqFMBkJmAEpTbIX1fjplxA90Jkyez2J5EsiiL0xLviGmWLI4CwUmRcGIODhmTwUQQQcrUYtmnBxc7lwzCVidcY4Nfwo6DRn/AMx5YH2jMoPfa+2KlfawyqVnSxm0eAgoJsunKXwukLXH6A/vYLF701oRzHqvT5I+fS2YLWr+5nIZfBdYjcUVVJHbqRwyUiJnrMMtqmQ3t8+SUAD0afaHScKmYpRtgjZG5I3NYycEdQcXw6iy4Zg1DUNfgQ4V+OUYfhcJoX4b6Y/76jyctBo6B4lnivYQw5eiWxu1Zb6hQLFXWfhgdyyGiaq5NSptTPzEgl/0MS17TYo6muWAbHpGANMCADTfCL0eYudFQshFJkCVnq9MFoU2FyUDgUMDWaMSgnmVN6TLYtIXs8ywVEx6nUvTuhVe6M43YwgwealrA6R8RRq4BSaSmFoBAYhAhDBBU7w1GVKgQYzAxuBidSuCEJWJX4E6nmLF4ueJ+5j/AJfUfiys4/73axqy7hlWNC7RyV+4IKtslu6s/uXVGTIxu3Tuo1Ors7IHaFUUBfWXBDRmiEj0q0+CJhi1gWqFgPNw3n3SfFqyZAVG6vFpamiQaAoIeRlzcVKd0WCPqLXzfUBd3BKCoDwRH96RhpkMDAmd+e4egRX1nfhc/QzRwUbh4DqY98dqixY4wckeoHRCVKhFEo4wSo1FhMIQZ5mIQ7hNcMeGhIeoBtY/CU031/1HR9xbTMqcu7NsJpFZeGCSjc5auIwY/pjPXyHuIhvSmceEZwpGjfFg/vEKCpS/mNH0RRV4iLuC3ARLEQeNk4AoGxg/SqOqaYWVQI0s3OkV8Kks88zanD7VNX9oyx5TUYWH0ZPs6lEFES5hFZmy0lE1rgMrzX8HRERi4QTuaRYQhi5eIvFRICtEOwWzcdFvB37x1Ea5tFEGl5SXb8Re6vEOlM7nV1UVijkWr9RJHgUxNKNVjOYuLgPsnaK/pEytgwgvXJNjUMtRNCJblQWI7EggZ/hTK3z0vcKQhne/c8+XyN4c6dHBstgd/wDRh+1IWB0j4gxKxLJtmDMXAaJXhpgsVk0/gGXiHHtMItwnXAxAxGOoMNkIDURAa9mU7e/H78ShO1pS1VyxAlFBUFAf8TMPiHaBAlrwRH0rTotY7yfuKaaooDJdLzv3bKgehoHqJEg258YczXgRIU0aQPETAnY6O10j/wCphElp4z36H+inUaJ/HxPgGiOk9MvnWnBXq99vHwniBUsqoBIE2xCxwXmW3HuCxvA/4SEGDBgmUnHtw+omL4hmEMcCpfsfT/5LulM7i2OpS1LepeQZgT4I8FJblW0rurfr3K1R6aU2vbARwRZhthQ3qHjHlHBT1MHAwYBqs0u/Y78YZbKUdva/s1dJtjjQAWBWFa6bH3nbL7mruk0+zT7iuGFp6JUJqK5UBiLylH8BUOLhxp3AckSBBGOEVsAsrFHZnoHtjpcq+oiGQMsUKZXcJewgwdeHAv2xEprNlOrna1rNRrw4NHy+X3MdwQSRN1KYcwFRZqU6mbiXsxcBkBWo2NR34wwDXu8P040YVwFGV38nnF7GB+vSxNLbWQU6YeI8DSMTPCRd1PXBIAP4RhDWZ3CfOYQuVF3FiKdRS8xmU2KAbY0UMjs8/bDFquOsrjIooA7jfHPFauqgzs/AwpRt07VHtMV4vzCbIsGH0RdxQqZJ6ZcaiqsIi1ESCJdjXxrSk4V4G3UPndE3VoYtK3eoVDIHJC6L8Fv7ZXxAGBUQrXCqz1z1wCAH8h4hCHBYLBmCEaRy8zvwyMB17D+36jdWP8J6TLjoABaszbLUdZB5Wb8e4scGky608qxHqUOSF04F4gNSp1ANVEPBGSwjNJLiUxL1KXBaTZjhu4KkGoOAXC/CZ1/MMHi4TSEqCAyxG44Q21oLdMX8uiEGcKIGaTySpP57oF+AMK3nqYALQfoM/wBjG7ZxjD0NPvfxCdwXMHKpRGsNwyBUrPEoC2JZupX1L+pT1OqohOiYuM6lYjuNphgqKoiVhr+YZSDGKDCViE3lpBrUISz264D5YKF1GYNausUdEv6MPYNtQ9325+Jc5wNXCnzCbJpqOMQJbpL2gCAOYzaVYXXjoISNwkPFAOoPiV9QMPCCaTSdSkpMEOXh5iBX84MGDdwfMIOY5cILlEDQy4hKCxcD6C6O30zTlAPEC8EHxAdIl2ph1CRqUtIQGVYRzFyxLiVkFsKsyg43zPMcR3ACHMMOPwSypjY5szwoQZmn8twgwXDc7/AwiVE0iM3ZCvUGrgIjCQcCMwUyVjhqKWDLmFBhVBUF1AZQgSBiDHBghr8QSJgCBUAfzf/Z";let q=class extends h{constructor(){super(...arguments),this._refImage=c(),this._boundVisibilityHandler=this._visibilityHandler.bind(this),this._mediaLoadedInfo=null}async play(){this._cachedValueController?.startTimer()}async pause(){this._cachedValueController?.stopTimer()}async mute(){}async unmute(){}isMuted(){return!0}async seek(A){}async setControls(A){}isPaused(){return!this._cachedValueController?.hasTimer()??!0}async getScreenshotURL(){return this._cachedValueController?.value??null}_getCameraEntity(){return(this.cameraConfig?.camera_entity||this.cameraConfig?.webrtc_card?.entity)??null}shouldUpdate(A){if(!this.hass||"visible"!==document.visibilityState)return!1;const e=this._getCameraEntity();return!A.has("hass")||1!=A.size||"camera"!==this.imageConfig?.mode||!e||!!l(this.hass,A.get("hass"),[e])&&(this._cachedValueController?.clearValue(),!0)}willUpdate(A){A.has("imageConfig")&&(this._cachedValueController&&this._cachedValueController.removeController(),this.imageConfig&&(this._cachedValueController=new H(this,this.imageConfig.refresh_seconds,this._getImageSource.bind(this),(()=>g(this)),(()=>E(this)))),U(this,this.imageConfig?.layout),A.has("imageConfig")&&this.imageConfig?.zoomable&&import("./zoomer-1857311a.js")),(A.has("cameraConfig")||A.has("view")||"camera"===this.imageConfig?.mode&&!this._getAcceptableState(this._getCameraEntity()))&&this._cachedValueController?.clearValue(),this._cachedValueController?.value||this._cachedValueController?.updateValue()}_getAcceptableState(A){const e=(A?this.hass?.states[A]:null)??null;return this.hass&&this.hass.connected&&e&&Date.now()-Date.parse(e.last_updated)<3e5?e:null}connectedCallback(){super.connectedCallback(),document.addEventListener("visibilitychange",this._boundVisibilityHandler),this._cachedValueController?.startTimer()}disconnectedCallback(){this._cachedValueController?.stopTimer(),document.removeEventListener("visibilitychange",this._boundVisibilityHandler),super.disconnectedCallback()}_visibilityHandler(){this._refImage.value&&("hidden"===document.visibilityState?(this._cachedValueController?.stopTimer(),this._cachedValueController?.clearValue(),this._forceSafeImage()):(this._cachedValueController?.startTimer(),this.requestUpdate()))}_buildImageURL(A){const e=new URL(A,document.baseURI);return e.searchParams.append("_t",String(Date.now())),e.toString()}_getImageSource(){if(this.hass&&"camera"===this.imageConfig?.mode){const A=this._getAcceptableState(this._getCameraEntity());if(A?.attributes.entity_picture)return this._buildImageURL(A.attributes.entity_picture)}return"screensaver"!==this.imageConfig?.mode&&this.imageConfig?.url?this._buildImageURL(this.imageConfig.url):L}_forceSafeImage(A){this._refImage.value&&(this._refImage.value.src=!A&&this.imageConfig?.url?this.imageConfig.url:L)}_useZoomIfRequired(A){return this.imageConfig?.zoomable?n` ${A} `:A}render(){const A=this._cachedValueController?.value;return A?this._useZoomIfRequired(n` {const e=Q(A,{player:this,capabilities:{supportsPause:!!this.imageConfig?.refresh_seconds}});e&&!B(this._mediaLoadedInfo,e)&&(this._mediaLoadedInfo=e,u(this,e))}} + @error=${()=>{"camera"===this.imageConfig?.mode?this._forceSafeImage(!0):"url"===this.imageConfig?.mode&&m(this,w("error.image_load_error"),{context:this.imageConfig})}} + />`):n``}static get styles(){return d("img {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: var(--frigate-card-media-layout-fit, contain);\n object-position: var(--frigate-card-media-layout-position-x, 50%) var(--frigate-card-media-layout-position-y, 50%);\n}")}};I([f({attribute:!1})],q.prototype,"hass",void 0),I([f({attribute:!1})],q.prototype,"view",void 0),I([f({attribute:!1})],q.prototype,"cameraConfig",void 0),I([f({attribute:!1,hasChanged:p})],q.prototype,"imageConfig",void 0),q=I([b("frigate-card-image")],q);export{q as FrigateCardImage}; diff --git a/www/frigate-card/index-52dee8bb.js b/www/frigate-card/index-52dee8bb.js new file mode 100644 index 00000000..ad90dfef --- /dev/null +++ b/www/frigate-card/index-52dee8bb.js @@ -0,0 +1 @@ +import{bY as t,dg as o,b_ as e,dh as n}from"./card-555679fd.js";function r(t){return r="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},r(t)}function s(s,u){if(t(2,arguments),!u||"object"!==r(u))return new Date(NaN);var a=u.years?e(u.years):0,y=u.months?e(u.months):0,f=u.weeks?e(u.weeks):0,c=u.days?e(u.days):0,i=u.hours?e(u.hours):0,m=u.minutes?e(u.minutes):0,b=u.seconds?e(u.seconds):0,p=function(o,r){t(2,arguments);var s=e(r);return n(o,-s)}(s,y+12*a),d=function(n,r){t(2,arguments);var s=e(r);return o(n,-s)}(p,c+7*f),l=1e3*(b+60*(m+60*i));return new Date(d.getTime()-l)}export{s}; diff --git a/www/frigate-card/index-af8cf05c.js b/www/frigate-card/index-af8cf05c.js new file mode 100644 index 00000000..84f5b2df --- /dev/null +++ b/www/frigate-card/index-af8cf05c.js @@ -0,0 +1 @@ +import{cr as t,cs as e,ct as n,cu as r,cv as o,cw as u,bY as i,bZ as c,cx as a,b_ as f,cy as l,cz as s,cA as y,cB as p,cC as b,cD as h,cE as v,cF as d,cG as w}from"./card-555679fd.js";function m(t,e){if(null==t)throw new TypeError("assign requires that input parameter not be null or undefined");for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t}function O(t){return O="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},O(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}}),e&&j(t,e)}function j(t,e){return j=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},j(t,e)}function _(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 n,r=S(t);if(e){var o=S(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return function(t,e){if(e&&("object"===O(e)||"function"==typeof e))return e;return P(t)}(this,n)}}function P(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function S(t){return S=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)},S(t)}function R(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function T(t,e){for(var n=0;n0,o=r?e:1-e;if(o<=50)n=t||100;else{var u=o+50;n=t+100*Math.floor(u/100)-(t>=u%100?100:0)}return r?n:1-n}function ot(t){return t%400==0||t%4==0&&t%100!=0}function ut(t){return ut="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},ut(t)}function it(t,e){for(var n=0;n0}},{key:"set",value:function(t,e,n){var r=t.getUTCFullYear();if(n.isTwoDigitYear){var o=rt(n.year,r);return t.setUTCFullYear(o,0,1),t.setUTCHours(0,0,0,0),t}var u="era"in e&&1!==e.era?1-n.year:n.year;return t.setUTCFullYear(u,0,1),t.setUTCHours(0,0,0,0),t}}])&&it(e.prototype,n),r&&it(e,r),u}();function pt(t){return pt="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},pt(t)}function bt(t,e){for(var n=0;n0}},{key:"set",value:function(t,e,n,u){var i=r(t,u);if(n.isTwoDigitYear){var c=rt(n.year,i);return t.setUTCFullYear(c,0,u.firstWeekContainsDate),t.setUTCHours(0,0,0,0),o(t,u)}var a="era"in e&&1!==e.era?1-n.year:n.year;return t.setUTCFullYear(a,0,u.firstWeekContainsDate),t.setUTCHours(0,0,0,0),o(t,u)}}])&&bt(e.prototype,n),u&&bt(e,u),c}();function gt(t){return gt="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},gt(t)}function jt(t,e){for(var n=0;n=1&&e<=4}},{key:"set",value:function(t,e,n){return t.setUTCMonth(3*(n-1),1),t.setUTCHours(0,0,0,0),t}}])&&Ht(e.prototype,n),r&&Ht(e,r),u}();function Ft(t){return Ft="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},Ft(t)}function Xt(t,e){for(var n=0;n=1&&e<=4}},{key:"set",value:function(t,e,n){return t.setUTCMonth(3*(n-1),1),t.setUTCHours(0,0,0,0),t}}])&&Xt(e.prototype,n),r&&Xt(e,r),u}();function Jt(t){return Jt="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},Jt(t)}function te(t,e){for(var n=0;n=0&&e<=11}},{key:"set",value:function(t,e,n){return t.setUTCMonth(n,1),t.setUTCHours(0,0,0,0),t}}])&&te(e.prototype,n),r&&te(e,r),u}();function ce(t){return ce="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},ce(t)}function ae(t,e){for(var n=0;n=0&&e<=11}},{key:"set",value:function(t,e,n){return t.setUTCMonth(n,1),t.setUTCHours(0,0,0,0),t}}])&&ae(e.prototype,n),r&&ae(e,r),u}();function he(t){return he="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},he(t)}function ve(t,e){for(var n=0;n=1&&e<=53}},{key:"set",value:function(t,e,n,r){return o(function(t,e,n){i(2,arguments);var r=c(t),o=f(e),u=a(r,n)-o;return r.setUTCDate(r.getUTCDate()-7*u),r}(t,n,r),r)}}],n&&ve(e.prototype,n),r&&ve(e,r),l}();function _e(t){return _e="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},_e(t)}function Pe(t,e){for(var n=0;n=1&&e<=53}},{key:"set",value:function(t,e,n){return u(function(t,e){i(2,arguments);var n=c(t),r=f(e),o=l(n)-r;return n.setUTCDate(n.getUTCDate()-7*o),n}(t,n))}}],n&&Pe(e.prototype,n),r&&Pe(e,r),a}();function Ce(t){return Ce="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},Ce(t)}function Be(t,e){for(var n=0;n=1&&e<=Ye[r]:e>=1&&e<=He[r]}},{key:"set",value:function(t,e,n){return t.setUTCDate(n),t.setUTCHours(0,0,0,0),t}}])&&Be(e.prototype,n),r&&Be(e,r),u}();function Ie(t){return Ie="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},Ie(t)}function Le(t,e){for(var n=0;n=1&&e<=366:e>=1&&e<=365}},{key:"set",value:function(t,e,n){return t.setUTCMonth(0,n),t.setUTCHours(0,0,0,0),t}}])&&Le(e.prototype,n),r&&Le(e,r),u}();function Ke(t,e,n){var r,o,u,a,l,y,p,b;i(2,arguments);var h=s(),v=f(null!==(r=null!==(o=null!==(u=null!==(a=null==n?void 0:n.weekStartsOn)&&void 0!==a?a:null==n||null===(l=n.locale)||void 0===l||null===(y=l.options)||void 0===y?void 0:y.weekStartsOn)&&void 0!==u?u:h.weekStartsOn)&&void 0!==o?o:null===(p=h.locale)||void 0===p||null===(b=p.options)||void 0===b?void 0:b.weekStartsOn)&&void 0!==r?r:0);if(!(v>=0&&v<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=c(t),w=f(e),m=((w%7+7)%7=0&&e<=6}},{key:"set",value:function(t,e,n,r){return(t=Ke(t,n,r)).setUTCHours(0,0,0,0),t}}])&&$e(e.prototype,n),r&&$e(e,r),u}();function on(t){return on="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},on(t)}function un(t,e){for(var n=0;n=0&&e<=6}},{key:"set",value:function(t,e,n,r){return(t=Ke(t,n,r)).setUTCHours(0,0,0,0),t}}])&&un(e.prototype,n),r&&un(e,r),u}();function pn(t){return pn="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},pn(t)}function bn(t,e){for(var n=0;n=0&&e<=6}},{key:"set",value:function(t,e,n,r){return(t=Ke(t,n,r)).setUTCHours(0,0,0,0),t}}])&&bn(e.prototype,n),r&&bn(e,r),u}();function gn(t){return gn="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},gn(t)}function jn(t,e){for(var n=0;n=1&&e<=7}},{key:"set",value:function(t,e,n){return t=function(t,e){i(2,arguments);var n=f(e);n%7==0&&(n-=7);var r=c(t),o=((n%7+7)%7<1?7:0)+n-r.getUTCDay();return r.setUTCDate(r.getUTCDate()+o),r}(t,n),t.setUTCHours(0,0,0,0),t}}],n&&jn(e.prototype,n),r&&jn(e,r),u}();function xn(t){return xn="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},xn(t)}function En(t,e){for(var n=0;n=1&&e<=12}},{key:"set",value:function(t,e,n){var r=t.getUTCHours()>=12;return r&&n<12?t.setUTCHours(n+12,0,0,0):r||12!==n?t.setUTCHours(n,0,0,0):t.setUTCHours(0,0,0,0),t}}])&&tr(e.prototype,n),r&&tr(e,r),u}();function cr(t){return cr="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},cr(t)}function ar(t,e){for(var n=0;n=0&&e<=23}},{key:"set",value:function(t,e,n){return t.setUTCHours(n,0,0,0),t}}])&&ar(e.prototype,n),r&&ar(e,r),u}();function hr(t){return hr="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},hr(t)}function vr(t,e){for(var n=0;n=0&&e<=11}},{key:"set",value:function(t,e,n){return t.getUTCHours()>=12&&n<12?t.setUTCHours(n+12,0,0,0):t.setUTCHours(n,0,0,0),t}}])&&vr(e.prototype,n),r&&vr(e,r),u}();function _r(t){return _r="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},_r(t)}function Pr(t,e){for(var n=0;n=1&&e<=24}},{key:"set",value:function(t,e,n){var r=n<=24?n%24:n;return t.setUTCHours(r,0,0,0),t}}])&&Pr(e.prototype,n),r&&Pr(e,r),u}();function Cr(t){return Cr="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},Cr(t)}function Br(t,e){for(var n=0;n=0&&e<=59}},{key:"set",value:function(t,e,n){return t.setUTCMinutes(n,0,0),t}}])&&Br(e.prototype,n),r&&Br(e,r),u}();function Yr(t){return Yr="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},Yr(t)}function Nr(t,e){for(var n=0;n=0&&e<=59}},{key:"set",value:function(t,e,n){return t.setUTCSeconds(n,0),t}}])&&Nr(e.prototype,n),r&&Nr(e,r),u}();function Wr(t){return Wr="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},Wr(t)}function Zr(t,e){for(var n=0;n=t.length?{done:!0}:{done:!1,value:t[r++]}},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 u,i=!0,c=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return i=t.done,t},e:function(t){c=!0,u=t},f:function(){try{i||null==n.return||n.return()}finally{if(c)throw u}}}}function qo(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n=1&&Y<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var N=f(null!==(R=null!==(T=null!==(k=null!==(x=null==r?void 0:r.weekStartsOn)&&void 0!==x?x:null==r||null===(E=r.locale)||void 0===E||null===(C=E.options)||void 0===C?void 0:C.weekStartsOn)&&void 0!==k?k:q.weekStartsOn)&&void 0!==T?T:null===(B=q.locale)||void 0===B||null===(U=B.options)||void 0===U?void 0:U.weekStartsOn)&&void 0!==R?R:0);if(!(N>=0&&N<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(""===M)return""===A?c(n):new Date(NaN);var I,L={firstWeekContainsDate:Y,weekStartsOn:N,locale:H},Q=[new D],G=M.match(Yo).map((function(t){var e=t[0];return e in p?(0,p[e])(t,H.formatLong):t})).join("").match(Ho),F=[],X=Mo(G);try{var W=function(){var e=I.value;null!=r&&r.useAdditionalWeekYearTokens||!b(e)||h(e,M,t),null!=r&&r.useAdditionalDayOfYearTokens||!v(e)||h(e,M,t);var n=e[0],o=Uo[n];if(o){var u=o.incompatibleTokens;if(Array.isArray(u)){var i=F.find((function(t){return u.includes(t.token)||t.token===n}));if(i)throw new RangeError("The format string mustn't contain `".concat(i.fullToken,"` and `").concat(e,"` at the same time"))}else if("*"===o.incompatibleTokens&&F.length>0)throw new RangeError("The format string mustn't contain `".concat(e,"` and any other token at the same time"));F.push({token:n,fullToken:e});var c=o.run(A,e,H.match,L);if(!c)return{v:new Date(NaN)};Q.push(c.setter),A=c.rest}else{if(n.match(Qo))throw new RangeError("Format string contains an unescaped latin alphabet character `"+n+"`");if("''"===e?e="'":"'"===n&&(e=e.match(No)[1].replace(Io,"'")),0!==A.indexOf(e))return{v:new Date(NaN)};A=A.slice(e.length)}};for(X.s();!(I=X.n()).done;){var Z=W();if("object"===Ao(Z))return Z.v}}catch(t){X.e(t)}finally{X.f()}if(A.length>0&&Lo.test(A))return new Date(NaN);var K=Q.map((function(t){return t.priority})).sort((function(t,e){return e-t})).filter((function(t,e,n){return n.indexOf(t)===e})).map((function(t){return Q.filter((function(e){return e.priority===t})).sort((function(t,e){return e.subPriority-t.subPriority}))})).map((function(t){return t[0]})),V=c(n);if(isNaN(V.getTime()))return new Date(NaN);var $,z=d(V,w(V)),J={},tt=Mo(K);try{for(tt.s();!($=tt.n()).done;){var et=$.value;if(!et.validate(z,L))return new Date(NaN);var nt=et.set(z,J,L);Array.isArray(nt)?(z=nt[0],m(J,nt[1])):z=nt}}catch(t){tt.e(t)}finally{tt.f()}return z}export{Go as p}; diff --git a/www/frigate-card/lang-it-0e2e946c.js b/www/frigate-card/lang-it-0e2e946c.js new file mode 100644 index 00000000..cce710f3 --- /dev/null +++ b/www/frigate-card/lang-it-0e2e946c.js @@ -0,0 +1 @@ +var e={frigate_card:"Frigate card",frigate_card_description:"Una scheda Lovelace per l'uso con Frigate",live:"Live",no_media:"Nessun contenuto multimediale da visualizzare",recordings:"Registrazioni",version:"Versione"},i={cameras:{camera_entity:"Entità della telecamera",dependencies:{all_cameras:"Mostra eventi per tutte le telecamere con questa telecamera",cameras:"Mostra eventi per telecamere specifiche con questa telecamera",editor_label:"Opzioni di dipendenza"},engines:{editor_label:"Opzioni del motore della fotocamera"},frigate:{camera_name:"Nome della telecamera frigate (autodificato dall'entità)",client_id:"ID client Frigate (per > 1 Frigate server)",editor_label:"Frigate Opzione",labels:"Etichette per fregate/filtri per oggetti",url:"Frigate URL del server",zones:"Frigate Zone"},go2rtc:{editor_label:"Opzioni go2rtc",modes:{editor_label:"Modalità go2rtc",mjpeg:"JPEG animato (MJPEG)",mp4:"MPEG-4 (MP4)",mse:"Estensioni sorgente multimediale (MSE)",webrtc:"Comunicazione Web in tempo reale (WebRTC)"},stream:"nome del flusso go2rtc"},hide:"Nascondi la videocamera dall'interfaccia utente",icon:"Icona per questa telecamera (Autoidentificato dall'entità)",id:"ID univoco per questa telecamera in questa carta",image:{editor_label:"Opzioni immagine",refresh_seconds:"Numero di secondi dopo i quali aggiornare l'immagine live (0=mai)",url:"URL dell'immagine da utilizzare al posto dell'istantanea dell'entità fotocamera"},live_provider:"Provider di visualizzazione dal vivo per questa telecamera",live_provider_options:{editor_label:"Opzioni del fornitore in tempo reale"},live_providers:{auto:"Automatica",go2rtc:"go2rtc",ha:"Streaming video di Home Assistant (ovvero HLS, LL-HLS, WebRTC tramite HA)",image:"Immagini Home Assistant",jsmpeg:"JSMpeg","webrtc-card":"Scheda WebRTC (ovvero la scheda WebRTC di Alexxit)"},motioneye:{editor_label:"Opzioni di MotionEye",images:{directory_pattern:"Modello di directory delle immagini",file_pattern:"Modello di file di immagini"},movies:{directory_pattern:"Modello di directory dei film",file_pattern:"Modello di file di film"},url:"URL dell'interfaccia utente di MotionEye"},title:"Titolo per questa telecamera (Autoidentificato dall'entità)",triggers:{editor_label:"Trigger Opzioni",entities:"Trigger da altre entità",motion:"Trigger rilevando automaticamente dal sensore di movimento",occupancy:"Attivare rilevando automatico tramite il sensore di presenza"},webrtc_card:{editor_label:"Opzioni della scheda WebRTC",entity:"Entità della telecamera della scheda WebRTC (non una telecamera Frigate)",url:"URL della telecamera della scheda WebRTC"}},common:{controls:{builtin:"",filter:{editor_label:"Filtro multimediale",mode:"Modalità filtro",modes:{left:"Filtro multimediale in un cassetto a sinistra",none:"Nessun filtro multimediale",right:"Filtro multimediale in un cassetto a destra"}},next_previous:{editor_label:"Successivo e precedente",size:"Successiva e Precedenti dimensioni di controllo nei pixel",style:"Stile di controllo successivo e precedente",styles:{chevrons:"Chevrons",icons:"Icone",none:"Nessuno",thumbnails:"Miniature"}},thumbnails:{editor_label:"Miniature",media:"Se mostrare miniature di clip o istantanee",medias:{clips:"Miniature di clip",snapshots:"Miniature istantanee"},mode:"Modalità miniatura",modes:{above:"Miniature sopra",below:"Miniature sotto",left:"Miniature in un cassetto a sinistra",none:"Nessuna miniatura",right:"Miniature in un cassetto a destra"},show_details:"Mostra i dettagli con le miniature",show_download_control:"Mostra il controllo del download sulle miniature",show_favorite_control:"Mostra il controllo preferito sulle miniature",show_timeline_control:"Mostra il controllo della sequenza temporale sulle miniature",size:"Dimensione delle miniature in pixel"},timeline:{editor_label:"Mini Cronologia",mode:"Modalità",modes:{above:"sopra",below:"sotto",none:"sessuna"}},title:{duration_seconds:"Secondi per visualizzare il titolo popup (0 = per sempre)",editor_label:"Controlli titolo popup",mode:"Modalità di visualizzazione del titolo",modes:{none:"Nessuna visualizzazione del titolo","popup-bottom-left":"Popup in basso a sinistra","popup-bottom-right":"Popup in basso a destra","popup-top-left":"Popup in alto a sinistra","popup-top-right":"Popup in alto a destra"}}},layout:{fit:"Adatta al layout",fits:{contain:"Il supporto è contenuto/in cassetta delle lettere",cover:"Il supporto si espande proporzionalmente per coprire la scheda",fill:"Il supporto viene allungato per riempire la scheda"},position:{x:"Percentuale di posizionamento orizzontale",y:"Percentuale di posizionamento verticale"}},media_action_conditions:{all:"Tutte le opportunità",hidden:"Sul browser/nascondere le schede",never:"Mai",selected:"Sulla selezione",unselected:"Sulla non selezione",visible:"Sul browser/visibilità della scheda"},timeline:{clustering_threshold:"Il conteggio degli eventi in cui sono raggruppati (0 = nessun clustering)",media:"I media vengono visualizzati la sequenza temporale",medias:{all:"Tutti i tipi di media",clips:"Clip",snapshots:"Istantanee"},show_recordings:"Mostra registrazioni",style:"",styles:{ribbon:"",stack:""},window_seconds:"La lunghezza predefinita della vista della sequenza temporale in secondi"}},dimensions:{aspect_ratio:"Proporzioni predefinite (ad es. '16:9')",aspect_ratio_mode:"Modalità proporzioni",aspect_ratio_modes:{dynamic:"Le proporzioni si adattano ai media",static:"Proporzioni statiche",unconstrained:"Proporzioni non vincolate"},max_height:"",min_height:""},image:{layout:"Disposizione dell'immagine",mode:"Modalità Visualizza immagine",modes:{camera:"Istantanea della telecamera di Home Assistant dell'entità telecamera",screensaver:"Logo Frigate incorporato",url:"Immagine arbitraria specificata dall'URL"},refresh_seconds:"Numero di secondi dopo i quali aggiornare (0 = mai)",url:"URL di immagine statica per la vista dell'immagine",zoomable:""},live:{auto_mute:"Muta automaticamente le telecamere in diretta",auto_pause:"Metti in pausa automaticamente le telecamere in diretta",auto_play:"Gioca automaticamente le telecamere dal vivo",auto_unmute:"Riattiva automaticamente l'audio delle telecamere live",controls:{editor_label:"Controlli dal vivo"},draggable:"Il Visualizzatore eventi può essere trascinato oppure puoi scorrere",layout:"Disposizione dal vivo",lazy_load:"Le telecamere dal vivo sono pigramente cariche",lazy_unload:"Le telecamere dal vivo sono pigramente non caricate",microphone:{always_connected:"",disconnect_seconds:"",editor_label:"",enabled:""},preload:"Precarica Live View in background",show_image_during_load:"Mostra un'immagine fissa durante il caricamento del live streaming",transition_effect:"Effetto di transizione della telecamera dal vivo",zoomable:""},media_viewer:{auto_mute:"Muta automaticamente i media",auto_pause:"Metti in Pausa automaticamente i media",auto_play:"Riproduci automaticamente i contenuti multimediali",auto_unmute:"Riattiva automaticamente i contenuti multimediali",controls:{editor_label:"Controlli di visualizzatore multimediale"},draggable:"Il visualizzatore multimediale può essere trascinato oppure può scorrere",layout:"Layout del visualizzatore multimediale",lazy_load:"Il media Viewer viene caricato pigramente nel carosello",transition_effect:"Effetto di transizione del visualizzatore multimediale",transition_effects:{none:"Nessuna transizione",slide:"Transizione diapositiva"},zoomable:""},menu:{alignment:"Allineamento dei menu",alignments:{bottom:"Allineato al fondo",left:"Allineato a sinistra",right:"Allineato a destra",top:"Allineato in cima"},button_size:"Dimensione del pulsante menu in pixel",buttons:{alignment:"Allineamento dei pulsanti",alignments:{matching:"Corrispondenza con l'allineamento del menu",opposing:"Contrastare l'allineamento del menu"},camera_ui:"Interfaccia utente della fotocamera",cameras:"Telecamere",clips:"Clip",download:"Download",enabled:"Pulsante abilitato",expand:"Espandere",frigate:"Frigate menu / Visualizzazione predefinita",fullscreen:"A schermo intero",icon:"Icona",image:"Immagine",live:"Abitare",media_player:"Invia a Media Player",mute:"",play:"",priority:"Priorità",screenshot:"",snapshots:"Istantanee",substreams:"Flusso/i secondario/i",timeline:"Timeline",type:"",types:{momentary:"",toggle:""}},position:"Posizione del menu",positions:{bottom:"Posizionato sul fondo",left:"Posizionato a sinistra",right:"Posizionato a destra",top:"Posizionato in alto"},style:"Stile menu",styles:{hidden:"Menu nascosto",hover:"Menu al passaggio del mouse",none:"Nessun menu",outside:"Menu esterno",overlay:"Menu di overlay"}},overrides:{info:"Questa configurazione della scheda ha specificato manualmente le sostituzioni configurate che possono sostituire i valori mostrati nell'editor visivo, consultare l'editor di codice per visualizzare/modificare queste sostituzioni"},performance:{features:{animated_progress_indicator:"Indicatore di avanzamento animato",editor_label:"Opzioni funzionalità",media_chunk_size:"Dimensione del blocco multimediale"},profile:"Profilo delle prestazioni",profiles:{high:"Prestazioni alte",low:"Prestazioni basse"},style:{border_radius:"Curve",box_shadow:"Ombre",editor_label:"Opzione di stile"},warning:"Questa scheda è in modalità basso profilo, quindi le impostazioni predefinite sono state modificate per ottimizzare le prestazioni"},view:{camera_select:"Visualizza per le telecamere appena selezionate",dark_mode:"Tema scuro",dark_modes:{auto:"auto",off:"Off",on:"On"},default:"Visualizzazione predefinita",scan:{enabled:"Modalità di scansione abilitata",scan_mode:"Modalità di scansione",show_trigger_status:"Mostra bordo pulsante quando attivato",untrigger_reset:"Reset the view to default after untrigger",untrigger_seconds:"Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione"},timeout_seconds:"Ripristina la vista predefinita x secondi dopo l'azione dell'utente (0 = mai)",update_cycle_camera:"Scorri le telecamere quando si aggiorna la visualizzazione predefinita",update_force:"Aggiornamenti della scheda forza (ignora l'interazione dell'utente)",update_seconds:"Aggiorna la visualizzazione predefinita ogni x secondi (0 = mai)",views:{clip:"Clip più recente",clips:"Galleria delle clip",current:"Vista corrente",image:"Immagine statica",live:"Dal vivo",snapshot:"Snapshot più recente",snapshots:"Galleria delle istantanee",timeline:"Vista della timeline"}}},a={add_new_camera:"Aggiungi nuova telecamera",button:"Pulsante",camera:"Telecamera",cameras:"Telecamere",cameras_secondary:"Quali telecamere visualizzare su questa card",delete:"Elimina",dimensions:"Dimensioni",dimensions_secondary:"Dimensioni e opzioni di forma",image:"Immagine",image_secondary:"Opzioni di visualizzazione dell'immagine statica",live:"Live",live_secondary:"Opzioni di visualizzazione della telecamera live",media_gallery:"Galleria multimediale",media_gallery_secondary:"Opzioni della galleria multimediale",media_viewer:"Visualizzatore dei media",media_viewer_secondary:"Visualizzatore per supporti statici (clip, istantanee o registrazioni)",menu:"Menu",menu_secondary:"Opzioni di aspetto e funzionalità del menu",move_down:"Sposta verso il basso",move_up:"Sposta verso l'alto",overrides:"La sovrascrittura è attiva",overrides_secondary:"Rilevate sovrascritture della configurazione dinamica",timeline:"Timeline",timeline_secondary:"Opzioni della timeline degli eventi",upgrade:"Aggiornamento",upgrade_available:"È disponibile un aggiornamento della configurazione della scheda automatica",view:"Visualizzazione",view_secondary:"Cosa dovrebbe mostrare la carta e come mostrarla"},o={ptz:{down:"Giù",home:"Home",left:"Sinistra",right:"Destra",up:"Su",zoom_in:"Ingrandire",zoom_out:"Zoom indietro"}},t={could_not_render_elements:"Impossibile renderizzare gli elementi dell'immagine",could_not_resolve:"Impossibile risolvere l'URL dei media",diagnostics:"Diagnostica delle carte.Si prega di rivedere per informazioni riservate prima di condividere",download_no_media:"Nessun media da scaricare",download_sign_failed:"Impossibile firmare URL multimediale per il download",duplicate_camera_id:"Duplicato ID dellla telecamera Frigate, utilizzare il parametro 'ID' per identificare in modo univoco le telecamere",empty_response:"Ricevuto risposta vuota da Home Assistant per la richiesta",failed_response:"Impossibile ricevere risposta da Home Assistant per la richiesta",failed_retain:"Impossibile conservare l'evento",failed_sign:"Impossibile firmare l'URL ad Home Assistant",image_load_error:"L'immagine non può essere caricata",invalid_configuration:"Configurazione non valida",invalid_configuration_no_hint:"Nessun suggerimento di posizione disponibile (tipo difettoso o mancante?)",invalid_elements_config:"Configurazione degli elementi di immagine non valida",invalid_response:"Ricevuta una risposta non valida da Home Assistant per la richiesta",jsmpeg_no_player:"Impossibile avviare JSMPEG Player",live_camera_no_endpoint:"Impossibile ottenere l'endpoint della videocamera per questo provider live (configurazione incompleta?)",live_camera_not_found:"La telecamera configurata non è stata trovata",live_camera_unavailable:"Telecamera non disponibile",no_camera_engine:"Impossibile determinare il motore adatto per la fotocamera",no_camera_entity:"Impossibile trovare l'entità fotocamera",no_camera_entity_for_triggers:"È necessaria un'entità telecamera per rilevare automaticamente i trigger",no_camera_id:"Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",no_camera_name:"Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",no_live_camera:"Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",no_visible_cameras:"Nessuna telecamera visibile trovata, è necessario configurare almeno una telecamera non nascosta",reconnecting:"Riconnessione",timeline_no_cameras:"Nessuna telecamera damostrare in Frigate nella timeline",troubleshooting:"Controllare la risoluzione dei problemi",too_many_automations:"",unknown:"Errore sconosciuto",upgrade_available:"È disponibile un aggiornamento di configurazione della scheda automatizzato, visitare l'editor di schede visive",webrtc_card_reported_error:"La scheda WebRTC ha riportato un errore",webrtc_card_waiting:"Aspettando che la scheda WebRTC si carichi ..."},n={camera:"Camera",duration:"Durata",in_progress:"In corso",score:"Punteggio",seek:"Cercare",start:"Avvia",what:"Che cosa",where:"Dove"},r={all:"Tutto",camera:"Telecamera",favorite:"Preferito",media_type:"Tipo di supporto",media_types:{clips:"Clip",recordings:"Registrazioni",snapshots:"Istantanee"},not_favorite:"Non preferito",select_camera:"Seleziona fotocamera...",select_favorite:"Seleziona preferito...",select_media_type:"Seleziona il tipo di supporto...",select_what:"Seleziona cosa...",select_when:"Seleziona quando...",select_where:"Seleziona dove...",tag:"Tag",what:"Che cosa",when:"Quando",whens:{past_month:"Mese scorso",past_week:"Settimana scorso",today:"Oggi",yesterday:"Ieri"},where:"Dove"},l={camera:"Camera",duration:"Durata",events:"Eventi",in_progress:"In corso",seek:"Cercare",start:"Inizio"},s={no_thumbnail:"Nessuna miniatura disponibile",retain_indefinitely:"L'evento sarà mantenuto indefinitamente",timeline:"Vedi evento nella timeline"},d={pan_behavior:{pan:"",seek:"","seek-in-media":""},select_date:"Scegli la data"},m={common:e,config:i,editor:a,elements:o,error:t,event:n,media_filter:r,recording:l,thumbnail:s,timeline:d};export{e as common,i as config,m as default,a as editor,o as elements,t as error,n as event,r as media_filter,l as recording,s as thumbnail,d as timeline}; diff --git a/www/frigate-card/lang-pt-BR-1648942c.js b/www/frigate-card/lang-pt-BR-1648942c.js new file mode 100644 index 00000000..560094c1 --- /dev/null +++ b/www/frigate-card/lang-pt-BR-1648942c.js @@ -0,0 +1 @@ +var e={frigate_card:"Cartão Frigate",frigate_card_description:"Um cartão da Lovelace para usar com Frigate",live:"Ao Vivo",no_media:"Nenhuma mídia para exibir",recordings:"Gravações",version:"Versão"},a={cameras:{camera_entity:"Entidade da Câmera",dependencies:{all_cameras:"Mostrar eventos para todas as câmeras nesta câmera",cameras:"Mostrar eventos para câmeras específicas nesta câmera",editor_label:"Opções de dependência"},engines:{editor_label:"Opções do motor da câmera"},frigate:{camera_name:"Nome da câmera do Frigate (detectado automaticamente pela entidade)",client_id:"ID do cliente do Frigate (para >1 servidor Frigate)",editor_label:"Opções do Frigate",labels:"Rótulos do Frigate/filtros de objetos",url:"URL do servidor Frigate",zones:"Zonas do Frigate"},go2rtc:{editor_label:"Opções do go2rtc",modes:{editor_label:"Modos do go2rtc",mjpeg:"Motion JPEG (MJPEG)",mp4:"MPEG-4 (MP4)",mse:"Media Source Extensions (MSE)",webrtc:"Web Real-Time Communication (WebRTC)"},stream:"Nome do stream do go2rtc"},hide:"Ocultar câmera da interface do usuário",icon:"Ícone para esta câmera (detectado automaticamente pela entidade)",id:"ID exclusivo para esta câmera nesse cartão",image:{editor_label:"Opções de Imagem",refresh_seconds:"Número de segundos após os quais atualizar a imagem ao vivo (0=nunca)",url:"URL da imagem para usar em vez do instantâneo da entidade da câmera"},live_provider:"Provedor de visualização ao vivo para esta câmera",live_provider_options:{editor_label:"Opções do provedor de visualização ao vivo"},live_providers:{auto:"Automatico",go2rtc:"go2rtc",ha:"Stream de vídeo do Home Assistant (ou seja, HLS, LL-HLS, WebRTC via HA)",image:"Imagens do Home Assistant",jsmpeg:"JSMpeg","webrtc-card":"Cartão WebRTC (de @AlexxIT)"},motioneye:{editor_label:"Opções do MotionEye",images:{directory_pattern:"Padrão de diretório de imagens",file_pattern:"Padrão de arquivo de imagens"},movies:{directory_pattern:"Padrão de diretório de filmes",file_pattern:"Padrão de arquivo de filmes"},url:"URL da interface de usuário do MotionEye"},title:"Título para esta câmera (detectado automaticamente pela entidade)",triggers:{editor_label:"Opções de acionamento",entities:"Acionar a partir de outras entidades",motion:"Acionar detectando automaticamente o sensor de movimento",occupancy:"Acionar detectando automaticamente o sensor de ocupação"},webrtc_card:{editor_label:"Opções do cartão WebRTC",entity:"Entidade de câmera de cartão WebRTC (não é uma câmera Frigate)",url:"URL da câmera do cartão WebRTC"}},common:{controls:{builtin:"",filter:{editor_label:"Filtro de Mídia",mode:"Modo do filtro",modes:{left:"Filtro de mídia em uma gaveta à esquerda",none:"Sem filtro de mídia",right:"Filtro de mídia em uma gaveta à direita"}},next_previous:{editor_label:"Próximo",size:"Tamanho de controle próximo e anterior",style:"Estilo do controle próximo e anterior",styles:{chevrons:"Setas",icons:"Ícones",none:"Nenhum",thumbnails:"Miniaturas"}},thumbnails:{editor_label:"Miniaturas",media:"Se deve mostrar miniaturas de clipes ou snapshots",medias:{clips:"Miniaturas de clipes",snapshots:"Miniaturas de Snapshots"},mode:"Modo de miniaturas",modes:{above:"Miniaturas acima da mídia",below:"Miniaturas abaixo da mídia",left:"Miniaturas em uma gaveta à esquerda",none:"Sem miniaturas",right:"Miniaturas em uma gaveta à direita"},show_details:"Mostrar detalhes com miniaturas",show_download_control:"Mostrar controle de download nas miniaturas",show_favorite_control:"Mostrar controle de favorito nas miniaturas",show_timeline_control:"Mostrar controle da linha do tempo nas miniaturas",size:"Tamanho das miniaturas em pixels"},timeline:{editor_label:"Controles da linha do tempo",mode:"Modo",modes:{above:"Acima",below:"Abaixo",none:"Nenhum"}},title:{duration_seconds:"Segundos para exibir o pop-up (0 = para sempre)",editor_label:"Controles do pop-up de título",mode:"Modo de exibição de título de mídia",modes:{none:"Sem exibição de título","popup-bottom-left":"Pop-up no canto inferior esquerdo","popup-bottom-right":"Pop-up no canto inferior direito","popup-top-left":"Pop-up no canto superior esquerdo","popup-top-right":"Pop-up no canto superior direito"}}},layout:{fit:"Ajuste de layout",fits:{contain:"A mídia é contida no cartão",cover:"A mídia se expande proporcionalmente para cobrir o cartão",fill:"A mídia é esticada para preencher o cartão"},position:{x:"Porcentagem do posicionamento horizontal",y:"Porcentagem do posicionamento vertical"}},media_action_conditions:{all:"Todas as oportunidades",hidden:"Ao ocultar o navegador/aba",never:"Nunca",selected:"Ao selecionar",unselected:"Ao desselecionar",visible:"Ao mostrar o navegador/aba"},timeline:{clustering_threshold:"A contagem de eventos nos quais eles são agrupados (0 = sem agrupamento)",media:"A mídia que a linha do tempo exibe",medias:{all:"Todos os tipos de mídia",clips:"Clipes",snapshots:"Instantâneos"},show_recordings:"Mostrar gravações",style:"",styles:{ribbon:"",stack:""},window_seconds:"A duração padrão da visualização da linha do tempo em segundos"}},dimensions:{aspect_ratio:"Proporção padrão (e.g. '16:9')",aspect_ratio_mode:"Modo de proporção",aspect_ratio_modes:{dynamic:"A proporção se ajusta à mídia",static:"Proporção estática",unconstrained:"Proporção irrestrita"},max_height:"",min_height:""},image:{layout:"Layout da imagem",mode:"Modo de visualização de imagem",modes:{camera:"Instantâneo da câmera do Home Assistant, da entidade de câmera",screensaver:"Logo Frigate embutido",url:"Imagem arbitrária especificada por URL"},refresh_seconds:"Número de segundos após o qual atualizar (0 = nunca)",url:"Imagem arbitrária especificada por URL",zoomable:""},live:{auto_mute:"Silenciar câmeras ao vivo automaticamente",auto_pause:"Pausar câmeras ao vivo automaticamente",auto_play:"Reproduzir câmeras ao vivo automaticamente",auto_unmute:"Ativar automaticamente o som das câmeras ao vivo",controls:{editor_label:"Controles da visualização ao vivo"},draggable:"A visualização ao vivo das câmeras pode ser arrastada/deslizada",layout:"Layout dinâmico",lazy_load:"As câmeras ao vivo são carregadas lentamente",lazy_unload:"As câmeras ao vivo são descarregadas preguiçosamente",microphone:{always_connected:"",disconnect_seconds:"",editor_label:"",enabled:""},preload:"Pré-carregar a visualização ao vivo em segundo plano",show_image_during_load:"Mostrar imagem estática enquanto a transmissão ao vivo está carregando",transition_effect:"Efeito de transição de câmera ao vivo",zoomable:""},media_viewer:{auto_mute:"Silenciar mídia automaticamente",auto_pause:"Pausar mídia automaticamente",auto_play:"Reproduzir mídia automaticamente",auto_unmute:"Ativar mídia automaticamente",controls:{editor_label:"Controles do visualizador de mídia"},draggable:"Visualizador de eventos pode ser arrastado/deslizado",layout:"Layout do visualizador de mídia",lazy_load:"A mídia do Visualizador de eventos é carregada lentamente no carrossel",snapshot_click_plays_clip:"Clicar em um instantâneo reproduz um clipe relacionado",transition_effect:"Efeito de transição do Visualizador de eventos",transition_effects:{none:"Sem transição",slide:"Transição de slides"},zoomable:""},menu:{alignment:"Alinhamento do menu",alignments:{bottom:"Alinhado à parte inferior",left:"Alinhado à esquerda",right:"Alinhado à direita",top:"Alinhado ao topo"},button_size:"Tamanho do botão de menu (e.g. '40px')",buttons:{alignment:"Alinhamento do botão",alignments:{matching:"Mesmo alinhamento do menu",opposing:"Opor-se ao alinhamento do menu"},camera_ui:"Interface de usuário da câmera",cameras:"Selecionar câmera",clips:"Clipes",download:"Baixe a mídia do evento",enabled:"Botão ativado",expand:"Expandir",frigate:"Frigate menu / Visualização padrão",fullscreen:"Tela cheia",icon:"Ícone",image:"Imagem",live:"Ao vivo",media_player:"Enviar para o reprodutor de mídia",mute:"",play:"",priority:"Prioridade",recordings:"Gravações",screenshot:"",snapshots:"Instantâneos",substreams:"Substream(s)",timeline:"Linha do tempo",type:"",types:{momentary:"",toggle:""}},position:"Posição do menu",positions:{bottom:"Posicionado na parte inferior",left:"Posicionado à esquerda",right:"Posicionado à direita",top:"Posicionado no topo"},style:"Estilo do menu",styles:{hidden:"Menu oculto",hover:"Menu suspenso","hover-card":"Menu suspenso (em todo o cartão)",none:"Sem menu",outside:"Menu externo",overlay:"Menu sobreposto"}},overrides:{info:"Esta configuração do cartão especificou manualmente as substituições configuradas que podem substituir os valores mostrados no editor visual, consulte o editor de código para visualizar/modificar essas substituições"},performance:{features:{animated_progress_indicator:"Indicador de Carregamento Animado",editor_label:"Opções de recursos",media_chunk_size:"Tamanho do bloco de mídia"},profile:"Perfil de desempenho",profiles:{high:"Alto desempenho/completo",low:"Baixo desempenho"},style:{border_radius:"Curvas",box_shadow:"Sombras",editor_label:"Opções de estilo"},warning:"Este cartão está no modo de baixo desempenho, então os padrões foram alterados para otimizar o desempenho"},view:{camera_select:"Visualização de câmeras recém-selecionadas",dark_mode:"Modo escuro",dark_modes:{auto:"Automático",off:"Desligado",on:"Ligado"},default:"Visualização padrão",scan:{enabled:"Modo scan ativado",scan_mode:"Modo scan",show_trigger_status:"Pulsar borda quando acionado",untrigger_reset:"Redefinir a visualização para o padrão após desacionar",untrigger_seconds:"Segundos após a mudar para o estado inativo para desacionar"},timeout_seconds:"Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",update_cycle_camera:"Percorrer as câmeras quando a visualização padrão for atualizada",update_force:"Forçar atualizações do cartão (ignore a interação do usuário)",update_seconds:"Atualize a visualização padrão a cada X segundos (0 = nunca)",views:{clip:"Clipe mais recente",clips:"Galeria de clipes",current:"Visualização atual",image:"Imagem estática",live:"Visualização ao vivo",recording:"Gravação mais recente",recordings:"Galeria de gravações",snapshot:"Snapshot mais recente",snapshots:"Galeria de Snapshots",timeline:"Visualização da linha do tempo"}}},o={add_new_camera:"Adicionar nova câmera",button:"Botão",camera:"Câmera",cameras:"Câmeras",cameras_secondary:"Quais câmeras renderizar neste cartão",delete:"Excluir",dimensions:"Dimensões",dimensions_secondary:"Dimensões e opções de forma",image:"Imagem",image_secondary:"Opções de visualização de imagem estática",live:"Ao vivo",live_secondary:"Opções de visualização da câmera ao vivo",media_gallery:"Galeria de mídia",media_gallery_secondary:"Opções da galeria de mídia",media_viewer:"Visualizador de eventos",media_viewer_secondary:"Opções do visualizador de Snapshots e clipes",menu:"Menu",menu_secondary:"Opções de aparência do menu",move_down:"Descer",move_up:"Subir",overrides:"As substituições estão ativas",overrides_secondary:"Substituições de configuração dinâmica detectadas",performance:"Desempenho",performance_secondary:"Opções de desempenho do cartão",timeline:"Linha do tempo",timeline_secondary:"Opções do evento da linha do tempo",upgrade:"Upgrade",upgrade_available:"Um upgrade automático da configuração de cartão está disponível",view:"Visualizar",view_secondary:"O que o cartão deve mostrar e como mostrá-lo"},i={ptz:{down:"Baixo",home:"Casa",left:"Esquerda",right:"Direita",up:"Cima",zoom_in:"Aumentar Zoom",zoom_out:"Reduzir Zoom"}},r={could_not_render_elements:"Não foi possível renderizar os elementos da imagem",could_not_resolve:"Não foi possível resolver o URL de mídia",diagnostics:"Diagnósticos do cartão. Revise as informações confidenciais antes de compartilhar",download_no_media:"Nenhuma mídia para download",download_sign_failed:"Não foi possível assinar o URL de mídia para download",duplicate_camera_id:"Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",empty_response:"Sem resposta do Home Assistant para a solicitação",failed_response:"Falha ao receber resposta do Home Assistant para solicitação",failed_retain:"Não foi possível reter o evento",failed_sign:"Não foi possível assinar a URL do Home Assistant",image_load_error:"A imagem não pôde ser carregada",invalid_configuration:"Configuração inválida",invalid_configuration_no_hint:"Nenhuma dica de local disponível (tipo incorreto ou ausente?)",invalid_elements_config:"Configuração de elementos de imagem inválida",invalid_response:"Resposta inválida recebida do Home Assistant para a solicitação",jsmpeg_no_player:"Não foi possível iniciar o player JSMPEG",live_camera_no_endpoint:"Não foi possível obter o endereço da câmera para este provedor ao vivo (configuração incompleta?)",live_camera_not_found:"A entidade de câmera configurada não foi encontrada",live_camera_unavailable:"Câmera indisponível",no_camera_engine:"Não foi possível determinar o motor adequado para a câmera",no_camera_entity:"Não foi possível encontrar a entidade da câmera",no_camera_entity_for_triggers:"Uma entidade de câmera é necessária para detectar automaticamente os gatilhos",no_camera_id:"Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",no_camera_name:"Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",no_live_camera:"O parâmetro camera_entity deve ser definido e válido para este provedor ativo",no_visible_cameras:"Nenhuma câmera visível encontrada, você deve configurar pelo menos uma câmera não oculta",reconnecting:"Reconectando",timeline_no_cameras:"Nenhuma câmera do Frigate para mostrar na linha do tempo",too_many_automations:"",troubleshooting:"Verifique a solução de problemas",unknown:"Erro desconhecido",upgrade_available:"Uma atualização automatizada da configuração do cartão está disponível, visite o editor visual do cartão",webrtc_card_reported_error:"O cartão WebRTC relatou um erro",webrtc_card_waiting:"Aguardando o cartão WebRTC carregar ..."},t={camera:"Câmera",duration:"Duração",in_progress:"Em andamento",score:"Pontuação",seek:"Procurar",start:"Início",tag:"Etiqueta",what:"O que",where:"Onde"},s={all:"Todos",camera:"Câmera",favorite:"Favorito",media_type:"Tipo de mídia",media_types:{clips:"Clipes",recordings:"Gravações",snapshots:"Instantâneos"},not_favorite:"Não favorito",select_camera:"Selecione a câmera...",select_favorite:"Selecione favorito...",select_media_type:"Selecione o tipo de mídia...",select_tag:"Selecione a etiqueta...",select_what:"Selecione o que...",select_when:"Selecione quando...",select_where:"Selecione onde...",tag:"Etiqueta",what:"O que",when:"Quando",whens:{past_month:"Mês passado",past_week:"Semana passada",today:"Hoje",yesterday:"Ontem"},where:"Onde"},d={camera:"Câmera",duration:"Duração",events:"Eventos",in_progress:"Em andamento",seek:"Procurar",start:"Começar"},n={download:"Baixar mídia",no_thumbnail:"Nenhuma miniatura disponível",retain_indefinitely:"Evento será retido por tempo indeterminado",timeline:"Ver evento na linha do tempo"},m={pan_behavior:{pan:"",seek:"","seek-in-media":""},select_date:"Escolha a data"},c={common:e,config:a,editor:o,elements:i,error:r,event:t,media_filter:s,recording:d,thumbnail:n,timeline:m};export{e as common,a as config,c as default,o as editor,i as elements,r as error,t as event,s as media_filter,d as recording,n as thumbnail,m as timeline}; diff --git a/www/frigate-card/lang-pt-PT-440b6dfd.js b/www/frigate-card/lang-pt-PT-440b6dfd.js new file mode 100644 index 00000000..fd2ee7ab --- /dev/null +++ b/www/frigate-card/lang-pt-PT-440b6dfd.js @@ -0,0 +1 @@ +var e={frigate_card:"Cartão Frigate",frigate_card_description:"Um cartão da Lovelace para usar com Frigate",live:"Ao Vivo",no_media:"Sem média",recordings:"Gravações",version:"Versão"},a={cameras:{camera_entity:"Entidade da Câmera",dependencies:{all_cameras:"Mostrar eventos para todas as câmeras nesta câmera",cameras:"Mostrar eventos para câmeras específicas nesta câmera",editor_label:"Opções de dependência"},engines:{editor_label:"Editor de etiquetas"},frigate:{camera_name:"Nome da câmera do Frigate (detectado automaticamente pela entidade)",client_id:"ID do cliente do Frigate (para >1 servidor Frigate)",editor_label:"Opções do Frigate",labels:"Etiquetas",url:"URL do servidor Frigate",zones:"Zonas"},go2rtc:{editor_label:"Editor de etiquetas",modes:{editor_label:"Editor de etiquetas",mjpeg:"Mjpeg",mp4:"Mp4",mse:"Mse",webrtc:"Webrtc"},stream:"Stream"},hide:"Esconder",icon:"Ícone para esta câmera (detectado automaticamente pela entidade)",id:"ID exclusivo para esta câmera nesse cartão",image:{editor_label:"Editor etiquetas",refresh_seconds:"Atualizar em segundos",url:"Link"},live_provider:"Fonte de visualização ao vivo para esta câmera",live_provider_options:{editor_label:"Editor de etiquetas"},live_providers:{auto:"Automatico",go2rtc:"Go2rtc",ha:"Ha",image:"Imagem",jsmpeg:"JSMpeg","webrtc-card":"Cartão WebRTC (de @AlexxIT)"},motioneye:{editor_label:"Directoria pre-definido",images:{directory_pattern:"Directoria pre-definido",file_pattern:"Ficheiro pre-definido"},movies:{directory_pattern:"Directoria pre-definida",file_pattern:"Ficheiro pre-definido"},url:"Link"},title:"Título para esta câmera (detectado automaticamente pela entidade)",triggers:{editor_label:"Opções de activação",entities:"Activar a partir de outras entidades",motion:"Activar detectando automaticamente o sensor de movimento",occupancy:"Activar detectando automaticamente o sensor de ocupação"},webrtc_card:{editor_label:"Opções do cartão WebRTC",entity:"Entidade de câmera de cartão WebRTC (não é uma câmera Frigate)",url:"URL da câmera do cartão WebRTC"}},common:{controls:{builtin:"",filter:{editor_label:"Editor de titulos",mode:"Modo",modes:{left:"Esquerda",none:"Nenhum",right:"Direita"}},next_previous:{editor_label:"Editor de titulos",size:"Tamanho de controle próximo e anterior",style:"Estilo do controle próximo e anterior",styles:{chevrons:"Setas",icons:"Ícones",none:"Nenhum",thumbnails:"Miniaturas"}},thumbnails:{editor_label:"Editor de titulos",media:"Mostrar miniaturas de clipes ou snapshots",medias:{clips:"Miniaturas de clipes",snapshots:"Miniaturas de Snapshots"},mode:"Modos",modes:{above:"Miniaturas acima da mídia",below:"Miniaturas abaixo da mídia",left:"Miniaturas em uma gaveta à esquerda",none:"Sem miniaturas",right:"Miniaturas em uma gaveta à direita"},show_details:"Mostrar detalhes",show_download_control:"Mostrar o botão de download",show_favorite_control:"Mostrar o botão de favorito nas miniaturas",show_timeline_control:"Mostrar a linha do tempo nas miniaturas",size:"Tamanho das miniaturas em pixels"},timeline:{editor_label:"Controles de linha do tempo",mode:"Modo",modes:{above:"Por cima",below:"Abaixo",none:"Nenhum"}},title:{duration_seconds:"Segundos para exibir o pop-up (0 = para sempre)",editor_label:"Editor de titulos",mode:"Modo de exibição de título de mídia",modes:{none:"Sem exibição de título","popup-bottom-left":"Pop-up no canto inferior esquerdo","popup-bottom-right":"Pop-up no canto inferior direito","popup-top-left":"Pop-up no canto superior esquerdo","popup-top-right":"Pop-up no canto superior direito"}}},layout:{fit:"Fit",fits:{contain:"Conter",cover:"Tapar",fill:"Preencher"},position:{x:"Percentagem da localização horizontal",y:"Percentagem da localização vertical"}},media_action_conditions:{all:"Todas as oportunidades",hidden:"Ao ocultar o navegador/aba",never:"Nunca",selected:"Ao selecionar",unselected:"Ao desselecionar",visible:"Ao mostrar o navegador/aba"},timeline:{clustering_threshold:"A contagem de eventos nos quais eles são agrupados (0 = sem agrupamento)",media:"A mídia que a linha do tempo exibe",medias:{all:"Todos os tipos de mídia",clips:"Clipes",snapshots:"Instantâneos"},show_recordings:"Mostrar gravações",window_seconds:"A duração padrão da visualização da linha do tempo em segundos"}},dimensions:{aspect_ratio:"Proporção padrão (e.g. '16:9')",aspect_ratio_mode:"Modo de proporção",aspect_ratio_modes:{dynamic:"A proporção se ajusta à mídia",static:"Proporção estática",unconstrained:"Proporção irrestrita"}},image:{layout:"Layout",mode:"Modo de visualização de imagem",modes:{camera:"Instantâneo da câmera do Home Assistant, da entidade de câmera",screensaver:"Logo Frigate embutido",url:"Imagem arbitrária especificada por URL"},refresh_seconds:"Número de segundos após o qual atualizar (0 = nunca)",url:"Imagem arbitrária especificada por URL",zoomable:""},live:{auto_mute:"Silenciar câmeras ao vivo automaticamente",auto_pause:"Parar câmeras ao vivo automaticamente",auto_play:"Reproduzir câmeras ao vivo automaticamente",auto_unmute:"Ativar automaticamente o som das câmeras ao vivo",controls:{editor_label:"Controles da visualização ao vivo"},draggable:"A visualização ao vivo das câmeras pode ser arrastada/deslizada",layout:"layout",lazy_load:"As câmeras ao vivo são carregadas lentamente",lazy_unload:"As câmeras ao vivo são descarregadas preguiçosamente",microphone:{always_connected:"",disconnect_seconds:"",editor_label:"",enabled:""},preload:"Pré-carregar a visualização ao vivo em segundo plano",show_image_during_load:"Mostar imagem durante o carregamento",transition_effect:"Efeito de transição de câmera ao vivo",zoomable:""},media_viewer:{auto_mute:"Silenciar mídia automaticamente",auto_pause:"Parar mídia automaticamente",auto_play:"Reproduzir mídia automaticamente",auto_unmute:"Ativar mídia automaticamente",controls:{editor_label:"Controles do visualizador de mídia"},draggable:"Visualizador de eventos pode ser arrastado/deslizado",layout:"Layout",lazy_load:"A mídia do Visualizador de eventos é carregada lentamente no carrossel",transition_effect:"Efeito de transição do Visualizador de eventos",transition_effects:{none:"Sem transição",slide:"Transição de slides"},zoomable:""},menu:{alignment:"Alinhamento do menu",alignments:{bottom:"Alinhado à parte inferior",left:"Alinhado à esquerda",right:"Alinhado à direita",top:"Alinhado ao topo"},button_size:"Tamanho do botão de menu (e.g. '40px')",buttons:{alignment:"Alinhamento do botão",alignments:{matching:"Mesmo alinhamento do menu",opposing:"Opor-se ao alinhamento do menu"},camera_ui:"Camera",cameras:"Selecionar câmera",clips:"Clipes",download:"Descarregar mídia do evento",enabled:"Botão ativado",expand:"Expandir",frigate:"Frigate menu / Visualização padrão",fullscreen:"Tela cheia",icon:"Ícone",image:"Imagem",live:"Ao vivo",media_player:"Enviar para o reprodutor de mídia",mute:"",play:"",priority:"Prioridade",screenshot:"",snapshots:"Instantâneos",substreams:"substreams",timeline:"Linha do tempo",type:"",types:{momentary:"",toggle:""}},position:"Posição do menu",positions:{bottom:"Posicionado na parte inferior",left:"Posicionado à esquerda",right:"Posicionado à direita",top:"Posicionado no topo"},style:"Estilo do menu",styles:{hidden:"Menu oculto",hover:"Menu suspenso",none:"Sem menu",outside:"Menu externo",overlay:"Menu sobreposto"}},overrides:{info:"Esta configuração do cartão especificou manualmente as substituições configuradas que podem substituir os valores mostrados no editor visual, consulte o editor de código para visualizar/modificar essas substituições"},performance:{features:{animated_progress_indicator:"Animação na barra de progresso",editor_label:"Editor de etiquetas",media_chunk_size:"Tamanho do ficheiro"},profile:"Perfil",profiles:{high:"Alto",low:"Baixo"},style:{border_radius:"Tamanho do bordo",box_shadow:"Caixa de Fundo",editor_label:"Editor de etiquetas"},warning:"Avisos"},view:{camera_select:"Visualização de câmeras recém-selecionadas",dark_mode:"Modo escuro",dark_modes:{auto:"Automático",off:"Desligado",on:"Ligado"},default:"Visualização padrão",scan:{enabled:"Modo scan ativado",scan_mode:"Modo scan",show_trigger_status:"Exibir estado do gatilho",untrigger_reset:"Redefinir a visualização para o padrão após desacionar",untrigger_seconds:"Segundos após a mudar para o estado inativo para desacionar"},timeout_seconds:"Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",update_cycle_camera:"Percorrer as câmeras quando a visualização padrão for atualizada",update_force:"Forçar atualizações do cartão (ignore a interação do Utilizador)",update_seconds:"Atualize a visualização padrão a cada X segundos (0 = nunca)",views:{clip:"Clipe mais recente",clips:"Galeria de clipes",current:"Visualização atual",image:"Imagem estática",live:"Visualização ao vivo",snapshot:"Snapshot mais recente",snapshots:"Galeria de Snapshots",timeline:"Visualização da linha do tempo"}}},o={add_new_camera:"Adicionar nova câmera",button:"Botão",camera:"Câmera",cameras:"Câmeras",cameras_secondary:"Câmeras para renderizar neste cartão",delete:"Excluir",dimensions:"Dimensões",dimensions_secondary:"Dimensões e opções de forma",image:"Imagem",image_secondary:"Opções de visualização de imagem estática",live:"Ao vivo",live_secondary:"Opções de visualização da câmera ao vivo",media_gallery:"Galeria",media_gallery_secondary:"Galeria Secundaria",media_viewer:"Visualizador de eventos",media_viewer_secondary:"Opções do visualizador de Snapshots e clipes",menu:"Menu",menu_secondary:"Opções de aparência do menu",move_down:"Descer",move_up:"Subir",overrides:"As substituições estão ativas",overrides_secondary:"Substituições de configuração dinâmica detectadas",timeline:"Linha do tempo",timeline_secondary:"Opções do evento da linha do tempo",upgrade:"Actualização",upgrade_available:"Está disponível uma atualização automática do cartão",view:"Visualizar",view_secondary:"O que deve ser mostrado neste cartão"},i={ptz:{down:"Baixo",home:"Origem",left:"Esquerda",right:"Direira",up:"Cima",zoom_in:"Ampliar",zoom_out:"Reduzir"}},r={could_not_render_elements:"Não foi possível renderizar os elementos da imagem",could_not_resolve:"Não foi possível resolver o URL de mídia",diagnostics:"Diagnósticos do cartão. Reveja as informações confidenciais antes de partilhar",download_no_media:"Nenhuma mídia para download",download_sign_failed:"Não foi possível assinar o URL de mídia para download",duplicate_camera_id:"Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",empty_response:"Sem resposta do Home Assistant para a solicitação",failed_response:"Falha ao receber resposta do Home Assistant para solicitação",failed_retain:"Não foi possível reter o evento",failed_sign:"Não foi possível assinar a URL do Home Assistant",image_load_error:"A imagem não pôde ser carregada",invalid_configuration:"Configuração inválida",invalid_configuration_no_hint:"Nenhuma dica de local disponível (tipo incorreto ou ausente?)",invalid_elements_config:"Configuração de elementos de imagem inválida",invalid_response:"Resposta inválida recebida do Home Assistant para a solicitação",jsmpeg_no_player:"Não foi possível iniciar o player JSMPEG",live_camera_no_endpoint:"Nenhuma câmera ao vivo",live_camera_not_found:"Nenhuma câmera ao vivo não foi encontrada",live_camera_unavailable:"Câmera ao vivo indisponivel",no_camera_engine:"Não existe câmera",no_camera_entity:"Não existe uma entidade câmera",no_camera_entity_for_triggers:"Não existe camera para a acção",no_camera_id:"Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",no_camera_name:"Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",no_live_camera:"O parâmetro camera_entity deve ser definido e válido para este serviço ativo",no_visible_cameras:"Sem camaras visiveis",reconnecting:"A voltar a ligar",timeline_no_cameras:"Nenhuma câmera do Frigate para mostrar na linha do tempo",troubleshooting:"Verifique a solução de problemas",too_many_automations:"",unknown:"Erro desconhecido",upgrade_available:"Uma atualização automatizada da configuração do cartão está disponível, visite o editor visual do cartão",webrtc_card_reported_error:"O cartão WebRTC relatou um erro",webrtc_card_waiting:"Aguardar o cartão WebRTC carregar ..."},t={camera:"Camera",duration:"Duração",in_progress:"Em andamento",score:"Pontuação",seek:"Procurar",start:"Início",what:"O quê",where:"Onde"},s={all:"Todos",camera:"Camera",favorite:"Favoritos",media_type:"Tipos de media",media_types:{clips:"Clips",recordings:"Gravações",snapshots:"Imagens"},not_favorite:"Não favorito",select_camera:"Seleciona a camara",select_favorite:"Seleciona o favorito",select_media_type:"Seleciona o tipo de media",select_what:"Seleciona",select_when:"Seleciona quando",select_where:"Seleciona onde",what:"O que",when:"Quando",whens:{past_month:"O mes passado",past_week:"A semana passada",today:"Hoje",yesterday:"Ontem"},where:"Onde"},n={camera:"Camera",duration:"Duração",events:"Eventos",in_progress:"Em andamento",seek:"Procurar",start:"Começar"},d={no_thumbnail:"Nenhuma miniatura disponível",retain_indefinitely:"Evento será retido por tempo indeterminado",timeline:"Ver evento na linha do tempo"},m={pan_behavior:{pan:"Pan",seek:"Pan seeks across all media","seek-in-media":"Pan seeks within selected media item only"},select_date:"Selecionar a data"},l={common:e,config:a,editor:o,elements:i,error:r,event:t,media_filter:s,recording:n,thumbnail:d,timeline:m};export{e as common,a as config,l as default,o as editor,i as elements,r as error,t as event,s as media_filter,n as recording,d as thumbnail,m as timeline}; diff --git a/www/frigate-card/lazyload-c2d6254a.js b/www/frigate-card/lazyload-c2d6254a.js new file mode 100644 index 00000000..10bb306d --- /dev/null +++ b/www/frigate-card/lazyload-c2d6254a.js @@ -0,0 +1,44 @@ +import{cH as t,cI as a,cW as e,cX as o,bk as n,bl as i,bm as d,bn as r,s,cY as l,y as c,o as u,cZ as h,bj as p,cP as m,c_ as g,cN as b,c$ as v,cS as f,d0 as y,d1 as _,d2 as C}from"./card-555679fd.js"; +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const x={},z=t(class extends a{constructor(){super(...arguments),this.ot=x}render(t,a){return a()}update(t,[a,o]){if(Array.isArray(a)){if(Array.isArray(this.ot)&&this.ot.length===a.length&&a.every(((t,a)=>t===this.ot[a])))return e}else if(this.ot===a)return e;return this.ot=Array.isArray(a)?Array.from(a):a,this.render(a,o)}}),P=2,S=(t,a)=>{t._controlsHideTimer&&(t._controlsHideTimer.stop(),delete t._controlsHideTimer),t.controls=a},A=(t,a=1)=>{const e=t.controls;S(t,!1),t._controlsHideTimer??=new o,t._controlsHideTimer.start(a,(()=>{S(t,e)}))},L=async(t,a)=>{if(a?.play)try{await a.play()}catch(e){if("NotAllowedError"===e.name&&!t.isMuted()){await t.mute();try{await a.play()}catch(t){}}}};let k=class extends s{constructor(){super(...arguments),this.disabled=!1,this.label="",this._embedThumbnailTask=l(this,(()=>this.hass),(()=>this.thumbnail))}set controlConfig(t){t?.size&&this.style.setProperty("--frigate-card-next-prev-size",`${t.size}px`),this._controlConfig=t}render(){if(this.disabled||!this._controlConfig||"none"==this._controlConfig.style)return c``;const t={controls:!0,previous:"previous"===this.direction,next:"next"===this.direction,thumbnails:"thumbnails"===this._controlConfig.style,icons:["chevrons","icons"].includes(this._controlConfig.style),button:["chevrons","icons"].includes(this._controlConfig.style)};if(["chevrons","icons"].includes(this._controlConfig.style)){let a;if("chevrons"===this._controlConfig.style)a="previous"===this.direction?"mdi:chevron-left":"mdi:chevron-right";else{if(!this.icon)return c``;a=this.icon}return c` + + `}return this.thumbnail?h(this,this._embedThumbnailTask,(a=>a?c``:c``),{inProgressFunc:()=>c`
`}):c``}static get styles(){return p("ha-icon-button.button {\n color: var(--secondary-color, white);\n background-color: rgba(0, 0, 0, 0.6);\n border-radius: 50%;\n padding: 0px;\n margin: 3px;\n --ha-icon-display: block;\n /* Buttons can always be clicked */\n pointer-events: auto;\n opacity: 0.9;\n}\n\n@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n 100% {\n opacity: 1;\n }\n}\nha-icon[data-domain=alert][data-state=on],\nha-icon[data-domain=automation][data-state=on],\nha-icon[data-domain=binary_sensor][data-state=on],\nha-icon[data-domain=calendar][data-state=on],\nha-icon[data-domain=camera][data-state=streaming],\nha-icon[data-domain=cover][data-state=open],\nha-icon[data-domain=fan][data-state=on],\nha-icon[data-domain=humidifier][data-state=on],\nha-icon[data-domain=light][data-state=on],\nha-icon[data-domain=input_boolean][data-state=on],\nha-icon[data-domain=lock][data-state=unlocked],\nha-icon[data-domain=media_player][data-state=on],\nha-icon[data-domain=media_player][data-state=paused],\nha-icon[data-domain=media_player][data-state=playing],\nha-icon[data-domain=script][data-state=on],\nha-icon[data-domain=sun][data-state=above_horizon],\nha-icon[data-domain=switch][data-state=on],\nha-icon[data-domain=timer][data-state=active],\nha-icon[data-domain=vacuum][data-state=cleaning],\nha-icon[data-domain=group][data-state=on],\nha-icon[data-domain=group][data-state=home],\nha-icon[data-domain=group][data-state=open],\nha-icon[data-domain=group][data-state=locked],\nha-icon[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=pending],\nha-icon[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon[data-domain=plant][data-state=problem],\nha-icon[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\nha-icon-button[data-domain=alert][data-state=on],\nha-icon-button[data-domain=automation][data-state=on],\nha-icon-button[data-domain=binary_sensor][data-state=on],\nha-icon-button[data-domain=calendar][data-state=on],\nha-icon-button[data-domain=camera][data-state=streaming],\nha-icon-button[data-domain=cover][data-state=open],\nha-icon-button[data-domain=fan][data-state=on],\nha-icon-button[data-domain=humidifier][data-state=on],\nha-icon-button[data-domain=light][data-state=on],\nha-icon-button[data-domain=input_boolean][data-state=on],\nha-icon-button[data-domain=lock][data-state=unlocked],\nha-icon-button[data-domain=media_player][data-state=on],\nha-icon-button[data-domain=media_player][data-state=paused],\nha-icon-button[data-domain=media_player][data-state=playing],\nha-icon-button[data-domain=script][data-state=on],\nha-icon-button[data-domain=sun][data-state=above_horizon],\nha-icon-button[data-domain=switch][data-state=on],\nha-icon-button[data-domain=timer][data-state=active],\nha-icon-button[data-domain=vacuum][data-state=cleaning],\nha-icon-button[data-domain=group][data-state=on],\nha-icon-button[data-domain=group][data-state=home],\nha-icon-button[data-domain=group][data-state=open],\nha-icon-button[data-domain=group][data-state=locked],\nha-icon-button[data-domain=group][data-state=problem] {\n color: var(--paper-item-icon-active-color, #fdd835);\n}\n\nha-icon-button[data-domain=climate][data-state=cooling] {\n color: var(--cool-color, var(--state-climate-cool-color));\n}\n\nha-icon-button[data-domain=climate][data-state=heating] {\n color: var(--heat-color, var(--state-climate-heat-color));\n}\n\nha-icon-button[data-domain=climate][data-state=drying] {\n color: var(--dry-color, var(--state-climate-dry-color));\n}\n\nha-icon-button[data-domain=alarm_control_panel] {\n color: var(--alarm-color-armed, var(--label-badge-red));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=disarmed] {\n color: var(--alarm-color-disarmed, var(--label-badge-green));\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=pending],\nha-icon-button[data-domain=alarm_control_panel][data-state=arming] {\n color: var(--alarm-color-pending, var(--label-badge-yellow));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=alarm_control_panel][data-state=triggered] {\n color: var(--alarm-color-triggered, var(--label-badge-red));\n animation: pulse 1s infinite;\n}\n\nha-icon-button[data-domain=plant][data-state=problem],\nha-icon-button[data-domain=zwave][data-state=dead] {\n color: var(--state-icon-error-color);\n}\n\n/* Color the icon if unavailable */\nha-icon-button[data-state=unavailable] {\n color: var(--state-unavailable-color);\n}\n\n:host {\n --frigate-card-next-prev-size: 48px;\n --frigate-card-next-prev-size-hover: calc(var(--frigate-card-next-prev-size) * 2);\n --frigate-card-prev-position: 45px;\n --frigate-card-next-position: 45px;\n --mdc-icon-button-size: var(--frigate-card-next-prev-size);\n --mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);\n}\n\n.controls {\n position: absolute;\n z-index: 1;\n overflow: hidden;\n}\n\n.controls.previous {\n left: var(--frigate-card-prev-position);\n}\n\n.controls.next {\n right: var(--frigate-card-next-position);\n}\n\n.controls.icons {\n top: calc(50% - var(--frigate-card-next-prev-size) / 2);\n}\n\n.controls.thumbnails {\n border-radius: 50%;\n height: var(--frigate-card-next-prev-size);\n top: calc(50% - var(--frigate-card-next-prev-size) / 2);\n box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 20px 5px black);\n transition: all 0.2s ease-out;\n opacity: 0.8;\n aspect-ratio: 1/1;\n}\n\n.controls.thumbnails:hover {\n opacity: 1 !important;\n height: var(--frigate-card-next-prev-size-hover);\n top: calc(50% - var(--frigate-card-next-prev-size-hover) / 2);\n}\n\n.controls.previous.thumbnails:hover {\n left: calc(var(--frigate-card-prev-position) - (var(--frigate-card-next-prev-size-hover) - var(--frigate-card-next-prev-size)) / 2);\n}\n\n.controls.next.thumbnails:hover {\n right: calc(var(--frigate-card-next-position) - (var(--frigate-card-next-prev-size-hover) - var(--frigate-card-next-prev-size)) / 2);\n}")}};n([i({attribute:!1})],k.prototype,"direction",void 0),n([i({attribute:!1})],k.prototype,"hass",void 0),n([d()],k.prototype,"_controlConfig",void 0),n([i({attribute:!1})],k.prototype,"thumbnail",void 0),n([i({attribute:!1})],k.prototype,"icon",void 0),n([i({attribute:!0,type:Boolean})],k.prototype,"disabled",void 0),n([i()],k.prototype,"label",void 0),k=n([r("frigate-card-next-previous-control")],k);const $=(t,a)=>{var e,o;a.stopPropagation(),e=a.composedPath()[0],o={slide:t,mediaLoadedInfo:a.detail},_(e,"carousel:media:loaded",o)},H=(t,a)=>{var e;a.stopPropagation(),e=a.composedPath()[0],_(e,"carousel:media:unloaded",{slide:t})};let M=class extends s{constructor(){super(),this.selected=0,this._mediaLoadedInfo={},this._nextControlRef=m(),this._previousControlRef=m(),this._titleControlRef=m(),this._titleTimer=new o,this._boundAutoPlayHandler=this.autoPlay.bind(this),this._boundAutoUnmuteHandler=this.autoUnmute.bind(this),this._boundTitleHandler=this._titleHandler.bind(this),this._debouncedAdaptContainerHeightToSlide=g(this._adaptContainerHeightToSlide.bind(this),100,{trailing:!0}),this._refCarousel=m(),this._slideResizeObserver=new ResizeObserver(this._reInitAndAdjustHeight.bind(this)),this._intersectionObserver=new IntersectionObserver(this._intersectionHandler.bind(this))}frigateCardCarousel(){return this._refCarousel.value??null}_getAutoMediaPlugin(){return this.frigateCardCarousel()?.carousel()?.plugins().autoMedia??null}autoPlay(){const t=this._getAutoMediaPlugin()?.options;t?.autoPlayCondition&&["all","selected"].includes(t?.autoPlayCondition)&&this._getAutoMediaPlugin()?.play()}autoPause(){const t=this._getAutoMediaPlugin()?.options;t?.autoPauseCondition&&["all","selected"].includes(t.autoPauseCondition)&&this._getAutoMediaPlugin()?.pause()}autoUnmute(){const t=this._getAutoMediaPlugin()?.options;t?.autoUnmuteCondition&&["all","selected"].includes(t?.autoUnmuteCondition)&&this._getAutoMediaPlugin()?.unmute()}autoMute(){const t=this._getAutoMediaPlugin()?.options;t?.autoMuteCondition&&["all","selected"].includes(t?.autoMuteCondition)&&this._getAutoMediaPlugin()?.mute()}_titleHandler(){const t=()=>{this._titleTimer.stop(),this._titleControlRef.value?.show()};this._titleControlRef.value?.isVisible()&&t(),this._titleTimer.start(.5,t)}connectedCallback(){super.connectedCallback(),this.addEventListener("frigate-card:media:loaded",this._boundAutoPlayHandler),this.addEventListener("frigate-card:media:loaded",this._boundAutoUnmuteHandler),this.addEventListener("frigate-card:media:loaded",this._debouncedAdaptContainerHeightToSlide),this.addEventListener("frigate-card:media:loaded",this._boundTitleHandler),this._intersectionObserver.observe(this)}disconnectedCallback(){this.removeEventListener("frigate-card:media:loaded",this._boundAutoPlayHandler),this.removeEventListener("frigate-card:media:loaded",this._boundAutoUnmuteHandler),this.removeEventListener("frigate-card:media:loaded",this._debouncedAdaptContainerHeightToSlide),this.removeEventListener("frigate-card:media:loaded",this._boundTitleHandler),this._intersectionObserver.disconnect(),this._mediaLoadedInfo={},super.disconnectedCallback()}_reInitAndAdjustHeight(){this.frigateCardCarousel()?.carouselReInitWhenSafe(),this._debouncedAdaptContainerHeightToSlide()}_intersectionHandler(t){t.some((t=>t.isIntersecting))&&this._reInitAndAdjustHeight()}_adaptContainerHeightToSlide(){const t=this.frigateCardCarousel()?.getCarouselSelected();if(t){this.style.removeProperty("max-height");const a=t.element.getBoundingClientRect().height;void 0!==a&&a>0&&(this.style.maxHeight=`${a}px`)}}_dispatchMediaLoadedInfo(t){const a=t.index;void 0!==a&&a in this._mediaLoadedInfo&&b(this,this._mediaLoadedInfo[a])}_storeMediaLoadedInfo(t){t.stopPropagation();const a=t.detail.mediaLoadedInfo,e=t.detail.slide;a&&v(a)&&(this._mediaLoadedInfo[e]=a,this.frigateCardCarousel()?.getCarouselSelected()?.index===e&&b(this,a))}_removeMediaLoadedInfo(t){const a=t.detail.slide;delete this._mediaLoadedInfo[a],this.frigateCardCarousel()?.getCarouselSelected()?.index!==a&&t.stopPropagation()}render(){const t=t=>{this._slideResizeObserver.disconnect();const a=this.getRootNode();a&&a instanceof ShadowRoot&&this._slideResizeObserver.observe(a.host);const e=t.detail;this._slideResizeObserver.observe(e.element),_(this,"media-carousel:select",e),this._dispatchMediaLoadedInfo(e)};return c` {t(a)}} + @frigate-card:carousel:media:loaded=${this._storeMediaLoadedInfo.bind(this)} + @frigate-card:carousel:media:unloaded=${this._removeMediaLoadedInfo.bind(this)} + > + + + + + ${this.label&&this.titlePopupConfig?c` + `:""}`}static get styles(){return p(":host {\n display: block;\n width: 100%;\n height: 100%;\n --video-max-height: none;\n position: relative;\n}")}};n([i({attribute:!1})],M.prototype,"nextPreviousConfig",void 0),n([i({attribute:!1})],M.prototype,"carouselOptions",void 0),n([i({attribute:!1})],M.prototype,"carouselPlugins",void 0),n([i({attribute:!1,type:Number})],M.prototype,"selected",void 0),n([i({attribute:!0})],M.prototype,"transitionEffect",void 0),n([i({attribute:!1})],M.prototype,"label",void 0),n([i({attribute:!1})],M.prototype,"logo",void 0),n([i({attribute:!1})],M.prototype,"titlePopupConfig",void 0),M=n([r("frigate-card-media-carousel")],M);let w=class extends s{constructor(){super(...arguments),this._toastRef=m()}render(){if(!this.text||!this.config||"none"==this.config.mode||!this.fitInto)return c``;const t=this.config.mode.match(/-top-/)?"top":"bottom",a=this.config.mode.match(/-left$/)?"left":"right";return c` + ${this.logo?c``:""} + `}isVisible(){return this._toastRef.value?.opened??!1}hide(){this._toastRef.value&&(this._toastRef.value.opened=!1)}show(){this._toastRef.value&&(this._toastRef.value.opened=!1,this._toastRef.value.opened=!0)}static get styles(){return p(":host {\n --paper-toast-background-color: rgba(0,0,0,0.6);\n --paper-toast-color: white;\n}\n\npaper-toast {\n max-width: unset;\n min-width: unset;\n display: flex;\n align-items: center;\n}\n\npaper-toast img {\n max-height: 24px;\n padding-left: 10px;\n}")}};n([i({attribute:!1})],w.prototype,"config",void 0),n([i({attribute:!1})],w.prototype,"text",void 0),n([i({attribute:!1})],w.prototype,"fitInto",void 0),n([i({attribute:!1})],w.prototype,"logo",void 0),w=n([r("frigate-card-title-control")],w);const I={active:!0,breakpoints:{}};function R(t){const a=C.optionsHandler(),e=a.merge(I,R.globalOptions);let o,n,i;function d(){"hidden"===document.visibilityState?(o.autoPauseCondition&&["all","hidden"].includes(o.autoPauseCondition)&&function(){for(const t of i)r(t)?.pause()}(),o.autoMuteCondition&&["all","hidden"].includes(o.autoMuteCondition)&&function(){for(const t of i)r(t)?.mute()}()):"visible"===document.visibilityState&&(o.autoPlayCondition&&["all","visible"].includes(o.autoPlayCondition)&&s(),o.autoUnmuteCondition&&["all","visible"].includes(o.autoUnmuteCondition)&&u())}function r(t){return o.playerSelector?t?.querySelector(o.playerSelector):null}function s(){r(i[n.selectedScrollSnap()])?.play()}function l(){r(i[n.selectedScrollSnap()])?.pause()}function c(){r(i[n.previousScrollSnap()])?.pause()}function u(){r(i[n.selectedScrollSnap()])?.unmute()}function h(){r(i[n.selectedScrollSnap()])?.mute()}function p(){r(i[n.previousScrollSnap()])?.mute()}const m={name:"autoMedia",options:a.merge(e,t),init:function(t){n=t,o=a.atMedia(m.options),i=n.slideNodes(),n.on("destroy",l),o.autoPauseCondition&&["all","unselected"].includes(o.autoPauseCondition)&&n.on("select",c),n.on("destroy",h),o.autoMuteCondition&&["all","unselected"].includes(o.autoMuteCondition)&&n.on("select",p),document.addEventListener("visibilitychange",d)},destroy:function(){n.off("destroy",l),o.autoPauseCondition&&["all","unselected"].includes(o.autoPauseCondition)&&n.off("select",c),n.off("destroy",h),o.autoMuteCondition&&["all","unselected"].includes(o.autoMuteCondition)&&n.off("select",p),document.removeEventListener("visibilitychange",d)},play:s,pause:l,mute:h,unmute:u};return m}R.globalOptions=void 0;const E={active:!0,breakpoints:{},lazyLoadCount:0};function T(t){const a=C.optionsHandler(),e=a.merge(E,T.globalOptions);let o,n,i;const d=new Set,r=["init","select","resize"],s=["select"];function l(){"hidden"===document.visibilityState&&o.lazyUnloadCallback&&o.lazyUnloadCondition&&["all","hidden"].includes(o.lazyUnloadCondition)?d.forEach((t=>{o.lazyUnloadCallback&&(o.lazyUnloadCallback(t,i[t]),d.delete(t))})):"visible"===document.visibilityState&&o.lazyLoadCallback&&u()}function c(t){return d.has(t)}function u(){const t=o.lazyLoadCount??0,a=n.selectedScrollSnap(),e=new Set;for(let o=1;o<=t&&a-o>=0;o++)e.add(a-o);e.add(a);for(let o=1;o<=t&&a+o{!c(t)&&o.lazyLoadCallback&&(d.add(t),o.lazyLoadCallback(t,i[t]))}))}function h(){const t=n.previousScrollSnap();c(t)&&o.lazyUnloadCallback&&(o.lazyUnloadCallback(t,i[t]),d.delete(t))}const p={name:"lazyload",options:a.merge(e,t),init:function(t){n=t,o=a.atMedia(p.options),i=n.slideNodes(),o.lazyLoadCallback&&r.forEach((t=>n.on(t,u))),o.lazyUnloadCallback&&o.lazyUnloadCondition&&["all","unselected"].includes(o.lazyUnloadCondition)&&s.forEach((t=>n.on(t,h))),document.addEventListener("visibilitychange",l)},destroy:function(){o.lazyLoadCallback&&r.forEach((t=>n.off(t,u))),o.lazyUnloadCallback&&s.forEach((t=>n.off(t,h))),document.removeEventListener("visibilitychange",l)},hasLazyloaded:c};return p}T.globalOptions=void 0;export{R as A,T as L,P as M,H as a,A as h,z as i,L as p,S as s,$ as w}; diff --git a/www/frigate-card/live-e0c9196c.js b/www/frigate-card/live-e0c9196c.js new file mode 100644 index 00000000..77be9d02 --- /dev/null +++ b/www/frigate-card/live-e0c9196c.js @@ -0,0 +1,127 @@ +import{cH as e,cI as i,cJ as t,cK as a,cL as r,l as s,cM as o,s as n,cN as d,y as l,bj as h,bk as c,bl as g,cO as u,bm as m,bn as v,cP as p,bQ as f,cQ as _,cR as C,cS as b,cT as $,cU as y,cV as w,o as P}from"./card-555679fd.js";import{L as M,A as S,i as k,w as x,a as L,p as I}from"./lazyload-c2d6254a.js";import{u as z}from"./media-layout-8e0c974f.js"; +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const E=e(class extends i{constructor(){super(...arguments),this.key=t}render(e,i){return this.key=e,i}update(e,[i,t]){return i!==this.key&&(a(e),this.key=i),t}});const R="frigate-card-live-provider",O=(e,i,t)=>{if(!t?.camera_entity)return r(e,s("error.no_live_camera"),{context:t}),null;const a=i.states[t.camera_entity];return a?"unavailable"===a.state?(o(e,s("error.live_camera_unavailable"),"info",{icon:"mdi:connection",context:t}),null):a:(r(e,s("error.live_camera_not_found"),{context:t}),null)};let V=class extends n{constructor(){super(),this._inBackground=!1,this._lastMediaLoadedInfo=null,this._messageReceivedPostRender=!1,this._renderKey=0,this._intersectionObserver=new IntersectionObserver(this._intersectionHandler.bind(this))}_intersectionHandler(e){this._inBackground=!e.some((e=>e.isIntersecting)),this._inBackground||this._messageReceivedPostRender||!this._lastMediaLoadedInfo||d(this._lastMediaLoadedInfo.source,this._lastMediaLoadedInfo.mediaLoadedInfo),this._messageReceivedPostRender&&!this._inBackground&&this.requestUpdate()}shouldUpdate(e){return!this._inBackground||!this._messageReceivedPostRender}connectedCallback(){this._intersectionObserver.observe(this),super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this._intersectionObserver.disconnect()}render(){if(!(this.hass&&this.liveConfig&&this.cameraManager&&this.view))return;const e=l`${E(this._renderKey,l` + {this._renderKey++,this._messageReceivedPostRender=!0,this._inBackground&&e.stopPropagation()}} + @frigate-card:media:loaded=${e=>{this._lastMediaLoadedInfo={source:e.composedPath()[0],mediaLoadedInfo:e.detail},this._inBackground&&e.stopPropagation()}} + @frigate-card:view:change=${e=>{this._inBackground&&e.stopPropagation()}} + > + + `)}`;return this._messageReceivedPostRender=!1,e}static get styles(){return h(":host {\n width: 100%;\n height: 100%;\n display: block;\n}")}};c([g({attribute:!1})],V.prototype,"conditionControllerEpoch",void 0),c([g({attribute:!1})],V.prototype,"hass",void 0),c([g({attribute:!1})],V.prototype,"view",void 0),c([g({attribute:!1})],V.prototype,"liveConfig",void 0),c([g({attribute:!1,hasChanged:u})],V.prototype,"liveOverrides",void 0),c([g({attribute:!1})],V.prototype,"cameraManager",void 0),c([g({attribute:!1})],V.prototype,"cardWideConfig",void 0),c([g({attribute:!1})],V.prototype,"microphoneStream",void 0),c([m()],V.prototype,"_inBackground",void 0),V=c([v("frigate-card-live")],V);let j=class extends n{constructor(){super(...arguments),this._cameraToSlide={},this._refMediaCarousel=p()}updated(e){super.updated(e),e.has("inBackground")&&this.updateComplete.then((async()=>{const e=this._refMediaCarousel.value;e&&(await e.updateComplete,this.inBackground?(e.autoPause(),e.autoMute()):(e.autoPlay(),e.autoUnmute()))}))}_getTransitionEffect(){return this.liveConfig?.transition_effect??f.live.transition_effect}_getSelectedCameraIndex(){const e=this.cameraManager?.getStore().getVisibleCameraIDs();return e&&this.view?Math.max(0,Array.from(e).indexOf(this.view.camera)):0}_getOptions(){return{draggable:this.liveConfig?.draggable,loop:!0}}_getPlugins(){const e=this.cameraManager?.getStore().getVisibleCameraIDs();return[...e&&e.size>1?[_({forceWheelAxis:"y"})]:[],M({...this.liveConfig?.lazy_load&&{lazyLoadCallback:(e,i)=>this._lazyloadOrUnloadSlide("load",e,i)},lazyUnloadCondition:this.liveConfig?.lazy_unload,lazyUnloadCallback:(e,i)=>this._lazyloadOrUnloadSlide("unload",e,i)}),S({playerSelector:R,...this.liveConfig?.auto_play&&{autoPlayCondition:this.liveConfig.auto_play},...this.liveConfig?.auto_pause&&{autoPauseCondition:this.liveConfig.auto_pause},...this.liveConfig?.auto_mute&&{autoMuteCondition:this.liveConfig.auto_mute},...this.liveConfig?.auto_unmute&&{autoUnmuteCondition:this.liveConfig.auto_unmute}})]}_getLazyLoadCount(){return!1===this.liveConfig?.lazy_load?null:0}_getSlides(){const e=this.cameraManager?.getStore().getVisibleCameras();if(!e)return[[],{}];const i=[],t={};for(const[a,r]of e){const e=this.view?.context?.live?.overrides?.get(a)??a,s=a===e?r:this.cameraManager?.getStore().getCameraConfig(e),o=s?this._renderLive(e,s,i.length):null;o&&(t[a]=i.length,i.push(o))}return[i,t]}_setViewHandler(e){const i=this.cameraManager?.getStore().getVisibleCameras();i&&e.detail.index!==this._getSelectedCameraIndex()&&this._setViewCameraID(Array.from(i.keys())[e.detail.index])}_setViewCameraID(e){e&&this.view?.evolve({camera:e,query:null,queryResults:null}).mergeInContext({thumbnails:{fetch:!1}}).dispatchChangeEvent(this)}_lazyloadOrUnloadSlide(e,i,t){t instanceof HTMLSlotElement&&(t=t.assignedElements({flatten:!0})[0]);const a=t?.querySelector(R);a&&(a.disabled="load"!==e)}_renderLive(e,i,t){if(!(this.liveConfig&&this.hass&&this.cameraManager&&this.conditionControllerEpoch))return;const a=C(this.conditionControllerEpoch.controller,this.liveConfig,this.liveOverrides,{camera:e}),r=this.cameraManager.getCameraMetadata(this.hass,e);return l` +
+ this.cameraManager?.getCameraEndpoints(e)??void 0))} + .label=${r?.title??""} + .liveConfig=${a} + .hass=${this.hass} + .cardWideConfig=${this.cardWideConfig} + @frigate-card:media:loaded=${e=>{x(t,e)}} + @frigate-card:media:unloaded=${e=>{L(t,e)}} + > + +
+ `}_getCameraIDsOfNeighbors(){const e=this.cameraManager?.getStore().getVisibleCameras();if(!e||!this.view||!this.hass)return[null,null];const i=Array.from(e.keys()),t=i.indexOf(this.view.camera);return t<0||e.size<=1?[null,null]:[i[t>0?t-1:e.size-1],i[t+1this.view?.context?.live?.overrides?.get(e)??e,o=t?this.cameraManager.getCameraMetadata(this.hass,r(t)):null,n=this.cameraManager.getCameraMetadata(this.hass,r(this.view.camera)),d=a?this.cameraManager.getCameraMetadata(this.hass,r(a)):null;return l` + {$(this,{thumbnails:{fetch:!0}})}} + > + {this._setViewCameraID(t),y(e)}} + > + + ${e} + {this._setViewCameraID(a),y(e)}} + > + + + `}static get styles(){return h(".embla__slide {\n height: 100%;\n flex: 0 0 100%;\n}")}};c([g({attribute:!1})],j.prototype,"hass",void 0),c([g({attribute:!1})],j.prototype,"view",void 0),c([g({attribute:!1})],j.prototype,"liveConfig",void 0),c([g({attribute:!1,hasChanged:u})],j.prototype,"liveOverrides",void 0),c([g({attribute:!1})],j.prototype,"inBackground",void 0),c([g({attribute:!1})],j.prototype,"conditionControllerEpoch",void 0),c([g({attribute:!1})],j.prototype,"cardWideConfig",void 0),c([g({attribute:!1})],j.prototype,"cameraManager",void 0),c([g({attribute:!1})],j.prototype,"microphoneStream",void 0),j=c([v("frigate-card-live-carousel")],j);let B=class extends n{constructor(){super(...arguments),this.disabled=!1,this.label="",this._isVideoMediaLoaded=!1,this._refProvider=p(),this._importPromises=[]}async play(){await this.updateComplete,await(this._refProvider.value?.updateComplete),await I(this,this._refProvider.value)}async pause(){await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.pause())}async mute(){await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.mute())}async unmute(){await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.unmute())}isMuted(){return this._refProvider.value?.isMuted()??!0}async seek(e){await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.seek(e))}async setControls(e){await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.setControls(e))}isPaused(){return this._refProvider.value?.isPaused()??!0}async getScreenshotURL(){return await this.updateComplete,await(this._refProvider.value?.updateComplete),await(this._refProvider.value?.getScreenshotURL())??null}_getResolvedProvider(){return"auto"===this.cameraConfig?.live_provider?this.cameraConfig?.webrtc_card?.entity||this.cameraConfig?.webrtc_card?.url?"webrtc-card":this.cameraConfig?.camera_entity?"low"===this.cardWideConfig?.performance?.profile?"image":"ha":this.cameraConfig?.frigate.camera_name?"jsmpeg":f.cameras.live_provider:this.cameraConfig?.live_provider||"image"}_shouldShowImageDuringLoading(){return!!this.cameraConfig?.camera_entity&&!!this.hass&&!!this.liveConfig?.show_image_during_load}disconnectedCallback(){this._isVideoMediaLoaded=!1}_videoMediaShowHandler(){this._isVideoMediaLoaded=!0}willUpdate(e){if(e.has("disabled")&&this.disabled&&(this._isVideoMediaLoaded=!1,w(this)),e.has("liveConfig")&&(z(this,this.liveConfig?.layout),this.liveConfig?.show_image_during_load&&this._importPromises.push(import("./live-image-c8850fc4.js")),this.liveConfig?.zoomable&&this._importPromises.push(import("./zoomer-1857311a.js"))),e.has("cameraConfig")){const e=this._getResolvedProvider();"jsmpeg"===e?this._importPromises.push(import("./live-jsmpeg-9c767737.js")):"ha"===e?this._importPromises.push(import("./live-ha-df63bfc8.js")):"webrtc-card"===e?this._importPromises.push(import("./live-webrtc-card-dfc8f852.js")):"image"===e?this._importPromises.push(import("./live-image-c8850fc4.js")):"go2rtc"===e&&this._importPromises.push(import("./live-go2rtc-0795a62f.js"))}}async getUpdateComplete(){const e=await super.getUpdateComplete();return await Promise.all(this._importPromises),this._importPromises=[],e}_useZoomIfRequired(e){return this.liveConfig?.zoomable?l` this.setControls(!1)} + @frigate-card:zoom:unzoomed=${()=>this.setControls()} + > + ${e} + `:e}render(){if(this.disabled||!this.hass||!this.liveConfig||!this.cameraConfig)return;this.title=this.label,this.ariaLabel=this.label;const e=this._getResolvedProvider(),i=!this._isVideoMediaLoaded&&this._shouldShowImageDuringLoading(),t={hidden:i};return this._useZoomIfRequired(l` + ${i||"image"===e?l` {"image"===e?this._videoMediaShowHandler():i.stopPropagation()}} + > + `:l``} + ${"ha"===e?l` + `:"go2rtc"===e?l` + `:"webrtc-card"===e?l` + `:"jsmpeg"===e?l` + `:l``} + `)}static get styles(){return h(":host {\n display: block;\n height: 100%;\n width: 100;\n}\n\n.hidden {\n display: none;\n}")}};c([g({attribute:!1})],B.prototype,"hass",void 0),c([g({attribute:!1})],B.prototype,"cameraConfig",void 0),c([g({attribute:!1})],B.prototype,"cameraEndpoints",void 0),c([g({attribute:!1})],B.prototype,"liveConfig",void 0),c([g({attribute:!0,type:Boolean})],B.prototype,"disabled",void 0),c([g({attribute:!1})],B.prototype,"label",void 0),c([g({attribute:!1})],B.prototype,"cardWideConfig",void 0),c([g({attribute:!1})],B.prototype,"microphoneStream",void 0),c([m()],B.prototype,"_isVideoMediaLoaded",void 0),B=c([v(R)],B);export{V as FrigateCardLive,j as FrigateCardLiveCarousel,B as FrigateCardLiveProvider,O as getStateObjOrDispatchError}; diff --git a/www/frigate-card/live-go2rtc-0795a62f.js b/www/frigate-card/live-go2rtc-0795a62f.js new file mode 100644 index 00000000..00534318 --- /dev/null +++ b/www/frigate-card/live-go2rtc-0795a62f.js @@ -0,0 +1 @@ +import{dj as e,dk as t,dl as s,dm as i,s as n,di as o,cL as a,l as c,y as r,bj as h,bk as d,bl as l,bn as p}from"./card-555679fd.js";import{g as m}from"./endpoint-aa68fc9e.js";import{s as u,h as v,M as y}from"./lazyload-c2d6254a.js";import"./image-0b99ab11.js";import{m as b}from"./audio-557099cb.js";import"./media-layout-8e0c974f.js";class g extends HTMLElement{constructor(){super(),this.DISCONNECT_TIMEOUT=5e3,this.RECONNECT_TIMEOUT=3e4,this.CODECS=["avc1.640029","avc1.64002A","avc1.640033","hvc1.1.6.L153.B0","mp4a.40.2","mp4a.40.5","flac","opus"],this.mode="webrtc,mse,mp4,mjpeg",this.background=!1,this.visibilityThreshold=0,this.visibilityCheck=!0,this.pcConfig={iceServers:[{urls:"stun:stun.l.google.com:19302"}],sdpSemantics:"unified-plan"},this.wsState=WebSocket.CLOSED,this.pcState=WebSocket.CLOSED,this.video=null,this.ws=null,this.wsURL="",this.pc=null,this.connectTS=0,this.mseCodecs="",this.disconnectTID=0,this.reconnectTID=0,this.ondata=null,this.onmessage=null,this.microphoneStream=null,this.containingPlayer=null,this.controls=!0}reconnect(){this.wsState!==WebSocket.CLOSED?(this.ws?.addEventListener("close",(()=>this.onconnect())),this.ondisconnect()):(this.ondisconnect(),this.onconnect())}set src(e){"string"!=typeof e&&(e=e.toString()),e.startsWith("http")?e="ws"+e.substring(4):e.startsWith("/")&&(e="ws"+location.origin.substring(4)+e),this.wsURL=e,this.onconnect()}play(){}send(e){this.ws&&this.ws.send(JSON.stringify(e))}codecs(e){const t="mse"===e?e=>MediaSource.isTypeSupported(`video/mp4; codecs="${e}"`):e=>this.video.canPlayType(`video/mp4; codecs="${e}"`);return this.CODECS.filter(t).join()}connectedCallback(){if(this.disconnectTID&&(clearTimeout(this.disconnectTID),this.disconnectTID=0),this.video){const e=this.video.seekable;e.length>0&&(this.video.currentTime=e.end(e.length-1)),this.play()}else this.oninit();this.onconnect()}disconnectedCallback(){this.background||this.disconnectTID||this.wsState===WebSocket.CLOSED&&this.pcState===WebSocket.CLOSED||(this.disconnectTID=setTimeout((()=>{this.reconnectTID&&(clearTimeout(this.reconnectTID),this.reconnectTID=0),this.disconnectTID=0,this.ondisconnect()}),this.DISCONNECT_TIMEOUT))}oninit(){if(this.video=document.createElement("video"),u(this.video,this.controls),this.video.playsInline=!0,this.video.preload="auto",this.video.style.display="block",this.video.style.width="100%",this.video.style.height="100%",this.appendChild(this.video),!this.background){if("hidden"in document&&this.visibilityCheck&&document.addEventListener("visibilitychange",(()=>{document.hidden?this.disconnectedCallback():this.isConnected&&this.connectedCallback()})),"IntersectionObserver"in window&&this.visibilityThreshold){new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting?this.isConnected&&this.connectedCallback():this.disconnectedCallback()}))}),{threshold:this.visibilityThreshold}).observe(this)}this.video.onloadeddata=()=>{this.controls&&v(this.video,y),e(this,this.video,{player:this.containingPlayer,capabilities:{supports2WayAudio:!!this.pc,supportsPause:!0,hasAudio:b(this.video)}})},this.video.onvolumechange=()=>t(this),this.video.onplay=()=>s(this),this.video.onpause=()=>i(this),this.video.muted=!0}}onconnect(){return!(!this.isConnected||!this.wsURL||this.ws||this.pc)&&(this.wsState=WebSocket.CONNECTING,this.connectTS=Date.now(),this.ws=new WebSocket(this.wsURL),this.ws.binaryType="arraybuffer",this.ws.addEventListener("open",(e=>this.onopen(e))),this.ws.addEventListener("close",(e=>this.onclose(e))),!0)}ondisconnect(){this.wsState=WebSocket.CLOSED,this.ws&&(this.ws.close(),this.ws=null),this.pcState=WebSocket.CLOSED,this.pc&&(this.pc.close(),this.pc=null)}onopen(){this.wsState=WebSocket.OPEN,this.ws.addEventListener("message",(e=>{if("string"==typeof e.data){const t=JSON.parse(e.data);for(const e in this.onmessage)this.onmessage[e](t)}else this.ondata(e.data)})),this.ondata=null,this.onmessage={};const e=[];return this.mode.indexOf("mse")>=0&&"MediaSource"in window?(e.push("mse"),this.onmse()):this.mode.indexOf("mp4")>=0&&(e.push("mp4"),this.onmp4()),this.mode.indexOf("webrtc")>=0&&"RTCPeerConnection"in window&&(e.push("webrtc"),this.onwebrtc()),this.mode.indexOf("mjpeg")>=0&&(e.length?this.onmessage.mjpeg=t=>{"error"===t.type&&0===t.value.indexOf(e[0])&&this.onmjpeg()}:(e.push("mjpeg"),this.onmjpeg())),e}onclose(){if(this.wsState===WebSocket.CLOSED)return!1;this.wsState=WebSocket.CONNECTING,this.ws=null;const e=Math.max(this.RECONNECT_TIMEOUT-(Date.now()-this.connectTS),0);return this.reconnectTID=setTimeout((()=>{this.reconnectTID=0,this.onconnect()}),e),!0}onmse(){const e=new MediaSource;e.addEventListener("sourceopen",(()=>{URL.revokeObjectURL(this.video.src),this.send({type:"mse",value:this.codecs("mse")})}),{once:!0}),this.video.src=URL.createObjectURL(e),this.video.srcObject=null,this.play(),this.mseCodecs="",this.onmessage.mse=t=>{if("mse"!==t.type)return;this.mseCodecs=t.value;const s=e.addSourceBuffer(t.value);s.mode="segments",s.addEventListener("updateend",(()=>{if(!s.updating)try{if(n>0){const e=i.slice(0,n);n=0,s.appendBuffer(e)}else if(s.buffered&&s.buffered.length){const t=s.buffered.end(s.buffered.length-1)-15,i=s.buffered.start(0);t>i&&(s.remove(i,t),e.setLiveSeekableRange(t,t+15))}}catch(e){}}));const i=new Uint8Array(2097152);let n=0;this.ondata=e=>{if(s.updating||n>0){const t=new Uint8Array(e);i.set(t,n),n+=t.byteLength}else try{s.appendBuffer(e)}catch(e){}}}}onwebrtc(){const e=new RTCPeerConnection(this.pcConfig),t=document.createElement("video");t.addEventListener("loadeddata",(e=>this.onpcvideo(e)),{once:!0}),e.addEventListener("icecandidate",(e=>{const t=e.candidate?e.candidate.toJSON().candidate:"";this.send({type:"webrtc/candidate",value:t})})),e.addEventListener("track",(e=>{null===t.srcObject&&0!==e.streams.length&&"{"!==e.streams[0].id[0]&&"video"===e.track.kind&&(t.srcObject=e.streams[0])})),e.addEventListener("connectionstatechange",(()=>{"failed"!==e.connectionState&&"disconnected"!==e.connectionState||(e.close(),this.pcState=WebSocket.CLOSED,this.pc=null,this.onconnect())})),this.onmessage.webrtc=t=>{switch(t.type){case"webrtc/candidate":e.addIceCandidate({candidate:t.value,sdpMid:"0"}).catch((()=>console.debug));break;case"webrtc/answer":e.setRemoteDescription({type:"answer",sdp:t.value}).catch((()=>console.debug));break;case"error":if(t.value.indexOf("webrtc/offer")<0)return;e.close()}},e.addTransceiver("video",{direction:"recvonly"}),e.addTransceiver("audio",{direction:"recvonly"}),this.microphoneStream?.getTracks().forEach((t=>{e.addTransceiver(t,{direction:"sendonly"})})),e.createOffer().then((t=>{e.setLocalDescription(t).then((()=>{this.send({type:"webrtc/offer",value:t.sdp})}))})),this.pcState=WebSocket.CONNECTING,this.pc=e}onpcvideo(e){if(!this.pc)return;const t=e.target,s=this.pc.connectionState;if("connected"===s||"connecting"===s||!s){let e=0,s=0;const i=t.srcObject;i.getVideoTracks().length>0&&(e+=544),i.getAudioTracks().length>0&&(e+=258),this.mseCodecs.indexOf("hvc1.")>=0&&(s+=560),this.mseCodecs.indexOf("avc1.")>=0&&(s+=528),this.mseCodecs.indexOf("mp4a.")>=0&&(s+=257),e>=s?(this.video.srcObject=i,this.play(),this.pcState=WebSocket.OPEN,this.wsState=WebSocket.CLOSED,this.ws.close(),this.ws=null):(this.pcState=WebSocket.CLOSED,this.pc.close(),this.pc=null)}t.srcObject=null}onmjpeg(){let t=!1;this.ondata=s=>{u(this.video,!1),this.video.poster="data:image/jpeg;base64,"+g.btoa(s),t||(t=!0,e(this,this.video,{player:this.containingPlayer}))},this.send({type:"mjpeg"})}onmp4(){const t=document.createElement("canvas");let s;const i=document.createElement("video");i.autoplay=!0,i.playsInline=!0,i.muted=!0,i.addEventListener("loadeddata",(n=>{s||(t.width=i.videoWidth,t.height=i.videoHeight,s=t.getContext("2d"),e(this,i,{player:this.containingPlayer})),s.drawImage(i,0,0,t.width,t.height),u(this.video,!1),this.video.poster=t.toDataURL("image/jpeg")})),this.ondata=e=>{i.src="data:video/mp4;base64,"+g.btoa(e)},this.send({type:"mp4",value:this.codecs("mp4")})}static btoa(e){const t=new Uint8Array(e),s=t.byteLength;let i="";for(let e=0;e{let u=class extends(customElements.get("ha-web-rtc-player")){async play(){return this._video?.play()}async pause(){this._video?.pause()}async mute(){this._video&&(this._video.muted=!0)}async unmute(){this._video&&(this._video.muted=!1)}isMuted(){return this._video?.muted??!0}async seek(e){this._video&&(this._video.currentTime=e)}async setControls(e){this._video&&$(this._video,e??this.controls)}isPaused(){return this._video?.paused??!0}async getScreenshotURL(){return this._video?e(this._video):null}render(){return this._error?t(this,`${this._error} (${this.entityid})`):s` + + `}static get styles(){return[super.styles,o(_),l` + :host { + width: 100%; + height: 100%; + } + video { + width: 100%; + height: 100%; + } + `]}};n([v("#remote-stream")],u.prototype,"_video",void 0),u=n([d("frigate-card-ha-web-rtc-player")],u)})),customElements.whenDefined("ha-camera-stream").then((()=>{let e=class extends(customElements.get("ha-camera-stream")){async play(){return this._player?.play()}async pause(){this._player?.pause()}async mute(){this._player?.mute()}async unmute(){this._player?.unmute()}isMuted(){return this._player?.isMuted()??!0}async seek(e){this._player?.seek(e)}async setControls(e){this._player&&this._player.setControls(e??this.controls)}isPaused(){return this._player?.isPaused()??!0}async getScreenshotURL(){return this._player?await this._player.getScreenshotURL():null}render(){return this.stateObj?this._shouldRenderMJPEG?s` + {a(this,e,{player:this})}} + .src=${void 0===this._connected||this._connected?(e=this.stateObj,`/api/camera_proxy_stream/${e.entity_id}?token=${e.attributes.access_token}`):""} + /> + `:"hls"===this.stateObj.attributes.frontend_stream_type?this._url?s` `:s``:"web_rtc"===this.stateObj.attributes.frontend_stream_type?s``:void 0:s``;var e}static get styles(){return[super.styles,o(_),l` + :host { + width: 100%; + height: 100%; + } + img { + width: 100%; + height: 100%; + } + `]}};n([v("#player")],e.prototype,"_player",void 0),e=n([d("frigate-card-ha-camera-stream")],e)}));let w=class extends u{constructor(){super(...arguments),this.controls=!0,this._playerRef=c()}async play(){return this._playerRef.value?.play()}async pause(){this._playerRef.value?.pause()}async mute(){this._playerRef.value?.mute()}async unmute(){this._playerRef.value?.unmute()}isMuted(){return this._playerRef.value?.isMuted()??!0}async seek(e){this._playerRef.value?.seek(e)}async setControls(e){this._playerRef.value?.setControls(e??this.controls)}isPaused(){return this._playerRef.value?.isPaused()??!0}async getScreenshotURL(){return await(this._playerRef.value?.getScreenshotURL())??null}render(){if(!this.hass)return;const e=m(this,this.hass,this.cameraConfig);return e?s` + `:void 0}static get styles(){return o(":host {\n width: 100%;\n height: 100%;\n display: block;\n --video-max-height: none;\n}")}};n([p({attribute:!1})],w.prototype,"hass",void 0),n([p({attribute:!1})],w.prototype,"cameraConfig",void 0),n([p({attribute:!0,type:Boolean})],w.prototype,"controls",void 0),w=n([d("frigate-card-live-ha")],w);export{w as FrigateCardLiveHA}; diff --git a/www/frigate-card/live-image-c8850fc4.js b/www/frigate-card/live-image-c8850fc4.js new file mode 100644 index 00000000..5146c2b8 --- /dev/null +++ b/www/frigate-card/live-image-c8850fc4.js @@ -0,0 +1,7 @@ +import{s as e,cP as a,y as s,cS as t,bj as i,bk as r,bl as m,bn as o}from"./card-555679fd.js";import"./image-0b99ab11.js";import{getStateObjOrDispatchError as n}from"./live-e0c9196c.js";import"./media-layout-8e0c974f.js";import"./lazyload-c2d6254a.js";let u=class extends e{constructor(){super(...arguments),this._refImage=a()}async play(){await(this._refImage.value?.play())}async pause(){await(this._refImage.value?.pause())}async mute(){await(this._refImage.value?.mute())}async unmute(){await(this._refImage.value?.unmute())}isMuted(){return!!this._refImage.value?.isMuted()}async seek(e){await(this._refImage.value?.seek(e))}async setControls(e){await(this._refImage.value?.setControls(e))}isPaused(){return this._refImage.value?.isPaused()??!0}async getScreenshotURL(){return await(this._refImage.value?.getScreenshotURL())??null}render(){if(this.hass&&this.cameraConfig)return n(this,this.hass,this.cameraConfig),s` + `}static get styles(){return i(":host {\n width: 100%;\n height: 100%;\n display: block;\n}")}};r([m({attribute:!1})],u.prototype,"hass",void 0),r([m({attribute:!1})],u.prototype,"cameraConfig",void 0),u=r([o("frigate-card-live-image")],u);export{u as FrigateCardLiveImage}; diff --git a/www/frigate-card/live-jsmpeg-9c767737.js b/www/frigate-card/live-jsmpeg-9c767737.js new file mode 100644 index 00000000..f367a55f --- /dev/null +++ b/www/frigate-card/live-jsmpeg-9c767737.js @@ -0,0 +1,12 @@ +import{cH as A,dG as t,cW as i,dH as e,s,cX as o,dl as g,dm as I,dj as a,cL as B,l as r,y as C,db as n,bj as E,bk as Q,bl as h,bn as d}from"./card-555679fd.js";import{g as c}from"./endpoint-aa68fc9e.js";function l(){return l=Object.assign?Object.assign.bind():function(A){for(var t=1;t"string"==typeof A&&A.constructor===String,D=A=>"Promise"===Object.prototype.toString.call(A).slice(8,-1);var m="WJ3NAvwFY9",f="tR2-0dd-e1",y="ZgIIHVSSYI",R="kAA8SjbHe2",k="OueN4AU4CJ";!function(A,t){void 0===t&&(t={});var i=t.insertAt;if(A&&"undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===i&&e.firstChild?e.insertBefore(s,e.firstChild):e.appendChild(s),s.styleSheet?s.styleSheet.cssText=A:s.appendChild(document.createTextNode(A))}}(".WJ3NAvwFY9,.ZgIIHVSSYI,.kAA8SjbHe2,.tR2-0dd-e1{height:100%;left:0;position:absolute;top:0;width:100%;z-index:1}.ZgIIHVSSYI{-ms-flex-pack:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;justify-content:center}.WJ3NAvwFY9,.tR2-0dd-e1{display:block}.tR2-0dd-e1.OueN4AU4CJ{display:none}.ZgIIHVSSYI,.kAA8SjbHe2{-webkit-tap-highlight-color:rgba(255,0,0,0);cursor:pointer;opacity:.7}.OueN4AU4CJ.ZgIIHVSSYI,.OueN4AU4CJ.kAA8SjbHe2{display:none}.ZgIIHVSSYI{z-index:10}.ZgIIHVSSYI>svg{fill:#fff;height:12vw;max-height:60px;max-width:60px;width:12vw}.kAA8SjbHe2{-ms-flex-pack:end;-ms-flex-align:end;-webkit-align-items:flex-end;align-items:flex-end;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;justify-content:flex-end;z-index:10}.kAA8SjbHe2>svg{fill:#fff;height:9vw;margin:0 15px 15px 0;max-height:40px;max-width:40px;width:9vw}");var b=function(){function A(A,t,i,e){var s=void 0===i?{}:i,o=s.canvas,g=void 0===o?"":o,I=s.poster,a=void 0===I?"":I,B=s.autoplay,r=void 0!==B&&B,C=s.autoSetWrapperSize,n=void 0!==C&&C,E=s.loop,Q=void 0!==E&&E,h=s.control,d=void 0===h||h,c=s.decodeFirstFrame,u=void 0===c||c,w=s.picMode,D=void 0!==w&&w,m=s.progressive,f=void 0===m||m,y=s.chunkSize,R=void 0===y?1048576:y,k=s.hooks,b=void 0===k?{}:k;void 0===e&&(e={}),this.options=l({videoUrl:t,canvas:g,poster:a,picMode:D,autoplay:r,autoSetWrapperSize:n,loop:Q,control:d,decodeFirstFrame:u,progressive:f,chunkSize:R,hooks:l({play:function(){},pause:function(){},stop:function(){},load:function(){}},b)},e),this.options.needPlayButton=this.options.control&&!this.options.picMode,this.player=null,this.els={wrapper:p(A)?document.querySelector(A):A,canvas:null,playButton:document.createElement("div"),unmuteButton:null,poster:null},"static"===window.getComputedStyle(this.els.wrapper).getPropertyValue("position")&&(this.els.wrapper.style.position="relative"),this.els.wrapper.clientRect=this.els.wrapper.getBoundingClientRect(),this.initCanvas(),this.initPlayButton(),this.initPlayer()}var t=A.prototype;return t.initCanvas=function(){this.options.canvas?this.els.canvas=p(this.options.canvas)?document.querySelector(this.options.canvas):this.options.canvas:(this.els.canvas=document.createElement("canvas"),this.els.canvas.classList.add(m),this.els.wrapper.appendChild(this.els.canvas))},t.initPlayer=function(){var A=this;this.options=l(this.options,{canvas:this.els.canvas});var t=l({},this.options,{autoplay:!1});if(this.player=new X(this.options.videoUrl,t,{play:function(){A.options.needPlayButton&&A.els.playButton.classList.add(k),A.els.poster&&A.els.poster.classList.add(k),A.options.hooks.play()},pause:function(){A.options.needPlayButton&&A.els.playButton.classList.remove(k),A.options.hooks.pause()},stop:function(){A.els.poster&&A.els.poster.classList.remove(k),A.options.hooks.stop()},load:function(){A.options.autoplay&&A.play(),A._autoSetWrapperSize(),A.options.hooks.load()}}),this._copyPlayerFuncs(),this.els.wrapper.playerInstance=this.player,!this.options.poster||this.options.autoplay||this.player.options.streaming||(this.options.decodeFirstFrame=!1,this.els.poster=new Image,this.els.poster.src=this.options.poster,this.els.poster.classList.add(f),this.els.wrapper.appendChild(this.els.poster)),this.player.options.streaming||this.els.wrapper.addEventListener("click",this.onClick.bind(this)),(this.options.autoplay||this.player.options.streaming)&&this.els.playButton.classList.add(k),this.player.audioOut&&!this.player.audioOut.unlocked){var i=this.els.wrapper;(this.options.autoplay||this.player.options.streaming)&&(this.els.unmuteButton=document.createElement("div"),this.els.unmuteButton.innerHTML='\n\n \n\n',this.els.unmuteButton.classList.add(R),this.els.wrapper.appendChild(this.els.unmuteButton),i=this.els.unmuteButton),this.unlockAudioBound=this.onUnlockAudio.bind(this,i),i.addEventListener("touchstart",this.unlockAudioBound,!1),i.addEventListener("click",this.unlockAudioBound,!0)}},t.initPlayButton=function(){this.options.needPlayButton&&(this.els.playButton.classList.add(y),this.els.playButton.innerHTML='\n\n \n\n',this.els.wrapper.appendChild(this.els.playButton))},t._autoSetWrapperSize=function(){var A=this;if(!this.options.autoSetWrapperSize)return Promise.resolve();var t=this.player.video.destination;return t?Promise.resolve().then((function(){return function(A){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return D(A)?A:new Promise((i=>{A(),setTimeout(i,t)}))}((function(){A.els.wrapper.style.width=t.width+"px",A.els.wrapper.style.height=t.height+"px"}))})):Promise.resolve()},t.onUnlockAudio=function(A,t){var i=this;this.els.unmuteButton&&(t.preventDefault(),t.stopPropagation()),this.player.audioOut.unlock((function(){i.els.unmuteButton&&i.els.unmuteButton.classList.add(k),A.removeEventListener("touchstart",i.unlockAudioBound),A.removeEventListener("click",i.unlockAudioBound)}))},t.onClick=function(){this.options.control&&(this.player.isPlaying?this.pause():this.play())},t._copyPlayerFuncs=function(){var A=this;this.play=function(){return A.player.play()},this.pause=function(){return A.player.pause()},this.stop=function(){return A.player.stop()},this.destroy=function(){A.player.destroy(),A.els.wrapper.innerHTML="",A.els.wrapper.playerInstance=null}},A}(),G=function(){return window.performance?window.performance.now()/1e3:Date.now()/1e3},S=function(A,t){if(A.fill)A.fill(t);else for(var i=0;iA&&this.loadNextChunk())},t.destroy=function(){this.request.abort(),this.aborted=!0},t.loadNextChunk=function(){var A=this,t=this.loadedSize,i=Math.min(this.loadedSize+this.chunkSize-1,this.fileSize-1);if(t>=this.fileSize||this.aborted)return this.completed=!0,void(this.onCompletedCallback&&this.onCompletedCallback(this));this.isLoading=!0,this.loadStartTime=G(),this.request=new XMLHttpRequest,this.request.onreadystatechange=function(){A.request.readyState===A.request.DONE&&A.request.status>=200&&A.request.status<300?A.onChunkLoad(A.request.response):A.request.readyState===A.request.DONE&&A.loadFails++<3&&A.loadNextChunk()},0===t&&(this.request.onprogress=this.onProgress.bind(this)),this.request.open("GET",this.url+"?"+t+"-"+i),this.request.setRequestHeader("Range","bytes="+t+"-"+i),this.request.responseType="arraybuffer",this.request.send()},t.onProgress=function(A){this.progress=A.loaded/A.total},t.onChunkLoad=function(A){var t=!this.established;this.established=!0,this.progress=1,this.loadedSize+=A.byteLength,this.loadFails=0,this.isLoading=!1,t&&this.hookOnEstablished&&this.hookOnEstablished(),t&&this.onEstablishedCallback&&this.onEstablishedCallback(this),this.destination&&this.destination.write(A),this.loadTime=G()-this.loadStartTime,this.throttled||this.loadNextChunk()},A}(),N=function(){function A(A,t){this.url=A,this.options=t,this.socket=null,this.streaming=!0,this.callbacks={connect:[],data:[]},this.destination=null,this.reconnectInterval=void 0!==t.reconnectInterval?t.reconnectInterval:5,this.shouldAttemptReconnect=!!this.reconnectInterval,this.completed=!1,this.established=!1,this.progress=0,this.reconnectTimeoutId=0,this.onEstablishedCallback=t.onSourceEstablished,this.onCompletedCallback=t.onSourceCompleted,t.hookOnEstablished&&(this.hookOnEstablished=t.hookOnEstablished)}var t=A.prototype;return t.connect=function(A){this.destination=A},t.destroy=function(){clearTimeout(this.reconnectTimeoutId),this.shouldAttemptReconnect=!1,this.socket.close()},t.start=function(){this.shouldAttemptReconnect=!!this.reconnectInterval,this.progress=0,this.established=!1,this.options.protocols?this.socket=new WebSocket(this.url,this.options.protocols):this.socket=new WebSocket(this.url),this.socket.binaryType="arraybuffer",this.socket.onmessage=this.onMessage.bind(this),this.socket.onopen=this.onOpen.bind(this),this.socket.onerror=this.onClose.bind(this),this.socket.onclose=this.onClose.bind(this)},t.resume=function(){},t.onOpen=function(){this.progress=1},t.onClose=function(){var A=this;this.shouldAttemptReconnect&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=setTimeout((function(){A.start()}),1e3*this.reconnectInterval))},t.onMessage=function(A){var t=!this.established;this.established=!0,t&&this.hookOnEstablished&&this.hookOnEstablished(),t&&this.onEstablishedCallback&&this.onEstablishedCallback(this),this.destination&&this.destination.write(A.data)},A}(),L=function(){function A(t,i){"object"==typeof t?(this.bytes=t instanceof Uint8Array?t:new Uint8Array(t),this.byteLength=this.bytes.length):(this.bytes=new Uint8Array(t||1048576),this.byteLength=0),this.mode=i||A.MODE.EXPAND,this.index=0}var t=A.prototype;return t.resize=function(A){var t=new Uint8Array(A);0!==this.byteLength&&(this.byteLength=Math.min(this.byteLength,A),t.set(this.bytes,0,this.byteLength)),this.bytes=t,this.index=Math.min(this.index,this.byteLength<<3)},t.evict=function(A){var t=this.index>>3,i=this.bytes.length-this.byteLength;if(this.index===this.byteLength<<3||A>i+t)return this.byteLength=0,void(this.index=0);0!==t&&(this.bytes.copyWithin?this.bytes.copyWithin(0,t,this.byteLength):this.bytes.set(this.bytes.subarray(t,this.byteLength)),this.byteLength-=t,this.index-=t<<3)},t.write=function(t){var i="object"==typeof t[0],e=0,s=this.bytes.length-this.byteLength;if(i){e=0;for(var o=0;os)if(this.mode===A.MODE.EXPAND){var g=Math.max(2*this.bytes.length,e-s);this.resize(g)}else this.evict(e);if(i)for(var I=0;I>3;A>3;return A>=this.byteLength||0===this.bytes[A]&&0===this.bytes[A+1]&&1===this.bytes[A+2]},t.peek=function(A){for(var t=this.index,i=0;A;){var e=8-(7&t),s=e>3]&255>>8-s<>o,t+=s,A-=s}return i},t.read=function(A){var t=this.peek(A);return this.index+=A,t},t.skip=function(A){return this.index+=A},t.rewind=function(A){this.index=Math.max(this.index-A,0)},t.has=function(A){return(this.byteLength<<3)-this.index>=A},A}();L.MODE={EVICT:1,EXPAND:2};var U=function(){function A(){this.bits=null,this.leftoverBytes=null,this.guessVideoFrameEnd=!0,this.pidsToStreamIds={},this.pesPacketInfo={},this.startTime=0,this.currentTime=0}var t=A.prototype;return t.connect=function(A,t){this.pesPacketInfo[A]={destination:t,currentLength:0,totalLength:0,pts:0,buffers:[]}},t.write=function(A){if(this.leftoverBytes){var t=A.byteLength+this.leftoverBytes.byteLength;this.bits=new L(t),this.bits.write([this.leftoverBytes,A])}else this.bits=new L(A);for(;this.bits.has(1504)&&this.parsePacket(););var i=this.bits.byteLength-(this.bits.index>>3);this.leftoverBytes=i>0?this.bits.bytes.subarray(this.bits.index>>3):null},t.parsePacket=function(){if(71!==this.bits.read(8)&&!this.resync())return!1;var A=187+(this.bits.index>>3);this.bits.read(1);var t=this.bits.read(1);this.bits.read(1);var i=this.bits.read(13);this.bits.read(2);var e=this.bits.read(2);this.bits.read(4);var s=this.pidsToStreamIds[i];if(t&&s){var o=this.pesPacketInfo[s];o&&o.currentLength&&this.packetComplete(o)}if(1&e){if(2&e){var g=this.bits.read(8);this.bits.skip(g<<3)}if(t&&this.bits.nextBytesAreStartCode()){this.bits.skip(24),s=this.bits.read(8),this.pidsToStreamIds[i]=s;var I=this.bits.read(16);this.bits.skip(8);var a=this.bits.read(2);this.bits.skip(6);var B=this.bits.read(8),r=this.bits.index+(B<<3),C=this.pesPacketInfo[s];if(C){var n=0;if(2&a){this.bits.skip(4);var E=this.bits.read(3);this.bits.skip(1);var Q=this.bits.read(15);this.bits.skip(1);var h=this.bits.read(15);this.bits.skip(1),n=(1073741824*E+32768*Q+h)/9e4,this.currentTime=n,-1===this.startTime&&(this.startTime=n)}var d=I?I-B-3:0;this.packetStart(C,n,d)}this.bits.index=r}if(s){var c=this.pesPacketInfo[s];if(c){var l=this.bits.index>>3,u=!t&&2&e;(this.packetAddData(c,l,A)||this.guessVideoFrameEnd&&u)&&this.packetComplete(c)}}}return this.bits.index=A<<3,!0},t.resync=function(){if(!this.bits.has(9024))return!1;for(var A=this.bits.index>>3,t=0;t<187;t++)if(71===this.bits.bytes[A+t]){for(var i=!0,e=1;e<5;e++)if(71!==this.bits.bytes[A+t+188*e]){i=!1;break}if(i)return this.bits.index=A+t+1<<3,!0}return console.warn("JSMpeg: Possible garbage data. Skipping."),this.bits.skip(1496),!1},t.packetStart=function(A,t,i){A.totalLength=i,A.currentLength=0,A.pts=t},t.packetAddData=function(A,t,i){return A.buffers.push(this.bits.bytes.subarray(t,i)),A.currentLength+=i-t,0!==A.totalLength&&A.currentLength>=A.totalLength},t.packetComplete=function(A){A.destination.write(A.pts,A.buffers),A.totalLength=0,A.currentLength=0,A.buffers=[]},A}();U.STREAM={PACK_HEADER:186,SYSTEM_HEADER:187,PROGRAM_MAP:188,PRIVATE_1:189,PADDING:190,PRIVATE_2:191,AUDIO_1:192,VIDEO_1:224,DIRECTORY:255};var J=function(){function A(A){this.destination=null,this.canPlay=!1,this.collectTimestamps=!A.streaming,this.bytesWritten=0,this.timestamps=[],this.timestampIndex=0,this.startTime=0,this.decodedTime=0,Object.defineProperty(this,"currentTime",{get:this.getCurrentTime})}var t=A.prototype;return t.destroy=function(){},t.connect=function(A){this.destination=A},t.bufferGetIndex=function(){return this.bits.index},t.bufferSetIndex=function(A){this.bits.index=A},t.bufferWrite=function(A){return this.bits.write(A)},t.write=function(A,t){this.collectTimestamps&&(0===this.timestamps.length&&(this.startTime=A,this.decodedTime=A),this.timestamps.push({index:this.bytesWritten<<3,time:A})),this.bytesWritten+=this.bufferWrite(t),this.canPlay=!0},t.seek=function(A){if(this.collectTimestamps){this.timestampIndex=0;for(var t=0;tA);t++)this.timestampIndex=t;var i=this.timestamps[this.timestampIndex];i?(this.bufferSetIndex(i.index),this.decodedTime=i.time):(this.bits.index=0,this.decodedTime=this.startTime)}},t.decode=function(){this.advanceDecodedTime(0)},t.advanceDecodedTime=function(A){if(this.collectTimestamps){for(var t=-1,i=this.bufferGetIndex(),e=this.timestampIndex;ei);e++)t=e;if(-1!==t&&t!==this.timestampIndex)return this.timestampIndex=t,void(this.decodedTime=this.timestamps[this.timestampIndex].time)}this.decodedTime+=A},t.getCurrentTime=function(){return this.decodedTime},A}(),T=function(A){function t(t){var i;(i=A.call(this,t)||this).onDecodeCallback=t.onVideoDecode;var e=t.videoBufferSize||524288,s=t.streaming?L.MODE.EVICT:L.MODE.EXPAND;return i.bits=new L(e,s),i.customIntraQuantMatrix=new Uint8Array(64),i.customNonIntraQuantMatrix=new Uint8Array(64),i.blockData=new Int32Array(64),i.currentFrame=0,i.decodeFirstFrame=!1!==t.decodeFirstFrame,i}u(t,A);var i=t.prototype;return i.write=function(A,i){if(J.prototype.write.call(this,A,i),!this.hasSequenceHeader){if(-1===this.bits.findStartCode(t.START.SEQUENCE))return!1;this.decodeSequenceHeader(),this.decodeFirstFrame&&this.decode()}},i.decode=function(){var A=G();if(!this.hasSequenceHeader)return!1;if(-1===this.bits.findStartCode(t.START.PICTURE))return!1;this.decodePicture(),this.advanceDecodedTime(1/this.frameRate);var i=G()-A;return this.onDecodeCallback&&this.onDecodeCallback(this,i),!0},i.readHuffman=function(A){var t=0;do{t=A[t+this.bits.read(1)]}while(t>=0&&0!==A[t]);return A[t+2]},i.decodeSequenceHeader=function(){var A=this.bits.read(12),i=this.bits.read(12);if(this.bits.skip(4),this.frameRate=t.PICTURE_RATE[this.bits.read(4)],this.bits.skip(30),A===this.width&&i===this.height||(this.width=A,this.height=i,this.initBuffers(),this.destination&&this.destination.resize(A,i)),this.bits.read(1)){for(var e=0;e<64;e++)this.customIntraQuantMatrix[t.ZIG_ZAG[e]]=this.bits.read(8);this.intraQuantMatrix=this.customIntraQuantMatrix}if(this.bits.read(1)){for(var s=0;s<64;s++){var o=t.ZIG_ZAG[s];this.customNonIntraQuantMatrix[o]=this.bits.read(8)}this.nonIntraQuantMatrix=this.customNonIntraQuantMatrix}this.hasSequenceHeader=!0},i.initBuffers=function(){this.intraQuantMatrix=t.DEFAULT_INTRA_QUANT_MATRIX,this.nonIntraQuantMatrix=t.DEFAULT_NON_INTRA_QUANT_MATRIX,this.mbWidth=this.width+15>>4,this.mbHeight=this.height+15>>4,this.mbSize=this.mbWidth*this.mbHeight,this.codedWidth=this.mbWidth<<4,this.codedHeight=this.mbHeight<<4,this.codedSize=this.codedWidth*this.codedHeight,this.halfWidth=this.mbWidth<<3,this.halfHeight=this.mbHeight<<3,this.currentY=new Uint8ClampedArray(this.codedSize),this.currentY32=new Uint32Array(this.currentY.buffer),this.currentCr=new Uint8ClampedArray(this.codedSize>>2),this.currentCr32=new Uint32Array(this.currentCr.buffer),this.currentCb=new Uint8ClampedArray(this.codedSize>>2),this.currentCb32=new Uint32Array(this.currentCb.buffer),this.forwardY=new Uint8ClampedArray(this.codedSize),this.forwardY32=new Uint32Array(this.forwardY.buffer),this.forwardCr=new Uint8ClampedArray(this.codedSize>>2),this.forwardCr32=new Uint32Array(this.forwardCr.buffer),this.forwardCb=new Uint8ClampedArray(this.codedSize>>2),this.forwardCb32=new Uint32Array(this.forwardCb.buffer)},i.decodePicture=function(){if(this.currentFrame++,this.bits.skip(10),this.pictureType=this.bits.read(3),this.bits.skip(16),!(this.pictureType<=0||this.pictureType>=t.PICTURE_TYPE.B)){if(this.pictureType===t.PICTURE_TYPE.PREDICTIVE){if(this.fullPelForward=this.bits.read(1),this.forwardFCode=this.bits.read(3),0===this.forwardFCode)return;this.forwardRSize=this.forwardFCode-1,this.forwardF=1<=t.START.SLICE_FIRST&&A<=t.START.SLICE_LAST;)this.decodeSlice(255&A),A=this.bits.findNextStartCode();if(-1!==A&&this.bits.rewind(32),this.destination&&this.destination.render(this.currentY,this.currentCr,this.currentCb,!0),this.pictureType===t.PICTURE_TYPE.INTRA||this.pictureType===t.PICTURE_TYPE.PREDICTIVE){var i=this.forwardY,e=this.forwardY32,s=this.forwardCr,o=this.forwardCr32,g=this.forwardCb,I=this.forwardCb32;this.forwardY=this.currentY,this.forwardY32=this.currentY32,this.forwardCr=this.currentCr,this.forwardCr32=this.currentCr32,this.forwardCb=this.currentCb,this.forwardCb32=this.currentCb32,this.currentY=i,this.currentY32=e,this.currentCr=s,this.currentCr32=o,this.currentCb=g,this.currentCb32=I}}},i.decodeSlice=function(A){for(this.sliceBegin=!0,this.macroblockAddress=(A-1)*this.mbWidth-1,this.motionFwH=this.motionFwHPrev=0,this.motionFwV=this.motionFwVPrev=0,this.dcPredictorY=128,this.dcPredictorCr=128,this.dcPredictorCb=128,this.quantizerScale=this.bits.read(5);this.bits.read(1);)this.bits.skip(8);do{this.decodeMacroblock()}while(!this.bits.nextBytesAreStartCode())},i.decodeMacroblock=function(){for(var A=0,i=this.readHuffman(t.MACROBLOCK_ADDRESS_INCREMENT);34===i;)i=this.readHuffman(t.MACROBLOCK_ADDRESS_INCREMENT);for(;35===i;)A+=33,i=this.readHuffman(t.MACROBLOCK_ADDRESS_INCREMENT);if(A+=i,this.sliceBegin)this.sliceBegin=!1,this.macroblockAddress+=A;else{if(this.macroblockAddress+A>=this.mbSize)return;for(A>1&&(this.dcPredictorY=128,this.dcPredictorCr=128,this.dcPredictorCb=128,this.pictureType===t.PICTURE_TYPE.PREDICTIVE&&(this.motionFwH=this.motionFwHPrev=0,this.motionFwV=this.motionFwVPrev=0));A>1;)this.macroblockAddress++,this.mbRow=this.macroblockAddress/this.mbWidth|0,this.mbCol=this.macroblockAddress%this.mbWidth,this.copyMacroblock(this.motionFwH,this.motionFwV,this.forwardY,this.forwardCr,this.forwardCb),A--;this.macroblockAddress++}this.mbRow=this.macroblockAddress/this.mbWidth|0,this.mbCol=this.macroblockAddress%this.mbWidth;var e=t.MACROBLOCK_TYPE[this.pictureType];this.macroblockType=this.readHuffman(e),this.macroblockIntra=1&this.macroblockType,this.macroblockMotFw=8&this.macroblockType,0!=(16&this.macroblockType)&&(this.quantizerScale=this.bits.read(5)),this.macroblockIntra?(this.motionFwH=this.motionFwHPrev=0,this.motionFwV=this.motionFwVPrev=0):(this.dcPredictorY=128,this.dcPredictorCr=128,this.dcPredictorCb=128,this.decodeMotionVectors(),this.copyMacroblock(this.motionFwH,this.motionFwV,this.forwardY,this.forwardCr,this.forwardCb));for(var s=0!=(2&this.macroblockType)?this.readHuffman(t.CODE_BLOCK_PATTERN):this.macroblockIntra?63:0,o=0,g=32;o<6;o++)0!=(s&g)&&this.decodeBlock(o),g>>=1},i.decodeMotionVectors=function(){var A,i,e=0;this.macroblockMotFw?(0!==(A=this.readHuffman(t.MOTION))&&1!==this.forwardF?(e=this.bits.read(this.forwardRSize),i=(Math.abs(A)-1<(this.forwardF<<4)-1?this.motionFwHPrev-=this.forwardF<<5:this.motionFwHPrev<-this.forwardF<<4&&(this.motionFwHPrev+=this.forwardF<<5),this.motionFwH=this.motionFwHPrev,this.fullPelForward&&(this.motionFwH<<=1),0!==(A=this.readHuffman(t.MOTION))&&1!==this.forwardF?(e=this.bits.read(this.forwardRSize),i=(Math.abs(A)-1<(this.forwardF<<4)-1?this.motionFwVPrev-=this.forwardF<<5:this.motionFwVPrev<-this.forwardF<<4&&(this.motionFwVPrev+=this.forwardF<<5),this.motionFwV=this.motionFwVPrev,this.fullPelForward&&(this.motionFwV<<=1)):this.pictureType===t.PICTURE_TYPE.PREDICTIVE&&(this.motionFwH=this.motionFwHPrev=0,this.motionFwV=this.motionFwVPrev=0)},i.copyMacroblock=function(A,t,i,e,s){var o,g,I,a,B,r,C,n,E,Q,h,d,c,l,u,w,p,D,m,f=this.currentY32,y=this.currentCb32,R=this.currentCr32;if(g=(o=this.codedWidth)-16,I=A>>1,a=t>>1,B=1==(1&A),r=1==(1&t),C=((this.mbRow<<4)+a)*o+(this.mbCol<<4)+I,E=(n=this.mbRow*o+this.mbCol<<2)+(o<<2),B)if(r)for(;n>2&255,c|=(h=i[++C]+i[C+o])+d+2<<6&65280,c|=h+(d=i[++C]+i[C+o])+2<<14&16711680,h=i[++C]+i[C+o],C++,c|=h+d+2<<22&4278190080,f[n++]=c;n+=g>>2,C+=g-1}else for(;n>1&255,c|=(h=i[C++])+d+1<<7&65280,c|=h+(d=i[C++])+1<<15&16711680,c|=(h=i[C++])+d+1<<23&4278190080,f[n++]=c;n+=g>>2,C+=g-1}else if(r)for(;n>1&255,c|=i[++C]+i[C+o]+1<<7&65280,c|=i[++C]+i[C+o]+1<<15&16711680,c|=i[++C]+i[C+o]+1<<23&4278190080,C++,f[n++]=c;n+=g>>2,C+=g}else for(;n>2,C+=g}if(g=(o=this.halfWidth)-8,I=A/2>>1,a=t/2>>1,B=1==(A/2&1),r=1==(t/2&1),C=((this.mbRow<<3)+a)*o+(this.mbCol<<3)+I,E=(n=this.mbRow*o+this.mbCol<<1)+(o<<1),B)if(r)for(;n>2&255,m=p+(D=s[C]+s[C+o])+2>>2&255,w|=(l=e[++C]+e[C+o])+u+2<<6&65280,m|=(p=s[C]+s[C+o])+D+2<<6&65280,w|=l+(u=e[++C]+e[C+o])+2<<14&16711680,m|=p+(D=s[C]+s[C+o])+2<<14&16711680,l=e[++C]+e[C+o],p=s[C]+s[C+o],C++,w|=l+u+2<<22&4278190080,m|=p+D+2<<22&4278190080,R[n]=w,y[n]=m,n++;n+=g>>2,C+=g-1}else for(;n>1&255,m=p+(D=s[C++])+1>>1&255,w|=(l=e[C])+u+1<<7&65280,m|=(p=s[C++])+D+1<<7&65280,w|=l+(u=e[C])+1<<15&16711680,m|=p+(D=s[C++])+1<<15&16711680,w|=(l=e[C])+u+1<<23&4278190080,m|=(p=s[C++])+D+1<<23&4278190080,R[n]=w,y[n]=m,n++;n+=g>>2,C+=g-1}else if(r)for(;n>1&255,m=s[C]+s[C+o]+1>>1&255,w|=e[++C]+e[C+o]+1<<7&65280,m|=s[C]+s[C+o]+1<<7&65280,w|=e[++C]+e[C+o]+1<<15&16711680,m|=s[C]+s[C+o]+1<<15&16711680,w|=e[++C]+e[C+o]+1<<23&4278190080,m|=s[C]+s[C+o]+1<<23&4278190080,C++,R[n]=w,y[n]=m,n++;n+=g>>2,C+=g}else for(;n>2,C+=g}},i.decodeBlock=function(A){var i,e=0;if(this.macroblockIntra){var s,o;if(A<4?(s=this.dcPredictorY,o=this.readHuffman(t.DCT_DC_SIZE_LUMINANCE)):(s=4===A?this.dcPredictorCr:this.dcPredictorCb,o=this.readHuffman(t.DCT_DC_SIZE_CHROMINANCE)),o>0){var g=this.bits.read(o);this.blockData[0]=0!=(g&1<0&&0===this.bits.read(1))break;65535===n?(C=this.bits.read(6),0===(r=this.bits.read(8))?r=this.bits.read(8):128===r?r=this.bits.read(8)-256:r>128&&(r-=256)):(C=n>>8,r=255&n,this.bits.read(1)&&(r=-r)),e+=C;var E=t.ZIG_ZAG[e];e++,r<<=1,this.macroblockIntra||(r+=r<0?-1:1),0==(1&(r=r*this.quantizerScale*i[E]>>4))&&(r-=r>0?1:-1),r>2047?r=2047:r<-2048&&(r=-2048),this.blockData[E]=r*t.PREMULTIPLIER_MATRIX[E]}A<4?(I=this.currentY,B=this.codedWidth-8,a=this.mbRow*this.codedWidth+this.mbCol<<4,0!=(1&A)&&(a+=8),0!=(2&A)&&(a+=this.codedWidth<<3)):(I=4===A?this.currentCb:this.currentCr,B=(this.codedWidth>>1)-8,a=(this.mbRow*this.codedWidth<<2)+(this.mbCol<<3)),this.macroblockIntra?1===e?(t.CopyValueToDestination(this.blockData[0]+128>>8,I,a,B),this.blockData[0]=0):(t.IDCT(this.blockData),t.CopyBlockToDestination(this.blockData,I,a,B),S(this.blockData,0)):1===e?(t.AddValueToDestination(this.blockData[0]+128>>8,I,a,B),this.blockData[0]=0):(t.IDCT(this.blockData),t.AddBlockToDestination(this.blockData,I,a,B),S(this.blockData,0)),e=0},t.CopyBlockToDestination=function(A,t,i,e){for(var s=0;s<64;s+=8,i+=e+8)t[i+0]=A[s+0],t[i+1]=A[s+1],t[i+2]=A[s+2],t[i+3]=A[s+3],t[i+4]=A[s+4],t[i+5]=A[s+5],t[i+6]=A[s+6],t[i+7]=A[s+7]},t.AddBlockToDestination=function(A,t,i,e){for(var s=0;s<64;s+=8,i+=e+8)t[i+0]+=A[s+0],t[i+1]+=A[s+1],t[i+2]+=A[s+2],t[i+3]+=A[s+3],t[i+4]+=A[s+4],t[i+5]+=A[s+5],t[i+6]+=A[s+6],t[i+7]+=A[s+7]},t.CopyValueToDestination=function(A,t,i,e){for(var s=0;s<64;s+=8,i+=e+8)t[i+0]=A,t[i+1]=A,t[i+2]=A,t[i+3]=A,t[i+4]=A,t[i+5]=A,t[i+6]=A,t[i+7]=A},t.AddValueToDestination=function(A,t,i,e){for(var s=0;s<64;s+=8,i+=e+8)t[i+0]+=A,t[i+1]+=A,t[i+2]+=A,t[i+3]+=A,t[i+4]+=A,t[i+5]+=A,t[i+6]+=A,t[i+7]+=A},t.IDCT=function(A){for(var t,i,e,s,o,g,I,a,B,r,C,n,E,Q,h,d,c,l,u=0;u<8;++u)t=A[32+u],i=A[16+u]+A[48+u],e=A[40+u]-A[24+u],g=A[8+u]+A[56+u],I=A[24+u]+A[40+u],B=(E=(473*(s=A[8+u]-A[56+u])-196*e+128>>8)-(o=g+I))-(362*(g-I)+128>>8),Q=(r=(a=A[0+u])-t)+(C=(362*(A[16+u]-A[48+u])+128>>8)-i),h=(n=a+t)+i,d=r-C,c=n-i,l=-B-(473*e+196*s+128>>8),A[0+u]=o+h,A[8+u]=E+Q,A[16+u]=d-B,A[24+u]=c-l,A[32+u]=c+l,A[40+u]=B+d,A[48+u]=Q-E,A[56+u]=h-o;for(var w=0;w<64;w+=8)t=A[4+w],i=A[2+w]+A[6+w],e=A[5+w]-A[3+w],g=A[1+w]+A[7+w],I=A[3+w]+A[5+w],B=(E=(473*(s=A[1+w]-A[7+w])-196*e+128>>8)-(o=g+I))-(362*(g-I)+128>>8),Q=(r=(a=A[0+w])-t)+(C=(362*(A[2+w]-A[6+w])+128>>8)-i),h=(n=a+t)+i,d=r-C,c=n-i,l=-B-(473*e+196*s+128>>8),A[0+w]=o+h+128>>8,A[1+w]=E+Q+128>>8,A[2+w]=d-B+128>>8,A[3+w]=c-l+128>>8,A[4+w]=c+l+128>>8,A[5+w]=B+d+128>>8,A[6+w]=Q-E+128>>8,A[7+w]=h-o+128>>8},t}(J);T.prototype.frameRate=30,T.prototype.currentY=null,T.prototype.currentCr=null,T.prototype.currentCb=null,T.prototype.pictureType=0,T.prototype.forwardY=null,T.prototype.forwardCr=null,T.prototype.forwardCb=null,T.prototype.fullPelForward=!1,T.prototype.forwardFCode=0,T.prototype.forwardRSize=0,T.prototype.forwardF=0,T.prototype.quantizerScale=0,T.prototype.sliceBegin=!1,T.prototype.macroblockAddress=0,T.prototype.mbRow=0,T.prototype.mbCol=0,T.prototype.macroblockType=0,T.prototype.macroblockIntra=!1,T.prototype.macroblockMotFw=!1,T.prototype.motionFwH=0,T.prototype.motionFwV=0,T.prototype.motionFwHPrev=0,T.prototype.motionFwVPrev=0,T.prototype.dcPredictorY=0,T.prototype.dcPredictorCr=0,T.prototype.dcPredictorCb=0,T.prototype.blockData=null,T.PICTURE_RATE=[0,23.976,24,25,29.97,30,50,59.94,60,0,0,0,0,0,0,0],T.ZIG_ZAG=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),T.DEFAULT_INTRA_QUANT_MATRIX=new Uint8Array([8,16,19,22,26,27,29,34,16,16,22,24,27,29,34,37,19,22,26,27,29,34,34,38,22,22,26,27,29,34,37,40,22,26,27,29,32,35,40,48,26,27,29,32,35,40,48,58,26,27,29,34,38,46,56,69,27,29,35,38,46,56,69,83]),T.DEFAULT_NON_INTRA_QUANT_MATRIX=new Uint8Array([16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16]),T.PREMULTIPLIER_MATRIX=new Uint8Array([32,44,42,38,32,25,17,9,44,62,58,52,44,35,24,12,42,58,55,49,42,33,23,12,38,52,49,44,38,30,20,10,32,44,42,38,32,25,17,9,25,35,33,30,25,20,14,7,17,24,23,20,17,14,9,5,9,12,12,10,9,7,5,2]),T.MACROBLOCK_ADDRESS_INCREMENT=new Int16Array([3,6,0,9,12,0,0,0,1,15,18,0,21,24,0,27,30,0,33,36,0,0,0,3,0,0,2,39,42,0,45,48,0,0,0,5,0,0,4,51,54,0,57,60,0,0,0,7,0,0,6,63,66,0,69,72,0,75,78,0,81,84,0,-1,87,0,-1,90,0,93,96,0,99,102,0,105,108,0,111,114,0,0,0,9,0,0,8,117,120,0,123,126,0,129,132,0,135,138,0,0,0,15,0,0,14,0,0,13,0,0,12,0,0,11,0,0,10,141,-1,0,-1,144,0,147,150,0,153,156,0,159,162,0,165,168,0,171,174,0,177,180,0,183,-1,0,-1,186,0,189,192,0,195,198,0,201,204,0,207,210,0,213,216,0,219,222,0,0,0,21,0,0,20,0,0,19,0,0,18,0,0,17,0,0,16,0,0,35,0,0,34,0,0,33,0,0,32,0,0,31,0,0,30,0,0,29,0,0,28,0,0,27,0,0,26,0,0,25,0,0,24,0,0,23,0,0,22]),T.MACROBLOCK_TYPE_INTRA=new Int8Array([3,6,0,-1,9,0,0,0,1,0,0,17]),T.MACROBLOCK_TYPE_PREDICTIVE=new Int8Array([3,6,0,9,12,0,0,0,10,15,18,0,0,0,2,21,24,0,0,0,8,27,30,0,33,36,0,-1,39,0,0,0,18,0,0,26,0,0,1,0,0,17]),T.MACROBLOCK_TYPE_B=new Int8Array([3,6,0,9,15,0,12,18,0,24,21,0,0,0,12,27,30,0,0,0,14,39,42,0,36,33,0,0,0,4,0,0,6,54,48,0,45,51,0,0,0,8,0,0,10,-1,57,0,0,0,1,60,63,0,0,0,30,0,0,17,0,0,22,0,0,26]),T.MACROBLOCK_TYPE=[null,T.MACROBLOCK_TYPE_INTRA,T.MACROBLOCK_TYPE_PREDICTIVE,T.MACROBLOCK_TYPE_B],T.CODE_BLOCK_PATTERN=new Int16Array([6,3,0,9,18,0,12,15,0,24,33,0,36,39,0,27,21,0,30,42,0,60,57,0,54,48,0,69,51,0,81,75,0,63,84,0,45,66,0,72,78,0,0,0,60,105,120,0,132,144,0,114,108,0,126,141,0,87,93,0,117,96,0,0,0,32,135,138,0,99,123,0,129,102,0,0,0,4,90,111,0,0,0,8,0,0,16,0,0,44,150,168,0,0,0,28,0,0,52,0,0,62,183,177,0,156,180,0,0,0,1,165,162,0,0,0,61,0,0,56,171,174,0,0,0,2,0,0,40,153,186,0,0,0,48,192,189,0,147,159,0,0,0,20,0,0,12,240,249,0,0,0,63,231,225,0,195,219,0,252,198,0,0,0,24,0,0,36,0,0,3,207,261,0,243,237,0,204,213,0,210,234,0,201,228,0,216,222,0,258,255,0,264,246,0,-1,282,0,285,291,0,0,0,33,0,0,9,318,330,0,306,348,0,0,0,5,0,0,10,279,267,0,0,0,6,0,0,18,0,0,17,0,0,34,339,357,0,309,312,0,270,276,0,327,321,0,351,354,0,303,297,0,294,288,0,300,273,0,342,345,0,315,324,0,336,333,0,363,375,0,0,0,41,0,0,14,0,0,21,372,366,0,360,369,0,0,0,11,0,0,19,0,0,7,0,0,35,0,0,13,0,0,50,0,0,49,0,0,58,0,0,37,0,0,25,0,0,45,0,0,57,0,0,26,0,0,29,0,0,38,0,0,53,0,0,23,0,0,43,0,0,46,0,0,42,0,0,22,0,0,54,0,0,51,0,0,15,0,0,30,0,0,39,0,0,47,0,0,55,0,0,27,0,0,59,0,0,31]),T.MOTION=new Int16Array([3,6,0,12,9,0,0,0,0,18,15,0,24,21,0,0,0,-1,0,0,1,27,30,0,36,33,0,0,0,2,0,0,-2,42,45,0,48,39,0,60,54,0,0,0,3,0,0,-3,51,57,0,-1,69,0,81,75,0,78,63,0,72,66,0,96,84,0,87,93,0,-1,99,0,108,105,0,0,0,-4,90,102,0,0,0,4,0,0,-7,0,0,5,111,123,0,0,0,-5,0,0,7,114,120,0,126,117,0,0,0,-6,0,0,6,153,162,0,150,147,0,135,138,0,156,141,0,129,159,0,132,144,0,0,0,10,0,0,9,0,0,8,0,0,-8,171,198,0,0,0,-9,180,192,0,168,183,0,165,186,0,174,189,0,0,0,-10,177,195,0,0,0,12,0,0,16,0,0,13,0,0,14,0,0,11,0,0,15,0,0,-16,0,0,-12,0,0,-14,0,0,-15,0,0,-11,0,0,-13]),T.DCT_DC_SIZE_LUMINANCE=new Int8Array([6,3,0,18,15,0,9,12,0,0,0,1,0,0,2,27,24,0,21,30,0,0,0,0,36,33,0,0,0,4,0,0,3,39,42,0,0,0,5,0,0,6,48,45,0,51,-1,0,0,0,7,0,0,8]),T.DCT_DC_SIZE_CHROMINANCE=new Int8Array([6,3,0,12,9,0,18,15,0,24,21,0,0,0,2,0,0,1,0,0,0,30,27,0,0,0,3,36,33,0,0,0,4,42,39,0,0,0,5,48,45,0,0,0,6,51,-1,0,0,0,7,0,0,8]),T.DCT_COEFF=new Int32Array([3,6,0,12,9,0,0,0,1,21,24,0,18,15,0,39,27,0,33,30,0,42,36,0,0,0,257,60,66,0,54,63,0,48,57,0,0,0,513,51,45,0,0,0,2,0,0,3,81,75,0,87,93,0,72,78,0,96,90,0,0,0,1025,69,84,0,0,0,769,0,0,258,0,0,1793,0,0,65535,0,0,1537,111,108,0,0,0,1281,105,102,0,117,114,0,99,126,0,120,123,0,156,150,0,162,159,0,144,147,0,129,135,0,138,132,0,0,0,2049,0,0,4,0,0,514,0,0,2305,153,141,0,165,171,0,180,168,0,177,174,0,183,186,0,0,0,2561,0,0,3329,0,0,6,0,0,259,0,0,5,0,0,770,0,0,2817,0,0,3073,228,225,0,201,210,0,219,213,0,234,222,0,216,231,0,207,192,0,204,189,0,198,195,0,243,261,0,273,240,0,246,237,0,249,258,0,279,276,0,252,255,0,270,282,0,264,267,0,0,0,515,0,0,260,0,0,7,0,0,1026,0,0,1282,0,0,4097,0,0,3841,0,0,3585,315,321,0,333,342,0,312,291,0,375,357,0,288,294,0,-1,369,0,285,303,0,318,363,0,297,306,0,339,309,0,336,348,0,330,300,0,372,345,0,351,366,0,327,354,0,360,324,0,381,408,0,417,420,0,390,378,0,435,438,0,384,387,0,0,0,2050,396,402,0,465,462,0,0,0,8,411,399,0,429,432,0,453,414,0,426,423,0,0,0,10,0,0,9,0,0,11,0,0,5377,0,0,1538,0,0,771,0,0,5121,0,0,1794,0,0,4353,0,0,4609,0,0,4865,444,456,0,0,0,1027,459,450,0,0,0,261,393,405,0,0,0,516,447,441,0,516,519,0,486,474,0,510,483,0,504,498,0,471,537,0,507,501,0,522,513,0,534,531,0,468,477,0,492,495,0,549,546,0,525,528,0,0,0,263,0,0,2562,0,0,2306,0,0,5633,0,0,5889,0,0,6401,0,0,6145,0,0,1283,0,0,772,0,0,13,0,0,12,0,0,14,0,0,15,0,0,517,0,0,6657,0,0,262,540,543,0,480,489,0,588,597,0,0,0,27,609,555,0,606,603,0,0,0,19,0,0,22,591,621,0,0,0,18,573,576,0,564,570,0,0,0,20,552,582,0,0,0,21,558,579,0,0,0,23,612,594,0,0,0,25,0,0,24,600,615,0,0,0,31,0,0,30,0,0,28,0,0,29,0,0,26,0,0,17,0,0,16,567,618,0,561,585,0,654,633,0,0,0,37,645,648,0,0,0,36,630,636,0,0,0,34,639,627,0,663,666,0,657,624,0,651,642,0,669,660,0,0,0,35,0,0,267,0,0,40,0,0,268,0,0,266,0,0,32,0,0,264,0,0,265,0,0,38,0,0,269,0,0,270,0,0,33,0,0,39,0,0,7937,0,0,6913,0,0,7681,0,0,4098,0,0,7425,0,0,7169,0,0,271,0,0,274,0,0,273,0,0,272,0,0,1539,0,0,2818,0,0,3586,0,0,3330,0,0,3074,0,0,3842]),T.PICTURE_TYPE={INTRA:1,PREDICTIVE:2,B:3},T.START={SEQUENCE:179,SLICE_FIRST:1,SLICE_LAST:175,PICTURE:0,EXTENSION:181,USER_DATA:178};var v=function(A){function t(t){var i;return(i=A.call(this,t)||this).onDecodeCallback=t.onVideoDecode,i.module=t.wasmModule,i.bufferSize=t.videoBufferSize||524288,i.bufferMode=t.streaming?L.MODE.EVICT:L.MODE.EXPAND,i.decodeFirstFrame=!1!==t.decodeFirstFrame,i.hasSequenceHeader=!1,i}u(t,A);var i=t.prototype;return i.initializeWasmDecoder=function(){this.module.instance?(this.instance=this.module.instance,this.functions=this.module.instance.exports,this.decoder=this.functions._mpeg1_decoder_create(this.bufferSize,this.bufferMode)):console.warn("JSMpeg: WASM module not compiled yet")},i.destroy=function(){this.decoder&&this.functions._mpeg1_decoder_destroy(this.decoder)},i.bufferGetIndex=function(){if(this.decoder)return this.functions._mpeg1_decoder_get_index(this.decoder)},i.bufferSetIndex=function(A){this.decoder&&this.functions._mpeg1_decoder_set_index(this.decoder,A)},i.bufferWrite=function(A){this.decoder||this.initializeWasmDecoder();for(var t=0,i=0;i>2)),g=this.instance.heapU8.subarray(e,e+(this.codedSize>>2));this.destination.render(s,o,g,!1)}this.advanceDecodedTime(1/this.frameRate);var I=G()-A;return this.onDecodeCallback&&this.onDecodeCallback(this,I),!0},t}(J),x=function(A){function t(i){var e;(e=A.call(this,i)||this).onDecodeCallback=i.onAudioDecode;var s=i.audioBufferSize||131072,o=i.streaming?L.MODE.EVICT:L.MODE.EXPAND;e.bits=new L(s,o),e.left=new Float32Array(1152),e.right=new Float32Array(1152),e.sampleRate=44100,e.D=new Float32Array(1024),e.D.set(t.SYNTHESIS_WINDOW,0),e.D.set(t.SYNTHESIS_WINDOW,512),e.V=[new Float32Array(1024),new Float32Array(1024)],e.U=new Int32Array(32),e.VPos=0,e.allocation=[new Array(32),new Array(32)],e.scaleFactorInfo=[new Uint8Array(32),new Uint8Array(32)],e.scaleFactor=[new Array(32),new Array(32)],e.sample=[new Array(32),new Array(32)];for(var g=0;g<2;g++)for(var I=0;I<32;I++)e.scaleFactor[g][I]=[0,0,0],e.sample[g][I]=[0,0,0];return e}u(t,A);var i=t.prototype;return i.decode=function(){var A=G(),t=this.bits.index>>3;if(t>=this.bits.byteLength)return!1;var i=this.decodeFrame(this.left,this.right);if(this.bits.index=t+i<<3,!i)return!1;this.destination&&this.destination.play(this.sampleRate,this.left,this.right),this.advanceDecodedTime(this.left.length/this.sampleRate);var e=G()-A;return this.onDecodeCallback&&this.onDecodeCallback(this,e),!0},i.getCurrentTime=function(){var A=this.destination?this.destination.enqueuedTime:0;return this.decodedTime-A},i.decodeFrame=function(A,i){var e=this.bits.read(11),s=this.bits.read(2),o=this.bits.read(2),g=!this.bits.read(1);if(e!==t.FRAME_SYNC||s!==t.VERSION.MPEG_1||o!==t.LAYER.II)return 0;var I=this.bits.read(4)-1;if(I>13)return 0;var a=this.bits.read(2),B=t.SAMPLE_RATE[a];if(3===a)return 0;s===t.VERSION.MPEG_2&&(a+=4,I+=14);var r=this.bits.read(1);this.bits.read(1);var C=this.bits.read(2),n=0;C===t.MODE.JOINT_STEREO?n=this.bits.read(2)+1<<2:(this.bits.skip(2),n=C===t.MODE.MONO?0:32),this.bits.skip(4),g&&this.bits.skip(16);var E=144e3*t.BIT_RATE[I]/(B=t.SAMPLE_RATE[a])+r|0,Q=0,h=0;if(s===t.VERSION.MPEG_2)Q=2,h=30;else{var d=C===t.MODE.MONO?0:1,c=t.QUANT_LUT_STEP_1[d][I];h=63&(Q=t.QUANT_LUT_STEP_2[c][a]),Q>>=6}n>h&&(n=h);for(var l=0;l>1),U=this.VPos%128>>1;U<1024;){for(var J=0;J<32;++J)this.U[J]+=this.D[L++]*this.V[N][U++];U+=96,L+=32}for(U=1120-U,L-=480;U<1024;){for(var T=0;T<32;++T)this.U[T]+=this.D[L++]*this.V[N][U++];U+=96,L+=32}for(var v=0===N?A:i,x=0;x<32;x++)v[R+x]=this.U[x]/2147418112}R+=32}}return this.sampleRate=B,E},i.readAllocation=function(A,i){var e=t.QUANT_LUT_STEP_3[i][A],s=t.QUANT_LUT_STEP4[15&e][this.bits.read(e>>4)];return s?t.QUANT_TAB[s-1]:0},i.readSamples=function(A,i,e){var s=this.allocation[A][i],o=this.scaleFactor[A][i][e],g=this.sample[A][i],I=0;if(s){if(63===o)o=0;else{var a=o/3|0;o=t.SCALEFACTOR_BASE[o%3]+(1<>1)>>a}var B=s.levels;s.group?(I=this.bits.read(s.bits),g[0]=I%B,I=I/B|0,g[1]=I%B,g[2]=I/B|0):(g[0]=this.bits.read(s.bits),g[1]=this.bits.read(s.bits),g[2]=this.bits.read(s.bits));var r=65536/(B+1)|0;I=((B=(B+1>>1)-1)-g[0])*r,g[0]=I*(o>>12)+(I*(4095&o)+2048>>12)>>12,I=(B-g[1])*r,g[1]=I*(o>>12)+(I*(4095&o)+2048>>12)>>12,I=(B-g[2])*r,g[2]=I*(o>>12)+(I*(4095&o)+2048>>12)>>12}else g[0]=g[1]=g[2]=0},t.MatrixTransform=function(A,t,i,e){var s,o,g,I,a,B,r,C,n,E,Q,h,d,c,l,u,w,p,D,m,f,y,R,k,b,G,S,F,M,q,N,L,U;s=A[0][t]+A[31][t],o=.500602998235*(A[0][t]-A[31][t]),g=A[1][t]+A[30][t],I=.505470959898*(A[1][t]-A[30][t]),a=A[2][t]+A[29][t],B=.515447309923*(A[2][t]-A[29][t]),r=A[3][t]+A[28][t],C=.53104259109*(A[3][t]-A[28][t]),n=A[4][t]+A[27][t],E=.553103896034*(A[4][t]-A[27][t]),Q=A[5][t]+A[26][t],h=.582934968206*(A[5][t]-A[26][t]),d=A[6][t]+A[25][t],c=.622504123036*(A[6][t]-A[25][t]),l=A[7][t]+A[24][t],u=.674808341455*(A[7][t]-A[24][t]),w=A[8][t]+A[23][t],p=.744536271002*(A[8][t]-A[23][t]),D=A[9][t]+A[22][t],m=.839349645416*(A[9][t]-A[22][t]),f=A[10][t]+A[21][t],y=.972568237862*(A[10][t]-A[21][t]),R=A[11][t]+A[20][t],k=1.16943993343*(A[11][t]-A[20][t]),b=A[12][t]+A[19][t],G=1.48416461631*(A[12][t]-A[19][t]),S=A[13][t]+A[18][t],F=2.05778100995*(A[13][t]-A[18][t]),M=A[14][t]+A[17][t],q=3.40760841847*(A[14][t]-A[17][t]),U=s+(N=A[15][t]+A[16][t]),N=.502419286188*(s-N),s=g+M,M=.52249861494*(g-M),g=a+S,S=.566944034816*(a-S),a=r+b,b=.64682178336*(r-b),r=n+R,R=.788154623451*(n-R),n=Q+f,f=1.06067768599*(Q-f),Q=d+D,D=1.72244709824*(d-D),d=l+w,w=5.10114861869*(l-w),l=U+d,d=.509795579104*(U-d),U=s+Q,s=.601344886935*(s-Q),Q=g+n,n=.899976223136*(g-n),g=a+r,r=2.56291544774*(a-r),a=l+g,l=.541196100146*(l-g),g=U+Q,Q=1.30656296488*(U-Q),U=a+g,a=.707106781187*(a-g),g=l+Q,g+=l=.707106781187*(l-Q),Q=d+r,d=.541196100146*(d-r),r=s+n,n=1.30656296488*(s-n),s=Q+r,r=.707106781187*(Q-r),Q=d+n,s+=Q+=d=.707106781187*(d-n),Q+=r,r+=d,n=N+w,N=.509795579104*(N-w),w=M+D,M=.601344886935*(M-D),D=S+f,f=.899976223136*(S-f),S=b+R,R=2.56291544774*(b-R),b=n+S,n=.541196100146*(n-S),S=w+D,D=1.30656296488*(w-D),w=b+S,S=.707106781187*(b-S),b=n+D,D=.707106781187*(n-D),n=N+R,N=.541196100146*(N-R),R=M+f,f=1.30656296488*(M-f),M=n+R,R=.707106781187*(n-R),n=N+f,w+=M+=n+=N=.707106781187*(N-f),M+=b+=D,b+=n+=R,n+=S,S+=R+=N,R+=D,D+=N,f=o+(L=10.1900081235*(A[15][t]-A[16][t])),o=.502419286188*(o-L),L=I+q,I=.52249861494*(I-q),q=B+F,F=.566944034816*(B-F),B=C+G,C=.64682178336*(C-G),G=E+k,E=.788154623451*(E-k),k=h+y,y=1.06067768599*(h-y),h=c+m,m=1.72244709824*(c-m),c=u+p,u=5.10114861869*(u-p),p=f+c,c=.509795579104*(f-c),f=L+h,L=.601344886935*(L-h),h=q+k,k=.899976223136*(q-k),q=B+G,G=2.56291544774*(B-G),B=p+q,p=.541196100146*(p-q),q=f+h,h=1.30656296488*(f-h),f=B+q,q=.707106781187*(B-q),B=p+h,h=.707106781187*(p-h),p=c+G,G=.541196100146*(c-G),c=L+k,k=1.30656296488*(L-k),L=p+c,c=.707106781187*(p-c),p=G+k,L+=p+=k=.707106781187*(G-k),p+=c,G=c+k,c=o+u,o=.509795579104*(o-u),u=I+m,I=.601344886935*(I-m),m=F+y,y=.899976223136*(F-y),F=C+E,E=2.56291544774*(C-E),C=c+F,c=.541196100146*(c-F),F=u+m,m=1.30656296488*(u-m),u=C+F,F=.707106781187*(C-F),C=c+m,m=.707106781187*(c-m),c=o+E,o=.541196100146*(o-E),E=I+y,y=1.30656296488*(I-y),I=c+E,E=.707106781187*(c-E),c=o+y,f+=u+=I+=c+=o=.707106781187*(o-y),u+=L,L+=I+=C+=m,I+=B+=h,B+=C+=c+=E,C+=p,p+=c+=F,c+=q,q+=F+=E+=o,F+=G,G+=E+=m,E+=h,h+=m+=o,m+=k,k+=o,i[e+48]=-U,i[e+49]=i[e+47]=-f,i[e+50]=i[e+46]=-w,i[e+51]=i[e+45]=-u,i[e+52]=i[e+44]=-s,i[e+53]=i[e+43]=-L,i[e+54]=i[e+42]=-M,i[e+55]=i[e+41]=-I,i[e+56]=i[e+40]=-g,i[e+57]=i[e+39]=-B,i[e+58]=i[e+38]=-b,i[e+59]=i[e+37]=-C,i[e+60]=i[e+36]=-Q,i[e+61]=i[e+35]=-p,i[e+62]=i[e+34]=-n,i[e+63]=i[e+33]=-c,i[e+32]=-a,i[e+0]=a,i[e+31]=-q,i[e+1]=q,i[e+30]=-S,i[e+2]=S,i[e+29]=-F,i[e+3]=F,i[e+28]=-r,i[e+4]=r,i[e+27]=-G,i[e+5]=G,i[e+26]=-R,i[e+6]=R,i[e+25]=-E,i[e+7]=E,i[e+24]=-l,i[e+8]=l,i[e+23]=-h,i[e+9]=h,i[e+22]=-D,i[e+10]=D,i[e+21]=-m,i[e+11]=m,i[e+20]=-d,i[e+12]=d,i[e+19]=-k,i[e+13]=k,i[e+18]=-N,i[e+14]=N,i[e+17]=-o,i[e+15]=o,i[e+16]=0},t}(J);x.FRAME_SYNC=2047,x.VERSION={MPEG_2_5:0,MPEG_2:2,MPEG_1:3},x.LAYER={III:1,II:2,I:3},x.MODE={STEREO:0,JOINT_STEREO:1,DUAL_CHANNEL:2,MONO:3},x.SAMPLE_RATE=new Uint16Array([44100,48e3,32e3,0,22050,24e3,16e3,0]),x.BIT_RATE=new Uint16Array([32,48,56,64,80,96,112,128,160,192,224,256,320,384,8,16,24,32,40,48,56,64,80,96,112,128,144,160]),x.SCALEFACTOR_BASE=new Uint32Array([33554432,26632170,21137968]),x.SYNTHESIS_WINDOW=new Float32Array([0,-.5,-.5,-.5,-.5,-.5,-.5,-1,-1,-1,-1,-1.5,-1.5,-2,-2,-2.5,-2.5,-3,-3.5,-3.5,-4,-4.5,-5,-5.5,-6.5,-7,-8,-8.5,-9.5,-10.5,-12,-13,-14.5,-15.5,-17.5,-19,-20.5,-22.5,-24.5,-26.5,-29,-31.5,-34,-36.5,-39.5,-42.5,-45.5,-48.5,-52,-55.5,-58.5,-62.5,-66,-69.5,-73.5,-77,-80.5,-84.5,-88,-91.5,-95,-98,-101,-104,106.5,109,111,112.5,113.5,114,114,113.5,112,110.5,107.5,104,100,94.5,88.5,81.5,73,63.5,53,41.5,28.5,14.5,-1,-18,-36,-55.5,-76.5,-98.5,-122,-147,-173.5,-200.5,-229.5,-259.5,-290.5,-322.5,-355.5,-389.5,-424,-459.5,-495.5,-532,-568.5,-605,-641.5,-678,-714,-749,-783.5,-817,-849,-879.5,-908.5,-935,-959.5,-981,-1000.5,-1016,-1028.5,-1037.5,-1042.5,-1043.5,-1040,-1031.5,1018.5,1e3,976,946.5,911,869.5,822,767.5,707,640,565.5,485,397,302.5,201,92.5,-22.5,-144,-272.5,-407,-547.5,-694,-846,-1003,-1165,-1331.5,-1502,-1675.5,-1852.5,-2031.5,-2212.5,-2394,-2576.5,-2758.5,-2939.5,-3118.5,-3294.5,-3467.5,-3635.5,-3798.5,-3955,-4104.5,-4245.5,-4377.5,-4499,-4609.5,-4708,-4792.5,-4863.5,-4919,-4958,-4979.5,-4983,-4967.5,-4931.5,-4875,-4796,-4694.5,-4569.5,-4420,-4246,-4046,-3820,-3567,3287,2979.5,2644,2280.5,1888,1467.5,1018.5,541,35,-499,-1061,-1650,-2266.5,-2909,-3577,-4270,-4987.5,-5727.5,-6490,-7274,-8077.5,-8899.5,-9739,-10594.5,-11464.5,-12347,-13241,-14144.5,-15056,-15973.5,-16895.5,-17820,-18744.5,-19668,-20588,-21503,-22410.5,-23308.5,-24195,-25068.5,-25926.5,-26767,-27589,-28389,-29166.5,-29919,-30644.5,-31342,-32009.5,-32645,-33247,-33814.5,-34346,-34839.5,-35295,-35710,-36084.5,-36417.5,-36707.5,-36954,-37156.5,-37315,-37428,-37496,37519,37496,37428,37315,37156.5,36954,36707.5,36417.5,36084.5,35710,35295,34839.5,34346,33814.5,33247,32645,32009.5,31342,30644.5,29919,29166.5,28389,27589,26767,25926.5,25068.5,24195,23308.5,22410.5,21503,20588,19668,18744.5,17820,16895.5,15973.5,15056,14144.5,13241,12347,11464.5,10594.5,9739,8899.5,8077.5,7274,6490,5727.5,4987.5,4270,3577,2909,2266.5,1650,1061,499,-35,-541,-1018.5,-1467.5,-1888,-2280.5,-2644,-2979.5,3287,3567,3820,4046,4246,4420,4569.5,4694.5,4796,4875,4931.5,4967.5,4983,4979.5,4958,4919,4863.5,4792.5,4708,4609.5,4499,4377.5,4245.5,4104.5,3955,3798.5,3635.5,3467.5,3294.5,3118.5,2939.5,2758.5,2576.5,2394,2212.5,2031.5,1852.5,1675.5,1502,1331.5,1165,1003,846,694,547.5,407,272.5,144,22.5,-92.5,-201,-302.5,-397,-485,-565.5,-640,-707,-767.5,-822,-869.5,-911,-946.5,-976,-1e3,1018.5,1031.5,1040,1043.5,1042.5,1037.5,1028.5,1016,1000.5,981,959.5,935,908.5,879.5,849,817,783.5,749,714,678,641.5,605,568.5,532,495.5,459.5,424,389.5,355.5,322.5,290.5,259.5,229.5,200.5,173.5,147,122,98.5,76.5,55.5,36,18,1,-14.5,-28.5,-41.5,-53,-63.5,-73,-81.5,-88.5,-94.5,-100,-104,-107.5,-110.5,-112,-113.5,-114,-114,-113.5,-112.5,-111,-109,106.5,104,101,98,95,91.5,88,84.5,80.5,77,73.5,69.5,66,62.5,58.5,55.5,52,48.5,45.5,42.5,39.5,36.5,34,31.5,29,26.5,24.5,22.5,20.5,19,17.5,15.5,14.5,13,12,10.5,9.5,8.5,8,7,6.5,5.5,5,4.5,4,3.5,3.5,3,2.5,2.5,2,2,1.5,1.5,1,1,1,1,.5,.5,.5,.5,.5,.5]),x.QUANT_LUT_STEP_1=[[0,0,1,1,1,2,2,2,2,2,2,2,2,2],[0,0,0,0,0,0,1,1,1,2,2,2,2,2]],x.QUANT_TAB={A:91,B:94,C:8,D:12},x.QUANT_LUT_STEP_2=[[x.QUANT_TAB.C,x.QUANT_TAB.C,x.QUANT_TAB.D],[x.QUANT_TAB.A,x.QUANT_TAB.A,x.QUANT_TAB.A],[x.QUANT_TAB.B,x.QUANT_TAB.A,x.QUANT_TAB.B]],x.QUANT_LUT_STEP_3=[[68,68,52,52,52,52,52,52,52,52,52,52],[67,67,67,66,66,66,66,66,66,66,66,49,49,49,49,49,49,49,49,49,49,49,49,32,32,32,32,32,32,32],[69,69,69,69,52,52,52,52,52,52,52,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36]],x.QUANT_LUT_STEP4=[[0,1,2,17],[0,1,2,3,4,5,6,17],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,17],[0,1,3,5,6,7,8,9,10,11,12,13,14,15,16,17],[0,1,2,4,5,6,7,8,9,10,11,12,13,14,15,17],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]],x.QUANT_TAB=[{levels:3,group:1,bits:5},{levels:5,group:1,bits:7},{levels:7,group:0,bits:3},{levels:9,group:1,bits:10},{levels:15,group:0,bits:4},{levels:31,group:0,bits:5},{levels:63,group:0,bits:6},{levels:127,group:0,bits:7},{levels:255,group:0,bits:8},{levels:511,group:0,bits:9},{levels:1023,group:0,bits:10},{levels:2047,group:0,bits:11},{levels:4095,group:0,bits:12},{levels:8191,group:0,bits:13},{levels:16383,group:0,bits:14},{levels:32767,group:0,bits:15},{levels:65535,group:0,bits:16}];var Y=function(A){function t(t){var i;return(i=A.call(this,t)||this).onDecodeCallback=t.onAudioDecode,i.module=t.wasmModule,i.bufferSize=t.audioBufferSize||131072,i.bufferMode=t.streaming?L.MODE.EVICT:L.MODE.EXPAND,i.sampleRate=0,i}u(t,A);var i=t.prototype;return i.initializeWasmDecoder=function(){this.module.instance?(this.instance=this.module.instance,this.functions=this.module.instance.exports,this.decoder=this.functions._mp2_decoder_create(this.bufferSize,this.bufferMode)):console.warn("JSMpeg: WASM module not compiled yet")},i.destroy=function(){this.decoder&&this.functions._mp2_decoder_destroy(this.decoder)},i.bufferGetIndex=function(){if(this.decoder)return this.functions._mp2_decoder_get_index(this.decoder)},i.bufferSetIndex=function(A){this.decoder&&this.functions._mp2_decoder_set_index(this.decoder,A)},i.bufferWrite=function(A){this.decoder||this.initializeWasmDecoder();for(var t=0,i=0;i>4<<4;this.gl.viewport(0,0,i,this.height)},t.createTexture=function(A,t){var i=this.gl,e=i.createTexture();return i.bindTexture(i.TEXTURE_2D,e),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MAG_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_MIN_FILTER,i.LINEAR),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_S,i.CLAMP_TO_EDGE),i.texParameteri(i.TEXTURE_2D,i.TEXTURE_WRAP_T,i.CLAMP_TO_EDGE),i.uniform1i(i.getUniformLocation(this.program,t),A),e},t.createProgram=function(A,t){var i=this.gl,e=i.createProgram();return i.attachShader(e,this.compileShader(i.VERTEX_SHADER,A)),i.attachShader(e,this.compileShader(i.FRAGMENT_SHADER,t)),i.linkProgram(e),i.useProgram(e),e},t.compileShader=function(A,t){var i=this.gl,e=i.createShader(A);if(i.shaderSource(e,t),i.compileShader(e),!i.getShaderParameter(e,i.COMPILE_STATUS))throw new Error(i.getShaderInfoLog(e));return e},t.allowsClampedTextureData=function(){var A=this.gl,t=A.createTexture();return A.bindTexture(A.TEXTURE_2D,t),A.texImage2D(A.TEXTURE_2D,0,A.LUMINANCE,1,1,0,A.LUMINANCE,A.UNSIGNED_BYTE,new Uint8ClampedArray([0])),0===A.getError()},t.renderProgress=function(A){var t=this.gl;t.useProgram(this.loadingProgram);var i=t.getUniformLocation(this.loadingProgram,"progress");t.uniform1f(i,A),t.drawArrays(t.TRIANGLE_STRIP,0,4)},t.render=function(A,t,i,e){if(this.enabled){var s=this.gl,o=this.width+15>>4<<4,g=this.height,I=o>>1,a=g>>1;e&&this.shouldCreateUnclampedViews&&(A=new Uint8Array(A.buffer),t=new Uint8Array(t.buffer),i=new Uint8Array(i.buffer)),s.useProgram(this.program),this.updateTexture(s.TEXTURE0,this.textureY,o,g,A),this.updateTexture(s.TEXTURE1,this.textureCb,I,a,t),this.updateTexture(s.TEXTURE2,this.textureCr,I,a,i),s.drawArrays(s.TRIANGLE_STRIP,0,4)}},t.updateTexture=function(A,t,i,e,s){var o=this.gl;o.activeTexture(A),o.bindTexture(o.TEXTURE_2D,t),this.hasTextureData[A]?o.texSubImage2D(o.TEXTURE_2D,0,0,0,i,e,o.LUMINANCE,o.UNSIGNED_BYTE,s):(this.hasTextureData[A]=!0,o.texImage2D(o.TEXTURE_2D,0,o.LUMINANCE,i,e,0,o.LUMINANCE,o.UNSIGNED_BYTE,s))},t.deleteTexture=function(A,t){var i=this.gl;i.activeTexture(A),i.bindTexture(i.TEXTURE_2D,null),i.deleteTexture(t)},A.IsSupported=function(){try{if(!window.WebGLRenderingContext)return!1;var A=document.createElement("canvas");return!(!A.getContext("webgl")&&!A.getContext("experimental-webgl"))}catch(A){return!1}},A}();H.SHADER={FRAGMENT_YCRCB_TO_RGBA:["precision mediump float;","uniform sampler2D textureY;","uniform sampler2D textureCb;","uniform sampler2D textureCr;","varying vec2 texCoord;","mat4 rec601 = mat4(","1.16438, 0.00000, 1.59603, -0.87079,","1.16438, -0.39176, -0.81297, 0.52959,","1.16438, 2.01723, 0.00000, -1.08139,","0, 0, 0, 1",");","void main() {","float y = texture2D(textureY, texCoord).r;","float cb = texture2D(textureCb, texCoord).r;","float cr = texture2D(textureCr, texCoord).r;","gl_FragColor = vec4(y, cr, cb, 1.0) * rec601;","}"].join("\n"),FRAGMENT_LOADING:["precision mediump float;","uniform float progress;","varying vec2 texCoord;","void main() {","float c = ceil(progress-(1.0-texCoord.y));","gl_FragColor = vec4(c,c,c,1);","}"].join("\n"),VERTEX_IDENTITY:["attribute vec2 vertex;","varying vec2 texCoord;","void main() {","texCoord = vertex;","gl_Position = vec4((vertex * 2.0 - 1.0) * vec2(1, -1), 0.0, 1.0);","}"].join("\n")};var P=function(){function A(A){A.canvas?(this.canvas=A.canvas,this.ownsCanvasElement=!1):(this.canvas=document.createElement("canvas"),this.ownsCanvasElement=!0),this.width=this.canvas.width,this.height=this.canvas.height,this.enabled=!0,this.context=this.canvas.getContext("2d")}var t=A.prototype;return t.destroy=function(){this.ownsCanvasElement&&this.canvas.remove()},t.resize=function(A,t){this.width=0|A,this.height=0|t,this.canvas.width=this.width,this.canvas.height=this.height,this.imageData=this.context.getImageData(0,0,this.width,this.height),S(this.imageData.data,255)},t.renderProgress=function(A){var t=this.canvas.width,i=this.canvas.height,e=this.context;e.fillStyle="#222",e.fillRect(0,0,t,i),e.fillStyle="#fff",e.fillRect(0,i-i*A,t,i*A)},t.render=function(A,t,i){this.YCbCrToRGBA(A,t,i,this.imageData.data),this.context.putImageData(this.imageData,0,0)},t.YCbCrToRGBA=function(A,t,i,e){if(this.enabled)for(var s,o,g,I,a,B=this.width+15>>4<<4,r=B>>1,C=0,n=B,E=B+(B-this.width),Q=0,h=r-(this.width>>1),d=0,c=4*this.width,l=4*this.width,u=this.width>>1,w=this.height>>1,p=0;p>8)-179,I=(88*o>>8)-44+(183*s>>8)-91,a=o+(198*o>>8)-227;var m=A[C++],f=A[C++];e[d]=m+g,e[d+1]=m-I,e[d+2]=m+a,e[d+4]=f+g,e[d+5]=f-I,e[d+6]=f+a,d+=8;var y=A[n++],R=A[n++];e[c]=y+g,e[c+1]=y-I,e[c+2]=y+a,e[c+4]=R+g,e[c+5]=R-I,e[c+6]=R+a,c+=8}C+=E,n+=E,d+=l,c+=l,Q+=h}},A}(),O=function(){function A(){this.context=A.CachedContext=A.CachedContext||new(window.AudioContext||window.webkitAudioContext),this.gain=this.context.createGain(),this.destination=this.gain,this.gain.connect(this.context.destination),this.context._connections=(this.context._connections||0)+1,this.startTime=0,this.buffer=null,this.wallclockStartTime=0,this.volume=1,this.enabled=!0,this.unlocked=!A.NeedsUnlocking(),Object.defineProperty(this,"enqueuedTime",{get:this.getEnqueuedTime})}var t=A.prototype;return t.destroy=function(){this.gain.disconnect(),this.context._connections--,0===this.context._connections&&(this.context.close(),A.CachedContext=null)},t.play=function(A,t,i){if(this.enabled){if(!this.unlocked){var e=G();return this.wallclockStartTimethis.memory.buffer.byteLength){var i=this.brk-this.memory.buffer.byteLength,e=Math.ceil(i/this.pageSize);this.memory.grow(e),this.createHeapViews()}return t},t.c_abort=function(A){console.warn("JSMPeg: WASM abort",arguments)},t.c_assertFail=function(A){console.warn("JSMPeg: WASM ___assert_fail",arguments)},t.readDylinkSection=function(A){var t=new Uint8Array(A),i=0,e=function(){for(var A=0,e=1;;){var s=t[i++];if(A+=(127&s)*e,e*=128,!(128&s))return A}},s=function(A){for(var e=0;ethis.maxAudioLag&&(this.audioOut.resetEnqueuedTime(),this.audioOut.enabled=!1),A=this.audio.decode()}while(A);this.audioOut.enabled=!0}},t.updateForStaticFile=function(){var A=!1,t=0;if(this.audio&&this.audio.canPlay){for(;!A&&this.audio.decodedTime-this.audio.currentTime<.25;)A=!this.audio.decode();this.video&&this.video.currentTime0&&(e>2*s&&(this.startTime+=e),A=!this.video.decode()),t=this.demuxer.currentTime-i}this.source.resume(t),A&&this.source.completed?this.loop?this.seek(0):(this.stop(),this.options.onEnded&&this.options.onEnded(this)):A&&this.options.onStalled&&this.options.onStalled(this)},A}(),W={Player:X,VideoElement:b,BitBuffer:L,Source:{Ajax:M,AjaxProgressive:q,WebSocket:N,Fetch:function(){function A(A,t){this.url=A,this.destination=null,this.request=null,this.streaming=!0,this.completed=!1,this.established=!1,this.progress=0,this.aborted=!1,this.onEstablishedCallback=t.onSourceEstablished,this.onCompletedCallback=t.onSourceCompleted,t.hookOnEstablished&&(this.hookOnEstablished=t.hookOnEstablished)}var t=A.prototype;return t.connect=function(A){this.destination=A},t.start=function(){var A=this,t={method:"GET",headers:new Headers,cache:"default"};self.fetch(this.url,t).then((function(t){if(t.ok&&t.status>=200&&t.status<=299)return A.progress=1,A.established=!0,A.pump(t.body.getReader())})).catch((function(A){throw A}))},t.pump=function(A){var t=this;return A.read().then((function(i){if(!i.done)return t.aborted?A.cancel():(t.destination&&t.destination.write(i.value.buffer),t.pump(A));t.completed=!0})).catch((function(A){throw A}))},t.resume=function(){},t.abort=function(){this.aborted=!0},A}()},Demuxer:{TS:U},Decoder:{Base:J,MPEG1Video:T,MPEG1VideoWASM:v,MP2Audio:x,MP2AudioWASM:Y},Renderer:{WebGL:H,Canvas2D:P},AudioOutput:{WebAudio:O},WASMModule:K,Now:G,CreateVideoElements:function(){for(var A=document.querySelectorAll(".jsmpeg"),t=0;tthis.q=A)))}resume(){var A;null===(A=this.q)||void 0===A||A.call(this),this.Z=this.q=void 0}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const V=A=>!e(A)&&"function"==typeof A.then;const z=A(class extends t{constructor(){super(...arguments),this._$Cwt=1073741823,this._$Cyt=[],this._$CK=new Z(this),this._$CX=new j}render(...A){var t;return null!==(t=A.find((A=>!V(A))))&&void 0!==t?t:i}update(A,t){const e=this._$Cyt;let s=e.length;this._$Cyt=t;const o=this._$CK,g=this._$CX;this.isConnected||this.disconnected();for(let A=0;Athis._$Cwt);A++){const i=t[A];if(!V(i))return this._$Cwt=A,i;A{for(;g.get();)await g.get();const t=o.deref();if(void 0!==t){const e=t._$Cyt.indexOf(i);e>-1&&e{let i=!1;const e=new W.VideoElement(this,A,{canvas:this._jsmpegCanvasElement},{pauseWhenHidden:!1,autoplay:!1,protocols:[],audio:!1,videoBufferSize:4194304,preserveDrawingBuffer:!0,...this.cameraConfig?.jsmpeg?.options,reconnectInterval:0,onVideoDecode:()=>{!i&&this._jsmpegCanvasElement&&(i=!0,t(e))},onPlay:()=>g(this),onPause:()=>I(this)})})),this._jsmpegCanvasElement&&a(this,this._jsmpegCanvasElement,{player:this,capabilities:{supportsPause:!0}})}_resetPlayer(){if(this._refreshPlayerTimer.stop(),this._jsmpegVideoPlayer){try{this._jsmpegVideoPlayer.destroy()}catch(A){}this._jsmpegVideoPlayer=void 0}this._jsmpegCanvasElement&&(this._jsmpegCanvasElement.remove(),this._jsmpegCanvasElement=void 0)}connectedCallback(){super.connectedCallback(),this.isConnected&&this.requestUpdate()}disconnectedCallback(){this.isConnected||this._resetPlayer(),super.disconnectedCallback()}async _refreshPlayer(){if(!this.hass)return;this._resetPlayer(),this._jsmpegCanvasElement=document.createElement("canvas"),this._jsmpegCanvasElement.className="media";const A=this.cameraEndpoints?.jsmpeg;if(!A)return B(this,r("error.live_camera_no_endpoint"),{context:this.cameraConfig});const t=await c(this,this.hass,A,86400);t&&(await this._createJSMPEGPlayer(t),this._refreshPlayerTimer.start(82800,(()=>this.requestUpdate())))}render(){return C`${z((async()=>(await this._refreshPlayer(),this._jsmpegVideoPlayer&&this._jsmpegCanvasElement?C`${this._jsmpegCanvasElement}`:B(this,r("error.jsmpeg_no_player"))))(),n({cardWideConfig:this.cardWideConfig}))}`}static get styles(){return E(":host {\n width: 100%;\n height: 100%;\n display: flex;\n}\n\ncanvas {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--frigate-card-media-layout-fit, contain);\n object-position: var(--frigate-card-media-layout-position-x, 50%) var(--frigate-card-media-layout-position-y, 50%);\n}")}};Q([h({attribute:!1})],$.prototype,"cameraConfig",void 0),Q([h({attribute:!1})],$.prototype,"cameraEndpoints",void 0),Q([h({attribute:!1})],$.prototype,"cardWideConfig",void 0),$=Q([d("frigate-card-live-jsmpeg")],$);export{$ as FrigateCardLiveJSMPEG}; diff --git a/www/frigate-card/live-webrtc-card-dfc8f852.js b/www/frigate-card/live-webrtc-card-dfc8f852.js new file mode 100644 index 00000000..c0b14c7b --- /dev/null +++ b/www/frigate-card/live-webrtc-card-dfc8f852.js @@ -0,0 +1 @@ +import{s as t,dJ as e,di as a,cZ as r,dj as s,dl as n,dm as o,dk as i,bj as c,bk as d,bl as l,bn as u,db as h,l as p,cL as b,bX as y,y as g}from"./card-555679fd.js";import{m}from"./audio-557099cb.js";import{s as w,h as _,M as f}from"./lazyload-c2d6254a.js";let C=class extends t{constructor(){super(...arguments),this.controls=!1,this._webrtcTask=new e(this,this._getWebRTCCardElement,(()=>[1]))}async play(){return this._getPlayer()?.play()}async pause(){this._getPlayer()?.pause()}async mute(){const t=this._getPlayer();t&&(t.muted=!0)}async unmute(){const t=this._getPlayer();t&&(t.muted=!1)}isMuted(){return this._getPlayer()?.muted??!0}async seek(t){const e=this._getPlayer();e&&(e.currentTime=t)}async setControls(t){const e=this._getPlayer();e&&w(e,t??this.controls)}isPaused(){return this._getPlayer()?.paused??!0}async getScreenshotURL(){const t=this._getPlayer();return t?a(t):null}connectedCallback(){super.connectedCallback(),this.requestUpdate()}_getPlayer(){const t=this.renderRoot?.querySelector("#webrtc");return t?.video??null}async _getWebRTCCardElement(){return await customElements.whenDefined("webrtc-camera"),customElements.get("webrtc-camera")}_createWebRTC(){const t=this._webrtcTask.value;if(t&&this.hass&&this.cameraConfig){const e=new t,a={...this.cameraConfig.webrtc_card};return a.url||a.entity||!this.cameraEndpoints?.webrtcCard||(a.url=this.cameraEndpoints.webrtcCard.endpoint),e.setConfig(a),e.hass=this.hass,e}return null}render(){return r(this,this._webrtcTask,(()=>{let t;try{t=this._createWebRTC()}catch(t){return b(this,t instanceof y?t.message:p("error.webrtc_card_reported_error")+": "+t.message,{context:t.context})}return t&&(t.id="webrtc"),g`${t}`}),{inProgressFunc:()=>h({message:p("error.webrtc_card_waiting"),cardWideConfig:this.cardWideConfig})})}updated(){this.updateComplete.then((()=>{const t=this._getPlayer();t&&(w(t,this.controls),t.onloadeddata=()=>{this.controls&&_(t,f),s(this,t,{player:this,capabilities:{supportsPause:!0,hasAudio:m(t)}})},t.onplay=()=>n(this),t.onpause=()=>o(this),t.onvolumechange=()=>i(this))}))}static get styles(){return c(":host {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* Don't drop shadow or have radius for nested webrtc card */\n#webrtc ha-card {\n border-radius: 0px;\n margin: 0px;\n box-shadow: none;\n}\n\nha-card,\ndiv.fix-safari,\n#video {\n background: unset;\n background-color: unset;\n}\n\n#webrtc #video {\n object-fit: var(--frigate-card-media-layout-fit, contain);\n object-position: var(--frigate-card-media-layout-position-x, 50%) var(--frigate-card-media-layout-position-y, 50%);\n}")}};d([l({attribute:!1})],C.prototype,"cameraConfig",void 0),d([l({attribute:!1})],C.prototype,"cameraEndpoints",void 0),d([l({attribute:!1})],C.prototype,"cardWideConfig",void 0),d([l({attribute:!0,type:Boolean})],C.prototype,"controls",void 0),C=d([u("frigate-card-live-webrtc-card")],C);export{C as FrigateCardLiveWebRTCCard}; diff --git a/www/frigate-card/media-b0eb3f2a.js b/www/frigate-card/media-b0eb3f2a.js new file mode 100644 index 00000000..cb05b25e --- /dev/null +++ b/www/frigate-card/media-b0eb3f2a.js @@ -0,0 +1 @@ +var e;!function(e){e.MP4="mp4",e.HLS="hls"}(e||(e={}));class t{constructor(e,t){this._mediaType=e,this._cameraID=t}getContentType(){return"snapshot"===this._mediaType?"image":"video"}getCameraID(){return this._cameraID}getMediaType(){return this._mediaType}getVideoContentType(){return null}getID(){return null}getStartTime(){return null}getEndTime(){return null}getUsableEndTime(){return this.getEndTime()??(this.inProgress()?new Date:this.getStartTime())}inProgress(){return null}getContentID(){return null}getTitle(){return null}getThumbnail(){return null}isFavorite(){return null}includesTime(e){const t=this.getStartTime(),r=this.getUsableEndTime();return!!t&&!!r&&e>=t&&e<=r}setFavorite(e){}getWhere(){return null}}export{t as V,e as a}; diff --git a/www/frigate-card/media-layout-8e0c974f.js b/www/frigate-card/media-layout-8e0c974f.js new file mode 100644 index 00000000..bf994871 --- /dev/null +++ b/www/frigate-card/media-layout-8e0c974f.js @@ -0,0 +1 @@ +const t=(t,o)=>{void 0!==o?.fit?t.style.setProperty("--frigate-card-media-layout-fit",o.fit):t.style.removeProperty("--frigate-card-media-layout-fit");for(const e of["x","y"])void 0!==o?.position?.[e]?t.style.setProperty(`--frigate-card-media-layout-position-${e}`,`${o.position[e]}%`):t.style.removeProperty(`--frigate-card-media-layout-position-${e}`)};export{t as u}; diff --git a/www/frigate-card/timeline-6aa9e747.js b/www/frigate-card/timeline-6aa9e747.js new file mode 100644 index 00000000..fe1f3290 --- /dev/null +++ b/www/frigate-card/timeline-6aa9e747.js @@ -0,0 +1,182 @@ +import{dt as t,du as e,d8 as n,dv as o,dw as s,ca as r,dx as a,bl as l,bn as h,s as d,dn as c,y as u,bk as p,cP as m,cS as f,d1 as g,bj as v,cO as y,bm as b,dy as w,c2 as _,l as x,cp as k,cq as D,b$ as C,dz as S,cU as T,dA as E,d5 as M,cc as O,cd as I,dB as A,d7 as P,d4 as N,c5 as F,bQ as R,cn as L,c0 as j,dC as Y,cM as H}from"./card-555679fd.js";import{s as z}from"./index-52dee8bb.js";import{c as B}from"./_commonjsHelpers-1789f0cf.js";var W={exports:{}};!function(t){function e(t){if(t)return function(t){for(var i in e.prototype)t[i]=e.prototype[i];return t}(t)}W.exports=e,e.prototype.on=e.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},e.prototype.once=function(t,e){function i(){this.off(t,i),e.apply(this,arguments)}return i.fn=e,this.on(t,i),this},e.prototype.off=e.prototype.removeListener=e.prototype.removeAllListeners=e.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i,n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var o=0;o-1}var Ft=function(){function t(t,e){this.manager=t,this.set(e)}var e=t.prototype;return e.set=function(t){t===st&&(t=this.compute()),ot&&this.manager.element.style&&ct[t]&&(this.manager.element.style[nt]=t),this.actions=t.toLowerCase().trim()},e.update=function(){this.set(this.manager.options.touchAction)},e.compute=function(){var t=[];return At(this.manager.recognizers,(function(e){Pt(e.options.enable,[e])&&(t=t.concat(e.getTouchAction()))})),function(t){if(Nt(t,lt))return lt;var e=Nt(t,ht),i=Nt(t,dt);return e&&i?lt:e||i?e?ht:dt:Nt(t,at)?at:rt}(t.join(" "))},e.preventDefaults=function(t){var e=t.srcEvent,i=t.offsetDirection;if(this.manager.session.prevented)e.preventDefault();else{var n=this.actions,o=Nt(n,lt)&&!ct[lt],s=Nt(n,dt)&&!ct[dt],r=Nt(n,ht)&&!ct[ht];if(o){var a=1===t.pointers.length,l=t.distance<2,h=t.deltaTime<250;if(a&&l&&h)return}if(!r||!s)return o||s&&i&Tt||r&&i&Et?this.preventSrc(e):void 0}},e.preventSrc=function(t){this.manager.session.prevented=!0,t.preventDefault()},t}();function Rt(t,e){for(;t;){if(t===e)return!0;t=t.parentNode}return!1}function Lt(t){var e=t.length;if(1===e)return{x:J(t[0].clientX),y:J(t[0].clientY)};for(var i=0,n=0,o=0;o=tt(e)?t<0?kt:Dt:e<0?Ct:St}function Bt(t,e,i){return{x:e/t||0,y:i/t||0}}function Wt(t,e){var i=t.session,n=e.pointers,o=n.length;i.firstInput||(i.firstInput=jt(e)),o>1&&!i.firstMultiple?i.firstMultiple=jt(e):1===o&&(i.firstMultiple=!1);var s=i.firstInput,r=i.firstMultiple,a=r?r.center:s.center,l=e.center=Lt(n);e.timeStamp=et(),e.deltaTime=e.timeStamp-s.timeStamp,e.angle=Ht(a,l),e.distance=Yt(a,l),function(t,e){var i=e.center,n=t.offsetDelta||{},o=t.prevDelta||{},s=t.prevInput||{};e.eventType!==yt&&s.eventType!==wt||(o=t.prevDelta={x:s.deltaX||0,y:s.deltaY||0},n=t.offsetDelta={x:i.x,y:i.y}),e.deltaX=o.x+(i.x-n.x),e.deltaY=o.y+(i.y-n.y)}(i,e),e.offsetDirection=zt(e.deltaX,e.deltaY);var h,d,c=Bt(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=c.x,e.overallVelocityY=c.y,e.overallVelocity=tt(c.x)>tt(c.y)?c.x:c.y,e.scale=r?(h=r.pointers,Yt((d=n)[0],d[1],It)/Yt(h[0],h[1],It)):1,e.rotation=r?function(t,e){return Ht(e[1],e[0],It)+Ht(t[1],t[0],It)}(r.pointers,n):0,e.maxPointers=i.prevInput?e.pointers.length>i.prevInput.maxPointers?e.pointers.length:i.prevInput.maxPointers:e.pointers.length,function(t,e){var i,n,o,s,r=t.lastInterval||e,a=e.timeStamp-r.timeStamp;if(e.eventType!==_t&&(a>vt||void 0===r.velocity)){var l=e.deltaX-r.deltaX,h=e.deltaY-r.deltaY,d=Bt(a,l,h);n=d.x,o=d.y,i=tt(d.x)>tt(d.y)?d.x:d.y,s=zt(l,h),t.lastInterval=e}else i=r.velocity,n=r.velocityX,o=r.velocityY,s=r.direction;e.velocity=i,e.velocityX=n,e.velocityY=o,e.direction=s}(i,e);var u,p=t.element,m=e.srcEvent;Rt(u=m.composedPath?m.composedPath()[0]:m.path?m.path[0]:m.target,p)&&(p=u),e.target=p}function Gt(t,e,i){var n=i.pointers.length,o=i.changedPointers.length,s=e&yt&&n-o==0,r=e&(wt|_t)&&n-o==0;i.isFirst=!!s,i.isFinal=!!r,s&&(t.session={}),i.eventType=e,Wt(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function Vt(t){return t.trim().split(/\s+/g)}function Ut(t,e,i){At(Vt(e),(function(e){t.addEventListener(e,i,!1)}))}function $t(t,e,i){At(Vt(e),(function(e){t.removeEventListener(e,i,!1)}))}function qt(t){var e=t.ownerDocument||t;return e.defaultView||e.parentWindow||window}var Xt=function(){function t(t,e){var i=this;this.manager=t,this.callback=e,this.element=t.element,this.target=t.options.inputTarget,this.domHandler=function(e){Pt(t.options.enable,[t])&&i.handler(e)},this.init()}var e=t.prototype;return e.handler=function(){},e.init=function(){this.evEl&&Ut(this.element,this.evEl,this.domHandler),this.evTarget&&Ut(this.target,this.evTarget,this.domHandler),this.evWin&&Ut(qt(this.element),this.evWin,this.domHandler)},e.destroy=function(){this.evEl&&$t(this.element,this.evEl,this.domHandler),this.evTarget&&$t(this.target,this.evTarget,this.domHandler),this.evWin&&$t(qt(this.element),this.evWin,this.domHandler)},t}();function Kt(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var n=0;ni[e]})):n.sort()),n}var oe={touchstart:yt,touchmove:bt,touchend:wt,touchcancel:_t},se="touchstart touchmove touchend touchcancel",re=function(t){function e(){var i;return e.prototype.evTarget=se,(i=t.apply(this,arguments)||this).targetIds={},i}return U(e,t),e.prototype.handler=function(t){var e=oe[t.type],i=ae.call(this,t,e);i&&this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:ft,srcEvent:t})},e}(Xt);function ae(t,e){var i,n,o=ie(t.touches),s=this.targetIds;if(e&(yt|bt)&&1===o.length)return s[o[0].identifier]=!0,[o,o];var r=ie(t.changedTouches),a=[],l=this.target;if(n=o.filter((function(t){return Rt(t.target,l)})),e===yt)for(i=0;i-1&&n.splice(t,1)}),ue)}}function fe(t,e){t&yt?(this.primaryTouch=e.changedPointers[0].identifier,me.call(this,e)):t&(wt|_t)&&me.call(this,e)}function ge(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,n=0;n-1&&this.requireFail.splice(e,1),this},e.hasRequireFailures=function(){return this.requireFail.length>0},e.canRecognizeWith=function(t){return!!this.simultaneous[t.id]},e.emit=function(t){var e=this,i=this.state;function n(i){e.manager.emit(i,t)}i=xe&&n(e.options.event+Ee(i))},e.tryEmit=function(t){if(this.canEmit())return this.emit(t);this.state=Ce},e.canEmit=function(){for(var t=0;te.threshold&&o&e.direction},i.attrTest=function(t){return Ie.prototype.attrTest.call(this,t)&&(this.state&we||!(this.state&we)&&this.directionTest(t))},i.emit=function(e){this.pX=e.deltaX,this.pY=e.deltaY;var i=Ae(e.direction);i&&(e.additionalEvent=this.options.event+i),t.prototype.emit.call(this,e)},e}(Ie),Ne=function(t){function e(e){return void 0===e&&(e={}),t.call(this,V({event:"swipe",threshold:10,velocity:.3,direction:Tt|Et,pointers:1},e))||this}U(e,t);var i=e.prototype;return i.getTouchAction=function(){return Pe.prototype.getTouchAction.call(this)},i.attrTest=function(e){var i,n=this.options.direction;return n&(Tt|Et)?i=e.overallVelocity:n&Tt?i=e.overallVelocityX:n&Et&&(i=e.overallVelocityY),t.prototype.attrTest.call(this,e)&&n&e.offsetDirection&&e.distance>this.options.threshold&&e.maxPointers===this.options.pointers&&tt(i)>this.options.velocity&&e.eventType&wt},i.emit=function(t){var e=Ae(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)},e}(Ie),Fe=function(t){function e(e){return void 0===e&&(e={}),t.call(this,V({event:"pinch",threshold:0,pointers:2},e))||this}U(e,t);var i=e.prototype;return i.getTouchAction=function(){return[lt]},i.attrTest=function(e){return t.prototype.attrTest.call(this,e)&&(Math.abs(e.scale-1)>this.options.threshold||this.state&we)},i.emit=function(e){if(1!==e.scale){var i=e.scale<1?"in":"out";e.additionalEvent=this.options.event+i}t.prototype.emit.call(this,e)},e}(Ie),Re=function(t){function e(e){return void 0===e&&(e={}),t.call(this,V({event:"rotate",threshold:0,pointers:2},e))||this}U(e,t);var i=e.prototype;return i.getTouchAction=function(){return[lt]},i.attrTest=function(e){return t.prototype.attrTest.call(this,e)&&(Math.abs(e.rotation)>this.options.threshold||this.state&we)},e}(Ie),Le=function(t){function e(e){var i;return void 0===e&&(e={}),(i=t.call(this,V({event:"press",pointers:1,time:251,threshold:9},e))||this)._timer=null,i._input=null,i}U(e,t);var i=e.prototype;return i.getTouchAction=function(){return[rt]},i.process=function(t){var e=this,i=this.options,n=t.pointers.length===i.pointers,o=t.distancei.time;if(this._input=t,!o||!n||t.eventType&(wt|_t)&&!s)this.reset();else if(t.eventType&yt)this.reset(),this._timer=setTimeout((function(){e.state=ke,e.tryEmit()}),i.time);else if(t.eventType&wt)return ke;return Ce},i.reset=function(){clearTimeout(this._timer)},i.emit=function(t){this.state===ke&&(t&&t.eventType&wt?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=et(),this.manager.emit(this.options.event,this._input)))},e}(Me),je={domEvents:!1,touchAction:st,enable:!0,inputTarget:null,inputClass:null,cssProps:{userSelect:"none",touchSelect:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},Ye=[[Re,{enable:!1}],[Fe,{enable:!1},["rotate"]],[Ne,{direction:Tt}],[Pe,{direction:Tt},["swipe"]],[Oe],[Oe,{event:"doubletap",taps:2},["tap"]],[Le]];function He(t,e){var i,n=t.element;n.style&&(At(t.options.cssProps,(function(o,s){i=it(n.style,s),e?(t.oldCssProps[i]=n.style[i],n.style[i]=o):n.style[i]=t.oldCssProps[i]||""})),e||(t.oldCssProps={}))}var ze=function(){function t(t,e){var i,n=this;this.options=X({},je,e||{}),this.options.inputTarget=this.options.inputTarget||t,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=t,this.input=new((i=this).options.inputClass||(pt?ee:mt?re:ut?ve:ce))(i,Gt),this.touchAction=new Ft(this,this.options.touchAction),He(this,!0),At(this.options.recognizers,(function(t){var e=n.add(new t[0](t[1]));t[2]&&e.recognizeWith(t[2]),t[3]&&e.requireFailure(t[3])}),this)}var e=t.prototype;return e.set=function(t){return X(this.options,t),t.touchAction&&this.touchAction.update(),t.inputTarget&&(this.input.destroy(),this.input.target=t.inputTarget,this.input.init()),this},e.stop=function(t){this.session.stopped=t?2:1},e.recognize=function(t){var e=this.session;if(!e.stopped){var i;this.touchAction.preventDefaults(t);var n=this.recognizers,o=e.curRecognizer;(!o||o&&o.state&ke)&&(e.curRecognizer=null,o=null);for(var s=0;s\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",o=window.console&&(window.console.warn||window.console.log);return o&&o.call(window.console,n,i),t.apply(this,arguments)}}var qe=$e((function(t,e,i){for(var n=Object.keys(e),o=0;o2)return ii(ei(t[0],t[1]),...t.slice(2));const e=t[0],i=t[1];for(const t of Reflect.ownKeys(i))Object.prototype.propertyIsEnumerable.call(i,t)&&(i[t]===Je?delete e[t]:null===e[t]||null===i[t]||"object"!=typeof e[t]||"object"!=typeof i[t]||Array.isArray(e[t])||Array.isArray(i[t])?e[t]=ni(i[t]):e[t]=ii(e[t],i[t]));return e}function ni(t){return Array.isArray(t)?t.map((t=>ni(t))):"object"==typeof t&&null!==t?ii({},t):t}function oi(t){for(const e of Object.keys(t))t[e]===Je?delete t[e]:"object"==typeof t[e]&&null!==t[e]&&oi(t[e])}const si="undefined"!=typeof window?window.Hammer||Qe:function(){return function(){const t=()=>{};return{on:t,off:t,destroy:t,emit:t,get:()=>({set:t})}}()};function ri(t){this._cleanupQueue=[],this.active=!1,this._dom={container:t,overlay:document.createElement("div")},this._dom.overlay.classList.add("vis-overlay"),this._dom.container.appendChild(this._dom.overlay),this._cleanupQueue.push((()=>{this._dom.overlay.parentNode.removeChild(this._dom.overlay)}));const e=si(this._dom.overlay);e.on("tap",this._onTapOverlay.bind(this)),this._cleanupQueue.push((()=>{e.destroy()}));["tap","doubletap","press","pinch","pan","panstart","panmove","panend"].forEach((t=>{e.on(t,(t=>{t.srcEvent.stopPropagation()}))})),document&&document.body&&(this._onClick=e=>{(function(t,e){for(;t;){if(t===e)return!0;t=t.parentNode}return!1})(e.target,t)||this.deactivate()},document.body.addEventListener("click",this._onClick),this._cleanupQueue.push((()=>{document.body.removeEventListener("click",this._onClick)}))),this._escListener=t=>{("key"in t?"Escape"===t.key:27===t.keyCode)&&this.deactivate()}}G(ri.prototype),ri.current=null,ri.prototype.destroy=function(){this.deactivate();for(const t of this._cleanupQueue.splice(0).reverse())t()},ri.prototype.activate=function(){ri.current&&ri.current.deactivate(),ri.current=this,this.active=!0,this._dom.overlay.style.display="none",this._dom.container.classList.add("vis-active"),this.emit("change"),this.emit("activate"),document.body.addEventListener("keydown",this._escListener)},ri.prototype.deactivate=function(){this.active=!1,this._dom.overlay.style.display="block",this._dom.container.classList.remove("vis-active"),document.body.removeEventListener("keydown",this._escListener),this.emit("change"),this.emit("deactivate")},ri.prototype._onTapOverlay=function(t){this.activate(),t.srcEvent.stopPropagation()};const ai=/^\/?Date\((-?\d+)/i,li=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,hi=/^#?([a-f\d])([a-f\d])([a-f\d])$/i,di=/^rgb\( *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *\)$/i,ci=/^rgba\( *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *([01]|0?\.\d+) *\)$/i;function ui(t){return t instanceof Number||"number"==typeof t}function pi(t){return t instanceof String||"string"==typeof t}function mi(t){return"object"==typeof t&&null!==t}function fi(t,e,i,n){let o=!1;!0===n&&(o=null===e[i]&&void 0!==t[i]),o?delete t[i]:t[i]=e[i]}const gi=Object.assign;function vi(t,e,i=!1,n=!1){for(const o in e)(Object.prototype.hasOwnProperty.call(e,o)||!0===i)&&("object"==typeof e[o]&&null!==e[o]&&Object.getPrototypeOf(e[o])===Object.prototype?void 0===t[o]?t[o]=vi({},e[o],i):"object"==typeof t[o]&&null!==t[o]&&Object.getPrototypeOf(t[o])===Object.prototype?vi(t[o],e[o],i):fi(t,e,o,n):Array.isArray(e[o])?t[o]=e[o].slice():fi(t,e,o,n));return t}function yi(t){const e=typeof t;return"object"===e?null===t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":Array.isArray(t)?"Array":t instanceof Date?"Date":"Object":"number"===e?"Number":"boolean"===e?"Boolean":"string"===e?"String":void 0===e?"undefined":e}function bi(t,e){return[...t,e]}function wi(t){return t.slice()}const _i=Object.values;const xi={asBoolean:(t,e)=>("function"==typeof t&&(t=t()),null!=t?0!=t:e||null),asNumber:(t,e)=>("function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null),asString:(t,e)=>("function"==typeof t&&(t=t()),null!=t?String(t):e||null),asSize:(t,e)=>("function"==typeof t&&(t=t()),pi(t)?t:ui(t)?t+"px":e||null),asElement:(t,e)=>("function"==typeof t&&(t=t()),t||e||null)};function ki(t){let e;switch(t.length){case 3:case 4:return e=hi.exec(t),e?{r:parseInt(e[1]+e[1],16),g:parseInt(e[2]+e[2],16),b:parseInt(e[3]+e[3],16)}:null;case 6:case 7:return e=li.exec(t),e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null;default:return null}}function Di(t,e,i){return"#"+((1<<24)+(t<<16)+(e<<8)+i).toString(16).slice(1)}function Ci(t,e,i){t/=255,e/=255,i/=255;const n=Math.min(t,Math.min(e,i)),o=Math.max(t,Math.max(e,i));if(n===o)return{h:0,s:0,v:n};return{h:60*((t===n?3:i===n?1:5)-(t===n?e-i:i===n?t-e:i-t)/(o-n))/360,s:(o-n)/o,v:o}}const Si={split(t){const e={};return t.split(";").forEach((t=>{if(""!=t.trim()){const i=t.split(":"),n=i[0].trim(),o=i[1].trim();e[n]=o}})),e},join:t=>Object.keys(t).map((function(e){return e+": "+t[e]})).join("; ")};function Ti(t,e,i){let n,o,s;const r=Math.floor(6*t),a=6*t-r,l=i*(1-e),h=i*(1-a*e),d=i*(1-(1-a)*e);switch(r%6){case 0:n=i,o=d,s=l;break;case 1:n=h,o=i,s=l;break;case 2:n=l,o=i,s=d;break;case 3:n=l,o=h,s=i;break;case 4:n=d,o=l,s=i;break;case 5:n=i,o=l,s=h}return{r:Math.floor(255*n),g:Math.floor(255*o),b:Math.floor(255*s)}}function Ei(t,e,i){const n=Ti(t,e,i);return Di(n.r,n.g,n.b)}function Mi(t){const e=ki(t);if(!e)throw new TypeError(`'${t}' is not a valid color.`);return Ci(e.r,e.g,e.b)}function Oi(t){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t)}function Ii(t){return di.test(t)}function Ai(t){return ci.test(t)}function Pi(t){if(null===t||"object"!=typeof t)return null;if(t instanceof Element)return t;const e=Object.create(t);for(const i in t)Object.prototype.hasOwnProperty.call(t,i)&&"object"==typeof t[i]&&(e[i]=Pi(t[i]));return e}const Ni={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>t*(2-t),easeInOutQuad:t=>t<.5?2*t*t:(4-2*t)*t-1,easeInCubic:t=>t*t*t,easeOutCubic:t=>--t*t*t+1,easeInOutCubic:t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1,easeInQuart:t=>t*t*t*t,easeOutQuart:t=>1- --t*t*t*t,easeInOutQuart:t=>t<.5?8*t*t*t*t:1-8*--t*t*t*t,easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>1+--t*t*t*t*t,easeInOutQuint:t=>t<.5?16*t*t*t*t*t:1+16*--t*t*t*t*t};const Fi={black:"#000000",navy:"#000080",darkblue:"#00008B",mediumblue:"#0000CD",blue:"#0000FF",darkgreen:"#006400",green:"#008000",teal:"#008080",darkcyan:"#008B8B",deepskyblue:"#00BFFF",darkturquoise:"#00CED1",mediumspringgreen:"#00FA9A",lime:"#00FF00",springgreen:"#00FF7F",aqua:"#00FFFF",cyan:"#00FFFF",midnightblue:"#191970",dodgerblue:"#1E90FF",lightseagreen:"#20B2AA",forestgreen:"#228B22",seagreen:"#2E8B57",darkslategray:"#2F4F4F",limegreen:"#32CD32",mediumseagreen:"#3CB371",turquoise:"#40E0D0",royalblue:"#4169E1",steelblue:"#4682B4",darkslateblue:"#483D8B",mediumturquoise:"#48D1CC",indigo:"#4B0082",darkolivegreen:"#556B2F",cadetblue:"#5F9EA0",cornflowerblue:"#6495ED",mediumaquamarine:"#66CDAA",dimgray:"#696969",slateblue:"#6A5ACD",olivedrab:"#6B8E23",slategray:"#708090",lightslategray:"#778899",mediumslateblue:"#7B68EE",lawngreen:"#7CFC00",chartreuse:"#7FFF00",aquamarine:"#7FFFD4",maroon:"#800000",purple:"#800080",olive:"#808000",gray:"#808080",skyblue:"#87CEEB",lightskyblue:"#87CEFA",blueviolet:"#8A2BE2",darkred:"#8B0000",darkmagenta:"#8B008B",saddlebrown:"#8B4513",darkseagreen:"#8FBC8F",lightgreen:"#90EE90",mediumpurple:"#9370D8",darkviolet:"#9400D3",palegreen:"#98FB98",darkorchid:"#9932CC",yellowgreen:"#9ACD32",sienna:"#A0522D",brown:"#A52A2A",darkgray:"#A9A9A9",lightblue:"#ADD8E6",greenyellow:"#ADFF2F",paleturquoise:"#AFEEEE",lightsteelblue:"#B0C4DE",powderblue:"#B0E0E6",firebrick:"#B22222",darkgoldenrod:"#B8860B",mediumorchid:"#BA55D3",rosybrown:"#BC8F8F",darkkhaki:"#BDB76B",silver:"#C0C0C0",mediumvioletred:"#C71585",indianred:"#CD5C5C",peru:"#CD853F",chocolate:"#D2691E",tan:"#D2B48C",lightgrey:"#D3D3D3",palevioletred:"#D87093",thistle:"#D8BFD8",orchid:"#DA70D6",goldenrod:"#DAA520",crimson:"#DC143C",gainsboro:"#DCDCDC",plum:"#DDA0DD",burlywood:"#DEB887",lightcyan:"#E0FFFF",lavender:"#E6E6FA",darksalmon:"#E9967A",violet:"#EE82EE",palegoldenrod:"#EEE8AA",lightcoral:"#F08080",khaki:"#F0E68C",aliceblue:"#F0F8FF",honeydew:"#F0FFF0",azure:"#F0FFFF",sandybrown:"#F4A460",wheat:"#F5DEB3",beige:"#F5F5DC",whitesmoke:"#F5F5F5",mintcream:"#F5FFFA",ghostwhite:"#F8F8FF",salmon:"#FA8072",antiquewhite:"#FAEBD7",linen:"#FAF0E6",lightgoldenrodyellow:"#FAFAD2",oldlace:"#FDF5E6",red:"#FF0000",fuchsia:"#FF00FF",magenta:"#FF00FF",deeppink:"#FF1493",orangered:"#FF4500",tomato:"#FF6347",hotpink:"#FF69B4",coral:"#FF7F50",darkorange:"#FF8C00",lightsalmon:"#FFA07A",orange:"#FFA500",lightpink:"#FFB6C1",pink:"#FFC0CB",gold:"#FFD700",peachpuff:"#FFDAB9",navajowhite:"#FFDEAD",moccasin:"#FFE4B5",bisque:"#FFE4C4",mistyrose:"#FFE4E1",blanchedalmond:"#FFEBCD",papayawhip:"#FFEFD5",lavenderblush:"#FFF0F5",seashell:"#FFF5EE",cornsilk:"#FFF8DC",lemonchiffon:"#FFFACD",floralwhite:"#FFFAF0",snow:"#FFFAFA",yellow:"#FFFF00",lightyellow:"#FFFFE0",ivory:"#FFFFF0",white:"#FFFFFF"};class Ri{constructor(t=1){this.pixelRatio=t,this.generated=!1,this.centerCoordinates={x:144.5,y:144.5},this.r=289*.49,this.color={r:255,g:255,b:255,a:1},this.hueCircle=void 0,this.initialColor={r:255,g:255,b:255,a:1},this.previousColor=void 0,this.applied=!1,this.updateCallback=()=>{},this.closeCallback=()=>{},this._create()}insertTo(t){void 0!==this.hammer&&(this.hammer.destroy(),this.hammer=void 0),this.container=t,this.container.appendChild(this.frame),this._bindHammer(),this._setSize()}setUpdateCallback(t){if("function"!=typeof t)throw new Error("Function attempted to set as colorPicker update callback is not a function.");this.updateCallback=t}setCloseCallback(t){if("function"!=typeof t)throw new Error("Function attempted to set as colorPicker closing callback is not a function.");this.closeCallback=t}_isColorString(t){if("string"==typeof t)return Fi[t]}setColor(t,e=!0){if("none"===t)return;let i;const n=this._isColorString(t);if(void 0!==n&&(t=n),!0===pi(t)){if(!0===Ii(t)){const e=t.substr(4).substr(0,t.length-5).split(",");i={r:e[0],g:e[1],b:e[2],a:1}}else if(!0===Ai(t)){const e=t.substr(5).substr(0,t.length-6).split(",");i={r:e[0],g:e[1],b:e[2],a:e[3]}}else if(!0===Oi(t)){const e=ki(t);i={r:e.r,g:e.g,b:e.b,a:1}}}else if(t instanceof Object&&void 0!==t.r&&void 0!==t.g&&void 0!==t.b){const e=void 0!==t.a?t.a:"1.0";i={r:t.r,g:t.g,b:t.b,a:e}}if(void 0===i)throw new Error("Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: "+JSON.stringify(t));this._setColor(i,e)}show(){void 0!==this.closeCallback&&(this.closeCallback(),this.closeCallback=void 0),this.applied=!1,this.frame.style.display="block",this._generateHueCircle()}_hide(t=!0){!0===t&&(this.previousColor=Object.assign({},this.color)),!0===this.applied&&this.updateCallback(this.initialColor),this.frame.style.display="none",setTimeout((()=>{void 0!==this.closeCallback&&(this.closeCallback(),this.closeCallback=void 0)}),0)}_save(){this.updateCallback(this.color),this.applied=!1,this._hide()}_apply(){this.applied=!0,this.updateCallback(this.color),this._updatePicker(this.color)}_loadLast(){void 0!==this.previousColor?this.setColor(this.previousColor,!1):alert("There is no last color to load...")}_setColor(t,e=!0){!0===e&&(this.initialColor=Object.assign({},t)),this.color=t;const i=Ci(t.r,t.g,t.b),n=2*Math.PI,o=this.r*i.s,s=this.centerCoordinates.x+o*Math.sin(n*i.h),r=this.centerCoordinates.y+o*Math.cos(n*i.h);this.colorPickerSelector.style.left=s-.5*this.colorPickerSelector.clientWidth+"px",this.colorPickerSelector.style.top=r-.5*this.colorPickerSelector.clientHeight+"px",this._updatePicker(t)}_setOpacity(t){this.color.a=t/100,this._updatePicker(this.color)}_setBrightness(t){const e=Ci(this.color.r,this.color.g,this.color.b);e.v=t/100;const i=Ti(e.h,e.s,e.v);i.a=this.color.a,this.color=i,this._updatePicker()}_updatePicker(t=this.color){const e=Ci(t.r,t.g,t.b),i=this.colorPickerCanvas.getContext("2d");void 0===this.pixelRation&&(this.pixelRatio=(window.devicePixelRatio||1)/(i.webkitBackingStorePixelRatio||i.mozBackingStorePixelRatio||i.msBackingStorePixelRatio||i.oBackingStorePixelRatio||i.backingStorePixelRatio||1)),i.setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0);const n=this.colorPickerCanvas.clientWidth,o=this.colorPickerCanvas.clientHeight;i.clearRect(0,0,n,o),i.putImageData(this.hueCircle,0,0),i.fillStyle="rgba(0,0,0,"+(1-e.v)+")",i.circle(this.centerCoordinates.x,this.centerCoordinates.y,this.r),i.fill(),this.brightnessRange.value=100*e.v,this.opacityRange.value=100*t.a,this.initialColorDiv.style.backgroundColor="rgba("+this.initialColor.r+","+this.initialColor.g+","+this.initialColor.b+","+this.initialColor.a+")",this.newColorDiv.style.backgroundColor="rgba("+this.color.r+","+this.color.g+","+this.color.b+","+this.color.a+")"}_setSize(){this.colorPickerCanvas.style.width="100%",this.colorPickerCanvas.style.height="100%",this.colorPickerCanvas.width=289*this.pixelRatio,this.colorPickerCanvas.height=289*this.pixelRatio}_create(){if(this.frame=document.createElement("div"),this.frame.className="vis-color-picker",this.colorPickerDiv=document.createElement("div"),this.colorPickerSelector=document.createElement("div"),this.colorPickerSelector.className="vis-selector",this.colorPickerDiv.appendChild(this.colorPickerSelector),this.colorPickerCanvas=document.createElement("canvas"),this.colorPickerDiv.appendChild(this.colorPickerCanvas),this.colorPickerCanvas.getContext){const t=this.colorPickerCanvas.getContext("2d");this.pixelRatio=(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1),this.colorPickerCanvas.getContext("2d").setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0)}else{const t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerText="Error: your browser does not support HTML canvas",this.colorPickerCanvas.appendChild(t)}this.colorPickerDiv.className="vis-color",this.opacityDiv=document.createElement("div"),this.opacityDiv.className="vis-opacity",this.brightnessDiv=document.createElement("div"),this.brightnessDiv.className="vis-brightness",this.arrowDiv=document.createElement("div"),this.arrowDiv.className="vis-arrow",this.opacityRange=document.createElement("input");try{this.opacityRange.type="range",this.opacityRange.min="0",this.opacityRange.max="100"}catch(t){}this.opacityRange.value="100",this.opacityRange.className="vis-range",this.brightnessRange=document.createElement("input");try{this.brightnessRange.type="range",this.brightnessRange.min="0",this.brightnessRange.max="100"}catch(t){}this.brightnessRange.value="100",this.brightnessRange.className="vis-range",this.opacityDiv.appendChild(this.opacityRange),this.brightnessDiv.appendChild(this.brightnessRange);const t=this;this.opacityRange.onchange=function(){t._setOpacity(this.value)},this.opacityRange.oninput=function(){t._setOpacity(this.value)},this.brightnessRange.onchange=function(){t._setBrightness(this.value)},this.brightnessRange.oninput=function(){t._setBrightness(this.value)},this.brightnessLabel=document.createElement("div"),this.brightnessLabel.className="vis-label vis-brightness",this.brightnessLabel.innerText="brightness:",this.opacityLabel=document.createElement("div"),this.opacityLabel.className="vis-label vis-opacity",this.opacityLabel.innerText="opacity:",this.newColorDiv=document.createElement("div"),this.newColorDiv.className="vis-new-color",this.newColorDiv.innerText="new",this.initialColorDiv=document.createElement("div"),this.initialColorDiv.className="vis-initial-color",this.initialColorDiv.innerText="initial",this.cancelButton=document.createElement("div"),this.cancelButton.className="vis-button vis-cancel",this.cancelButton.innerText="cancel",this.cancelButton.onclick=this._hide.bind(this,!1),this.applyButton=document.createElement("div"),this.applyButton.className="vis-button vis-apply",this.applyButton.innerText="apply",this.applyButton.onclick=this._apply.bind(this),this.saveButton=document.createElement("div"),this.saveButton.className="vis-button vis-save",this.saveButton.innerText="save",this.saveButton.onclick=this._save.bind(this),this.loadButton=document.createElement("div"),this.loadButton.className="vis-button vis-load",this.loadButton.innerText="load last",this.loadButton.onclick=this._loadLast.bind(this),this.frame.appendChild(this.colorPickerDiv),this.frame.appendChild(this.arrowDiv),this.frame.appendChild(this.brightnessLabel),this.frame.appendChild(this.brightnessDiv),this.frame.appendChild(this.opacityLabel),this.frame.appendChild(this.opacityDiv),this.frame.appendChild(this.newColorDiv),this.frame.appendChild(this.initialColorDiv),this.frame.appendChild(this.cancelButton),this.frame.appendChild(this.applyButton),this.frame.appendChild(this.saveButton),this.frame.appendChild(this.loadButton)}_bindHammer(){this.drag={},this.pinch={},this.hammer=new si(this.colorPickerCanvas),this.hammer.get("pinch").set({enable:!0}),this.hammer.on("hammer.input",(t=>{t.isFirst&&this._moveSelector(t)})),this.hammer.on("tap",(t=>{this._moveSelector(t)})),this.hammer.on("panstart",(t=>{this._moveSelector(t)})),this.hammer.on("panmove",(t=>{this._moveSelector(t)})),this.hammer.on("panend",(t=>{this._moveSelector(t)}))}_generateHueCircle(){if(!1===this.generated){const t=this.colorPickerCanvas.getContext("2d");void 0===this.pixelRation&&(this.pixelRatio=(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1)),t.setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0);const e=this.colorPickerCanvas.clientWidth,i=this.colorPickerCanvas.clientHeight;let n,o,s,r;t.clearRect(0,0,e,i),this.centerCoordinates={x:.5*e,y:.5*i},this.r=.49*e;const a=2*Math.PI/360,l=1/360,h=1/this.r;let d;for(s=0;s<360;s++)for(r=0;ro.distance?" in "+zi.printLocation(n.path,t,"")+"Perhaps it was misplaced? Matching option found at: "+zi.printLocation(o.path,o.closestMatch,""):n.distance<=8?'. Did you mean "'+n.closestMatch+'"?'+zi.printLocation(n.path,t):". Did you mean one of these: "+zi.print(Object.keys(e))+zi.printLocation(i,t),console.error('%cUnknown option detected: "'+t+'"'+s,Hi),Yi=!0}static findInOptions(t,e,i,n=!1){let o=1e9,s="",r=[];const a=t.toLowerCase();let l;for(const h in e){let d;if(void 0!==e[h].__type__&&!0===n){const n=zi.findInOptions(t,e[h],bi(i,h));o>n.distance&&(s=n.closestMatch,r=n.path,o=n.distance,l=n.indexMatch)}else-1!==h.toLowerCase().indexOf(a)&&(l=h),d=zi.levenshteinDistance(t,h),o>d&&(s=h,r=wi(i),o=d)}return{closestMatch:s,path:r,distance:o,indexMatch:l}}static printLocation(t,e,i="Problem value found at: \n"){let n="\n\n"+i+"options = {\n";for(let e=0;e!1)){this.parent=t,this.changedOptions=[],this.container=e,this.allowCreation=!1,this.hideOption=o,this.options={},this.initialized=!1,this.popupCounter=0,this.defaultOptions={enabled:!1,filter:!0,container:void 0,showButton:!0},Object.assign(this.options,this.defaultOptions),this.configureOptions=i,this.moduleOptions={},this.domElements=[],this.popupDiv={},this.popupLimit=5,this.popupHistory={},this.colorPicker=new Ri(n),this.wrapper=void 0}setOptions(t){if(void 0!==t){this.popupHistory={},this._removePopup();let e=!0;if("string"==typeof t)this.options.filter=t;else if(Array.isArray(t))this.options.filter=t.join();else if("object"==typeof t){if(null==t)throw new TypeError("options cannot be null");void 0!==t.container&&(this.options.container=t.container),void 0!==t.filter&&(this.options.filter=t.filter),void 0!==t.showButton&&(this.options.showButton=t.showButton),void 0!==t.enabled&&(e=t.enabled)}else"boolean"==typeof t?(this.options.filter=!0,e=t):"function"==typeof t&&(this.options.filter=t,e=!0);!1===this.options.filter&&(e=!1),this.options.enabled=e}this._clean()}setModuleOptions(t){this.moduleOptions=t,!0===this.options.enabled&&(this._clean(),void 0!==this.options.container&&(this.container=this.options.container),this._create())}_create(){this._clean(),this.changedOptions=[];const t=this.options.filter;let e=0,i=!1;for(const n in this.configureOptions)Object.prototype.hasOwnProperty.call(this.configureOptions,n)&&(this.allowCreation=!1,i=!1,"function"==typeof t?(i=t(n,[]),i=i||this._handleObject(this.configureOptions[n],[n],!0)):!0!==t&&-1===t.indexOf(n)||(i=!0),!1!==i&&(this.allowCreation=!0,e>0&&this._makeItem([]),this._makeHeader(n),this._handleObject(this.configureOptions[n],[n])),e++);this._makeButton(),this._push()}_push(){this.wrapper=document.createElement("div"),this.wrapper.className="vis-configuration-wrapper",this.container.appendChild(this.wrapper);for(let t=0;t{i.appendChild(t)})),this.domElements.push(i),this.domElements.length}return 0}_makeHeader(t){const e=document.createElement("div");e.className="vis-configuration vis-config-header",e.innerText=t,this._makeItem([],e)}_makeLabel(t,e,i=!1){const n=document.createElement("div");if(n.className="vis-configuration vis-config-label vis-config-s"+e.length,!0===i){for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(Li("i","b",t))}else n.innerText=t+":";return n}_makeDropdown(t,e,i){const n=document.createElement("select");n.className="vis-configuration vis-config-select";let o=0;void 0!==e&&-1!==t.indexOf(e)&&(o=t.indexOf(e));for(let e=0;es&&1!==s&&(a.max=Math.ceil(e*t),h=a.max,l="range increased"),a.value=e}else a.value=n;const d=document.createElement("input");d.className="vis-configuration vis-config-rangeinput",d.value=a.value;const c=this;a.onchange=function(){d.value=this.value,c._update(Number(this.value),i)},a.oninput=function(){d.value=this.value};const u=this._makeLabel(i[i.length-1],i),p=this._makeItem(i,u,a,d);""!==l&&this.popupHistory[p]!==h&&(this.popupHistory[p]=h,this._setupPopup(l,p))}_makeButton(){if(!0===this.options.showButton){const t=document.createElement("div");t.className="vis-configuration vis-config-button",t.innerText="generate options",t.onclick=()=>{this._printOptions()},t.onmouseover=()=>{t.className="vis-configuration vis-config-button hover"},t.onmouseout=()=>{t.className="vis-configuration vis-config-button"},this.optionsContainer=document.createElement("div"),this.optionsContainer.className="vis-configuration vis-config-option-container",this.domElements.push(this.optionsContainer),this.domElements.push(t)}}_setupPopup(t,e){if(!0===this.initialized&&!0===this.allowCreation&&this.popupCounter{this._removePopup()},this.popupCounter+=1,this.popupDiv={html:i,index:e}}}_removePopup(){void 0!==this.popupDiv.html&&(this.popupDiv.html.parentNode.removeChild(this.popupDiv.html),clearTimeout(this.popupDiv.hideTimeout),clearTimeout(this.popupDiv.deleteTimeout),this.popupDiv={})}_showPopupIfNeeded(){if(void 0!==this.popupDiv.html){const t=this.domElements[this.popupDiv.index].getBoundingClientRect();this.popupDiv.html.style.left=t.left+"px",this.popupDiv.html.style.top=t.top-30+"px",document.body.appendChild(this.popupDiv.html),this.popupDiv.hideTimeout=setTimeout((()=>{this.popupDiv.html.style.opacity=0}),1500),this.popupDiv.deleteTimeout=setTimeout((()=>{this._removePopup()}),1800)}}_makeCheckbox(t,e,i){const n=document.createElement("input");n.type="checkbox",n.className="vis-configuration vis-config-checkbox",n.checked=t,void 0!==e&&(n.checked=e,e!==t&&("object"==typeof t?e!==t.enabled&&this.changedOptions.push({path:i,value:e}):this.changedOptions.push({path:i,value:e})));const o=this;n.onchange=function(){o._update(this.checked,i)};const s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,n)}_makeTextInput(t,e,i){const n=document.createElement("input");n.type="text",n.className="vis-configuration vis-config-text",n.value=e,e!==t&&this.changedOptions.push({path:i,value:e});const o=this;n.onchange=function(){o._update(this.value,i)};const s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,n)}_makeColorField(t,e,i){const n=t[1],o=document.createElement("div");"none"!==(e=void 0===e?n:e)?(o.className="vis-configuration vis-config-colorBlock",o.style.backgroundColor=e):o.className="vis-configuration vis-config-colorBlock none",e=void 0===e?n:e,o.onclick=()=>{this._showColorPicker(e,o,i)};const s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,o)}_showColorPicker(t,e,i){e.onclick=function(){},this.colorPicker.insertTo(e),this.colorPicker.show(),this.colorPicker.setColor(t),this.colorPicker.setUpdateCallback((t=>{const n="rgba("+t.r+","+t.g+","+t.b+","+t.a+")";e.style.backgroundColor=n,this._update(n,i)})),this.colorPicker.setCloseCallback((()=>{e.onclick=()=>{this._showColorPicker(t,e,i)}}))}_handleObject(t,e=[],i=!1){let n=!1;const o=this.options.filter;let s=!1;for(const r in t)if(Object.prototype.hasOwnProperty.call(t,r)){n=!0;const a=t[r],l=bi(e,r);if("function"==typeof o&&(n=o(r,e),!1===n&&!Array.isArray(a)&&"string"!=typeof a&&"boolean"!=typeof a&&a instanceof Object&&(this.allowCreation=!1,n=this._handleObject(a,l,!0),this.allowCreation=!1===i)),!1!==n){s=!0;const t=this._getValue(l);if(Array.isArray(a))this._handleArray(a,t,l);else if("string"==typeof a)this._makeTextInput(a,t,l);else if("boolean"==typeof a)this._makeCheckbox(a,t,l);else if(a instanceof Object){if(!this.hideOption(e,r,this.moduleOptions))if(void 0!==a.enabled){const t=bi(l,"enabled"),e=this._getValue(t);if(!0===e){const t=this._makeLabel(r,l,!0);this._makeItem(l,t),s=this._handleObject(a,l)||s}else this._makeCheckbox(a,e,l)}else{const t=this._makeLabel(r,l,!0);this._makeItem(l,t),s=this._handleObject(a,l)||s}}else console.error("dont know how to handle",a,r,l)}}return s}_handleArray(t,e,i){"string"==typeof t[0]&&"color"===t[0]?(this._makeColorField(t,e,i),t[1]!==e&&this.changedOptions.push({path:i,value:e})):"string"==typeof t[0]?(this._makeDropdown(t,e,i),t[0]!==e&&this.changedOptions.push({path:i,value:e})):"number"==typeof t[0]&&(this._makeRange(t,e,i),t[0]!==e&&this.changedOptions.push({path:i,value:Number(e)}))}_update(t,e){const i=this._constructOptions(t,e);this.parent.body&&this.parent.body.emitter&&this.parent.body.emitter.emit&&this.parent.body.emitter.emit("configChange",i),this.initialized=!0,this.parent.setOptions(i)}_constructOptions(t,e,i={}){let n=i;t="false"!==(t="true"===t||t)&&t;for(let i=0;in-this.padding&&(i=!0),o=i?this.x-e:this.x,s=r?this.y-t:this.y}else s=this.y-t,s+t+this.padding>i&&(s=i-t-this.padding),sn&&(o=n-e-this.padding),o>>0,n-=t,n*=t,t=n>>>0,n-=t,t+=4294967296*n}return 2.3283064365386963e-10*(t>>>0)}}();let i=e(" "),n=e(" "),o=e(" ");for(let s=0;s{const t=2091639*e+2.3283064365386963e-10*o;return e=i,i=n,n=t-(o=0|t)};return s.uint32=()=>4294967296*s(),s.fract53=()=>s()+11102230246251565e-32*(2097152*s()|0),s.algorithm="Alea",s.seed=t,s.version="0.9",s}(t.length?t:[Date.now()])},ColorPicker:Wi,Configurator:Gi,DELETE:Je,HSVToHex:Ei,HSVToRGB:Ti,Hammer:Vi,Popup:Ui,RGBToHSV:Ci,RGBToHex:Di,VALIDATOR_PRINT_STYLE:$i,Validator:qi,addClassName:function(t,e){let i=t.className.split(" ");const n=e.split(" ");i=i.concat(n.filter((function(t){return!i.includes(t)}))),t.className=i.join(" ")},addCssText:function(t,e){const i={...Si.split(t.style.cssText),...Si.split(e)};t.style.cssText=Si.join(i)},addEventListener:function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.includes("Firefox")&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},binarySearchCustom:function(t,e,i,n){let o=0,s=0,r=t.length-1;for(;s<=r&&o<1e4;){const a=Math.floor((s+r)/2),l=t[a],h=e(void 0===n?l[i]:l[i][n]);if(0==h)return a;-1==h?s=a+1:r=a-1,o++}return-1},binarySearchValue:function(t,e,i,n,o){let s,r,a,l,h=0,d=0,c=t.length-1;for(o=null!=o?o:function(t,e){return t==e?0:t0)return"before"==n?Math.max(0,l-1):l;if(o(r,e)<0&&o(a,e)>0)return"before"==n?l:Math.min(t.length-1,l+1);o(r,e)<0?d=l+1:c=l-1,h++}return-1},bridgeObject:Pi,copyAndExtendArray:bi,copyArray:wi,deepExtend:vi,deepObjectAssign:ei,easingFunctions:Ni,equalArray:function(t,e){if(t.length!==e.length)return!1;for(let i=0,n=t.length;i0&&e(n,t[o-1])<0;o--)t[o]=t[o-1];t[o]=n}return t},isDate:function(t){if(t instanceof Date)return!0;if(pi(t)){if(ai.exec(t))return!0;if(!isNaN(Date.parse(t)))return!0}return!1},isNumber:ui,isObject:mi,isString:pi,isValidHex:Oi,isValidRGB:Ii,isValidRGBA:Ai,mergeOptions:function(t,e,i,n={}){const o=function(t){return null!=t},s=function(t){return null!==t&&"object"==typeof t};if(!s(t))throw new Error("Parameter mergeTarget must be an object");if(!s(e))throw new Error("Parameter options must be an object");if(!o(i))throw new Error("Parameter option must have a value");if(!s(n))throw new Error("Parameter globalOptions must be an object");const r=e[i],a=s(n)&&!function(t){for(const e in t)if(Object.prototype.hasOwnProperty.call(t,e))return!1;return!0}(n)?n[i]:void 0,l=a?a.enabled:void 0;if(void 0===r)return;if("boolean"==typeof r)return s(t[i])||(t[i]={}),void(t[i].enabled=r);if(null===r&&!s(t[i])){if(!o(a))return;t[i]=Object.create(a)}if(!s(r))return;let h=!0;void 0!==r.enabled?h=r.enabled:void 0!==l&&(h=a.enabled),function(t,e,i){s(t[i])||(t[i]={});const n=e[i],o=t[i];for(const t in n)Object.prototype.hasOwnProperty.call(n,t)&&(o[t]=n[t])}(t,e,i),t[i].enabled=h},option:xi,overrideOpacity:function(t,e){if(t.includes("rgba"))return t;if(t.includes("rgb")){const i=t.substr(t.indexOf("(")+1).replace(")","").split(",");return"rgba("+i[0]+","+i[1]+","+i[2]+","+e+")"}{const i=ki(t);return null==i?t:"rgba("+i.r+","+i.g+","+i.b+","+e+")"}},parseColor:function(t,e){if(pi(t)){let e=t;if(Ii(e)){const t=e.substr(4).substr(0,e.length-5).split(",").map((function(t){return parseInt(t)}));e=Di(t[0],t[1],t[2])}if(!0===Oi(e)){const t=Mi(e),i={h:t.h,s:.8*t.s,v:Math.min(1,1.02*t.v)},n={h:t.h,s:Math.min(1,1.25*t.s),v:.8*t.v},o=Ei(n.h,n.s,n.v),s=Ei(i.h,i.s,i.v);return{background:e,border:o,highlight:{background:s,border:o},hover:{background:s,border:o}}}return{background:e,border:e,highlight:{background:e,border:e},hover:{background:e,border:e}}}if(e){return{background:t.background||e.background,border:t.border||e.border,highlight:pi(t.highlight)?{border:t.highlight,background:t.highlight}:{background:t.highlight&&t.highlight.background||e.highlight.background,border:t.highlight&&t.highlight.border||e.highlight.border},hover:pi(t.hover)?{border:t.hover,background:t.hover}:{border:t.hover&&t.hover.border||e.hover.border,background:t.hover&&t.hover.background||e.hover.background}}}return{background:t.background||void 0,border:t.border||void 0,highlight:pi(t.highlight)?{border:t.highlight,background:t.highlight}:{background:t.highlight&&t.highlight.background||void 0,border:t.highlight&&t.highlight.border||void 0},hover:pi(t.hover)?{border:t.hover,background:t.hover}:{border:t.hover&&t.hover.border||void 0,background:t.hover&&t.hover.background||void 0}}},preventDefault:function(t){t||(t=window.event),t&&(t.preventDefault?t.preventDefault():t.returnValue=!1)},pureDeepObjectAssign:ti,recursiveDOMDelete:function t(e){if(e)for(;!0===e.hasChildNodes();){const i=e.firstChild;i&&(t(i),e.removeChild(i))}},removeClassName:function(t,e){let i=t.className.split(" ");const n=e.split(" ");i=i.filter((function(t){return!n.includes(t)})),t.className=i.join(" ")},removeCssText:function(t,e){const i=Si.split(t.style.cssText),n=Si.split(e);for(const t in n)Object.prototype.hasOwnProperty.call(n,t)&&delete i[t];t.style.cssText=Si.join(i)},removeEventListener:function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.includes("Firefox")&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},selectiveBridgeObject:function(t,e){if(null!==e&&"object"==typeof e){const i=Object.create(e);for(let n=0;n{e||(e=!0,requestAnimationFrame((()=>{e=!1,t()})))}},toArray:_i,topMost:function(t,e){let i;Array.isArray(e)||(e=[e]);for(const n of t)if(n){i=n[e[0]];for(let t=1;t1&&void 0!==arguments[1]?arguments[1]:0,i=(en[t[e+0]]+en[t[e+1]]+en[t[e+2]]+en[t[e+3]]+"-"+en[t[e+4]]+en[t[e+5]]+"-"+en[t[e+6]]+en[t[e+7]]+"-"+en[t[e+8]]+en[t[e+9]]+"-"+en[t[e+10]]+en[t[e+11]]+en[t[e+12]]+en[t[e+13]]+en[t[e+14]]+en[t[e+15]]).toLowerCase();if(!tn(i))throw TypeError("Stringified UUID is invalid");return i}var sn="6ba7b810-9dad-11d1-80b4-00c04fd430c8",rn="6ba7b811-9dad-11d1-80b4-00c04fd430c8";function an(t,e,i){function n(t,n,o,s){if("string"==typeof t&&(t=function(t){t=unescape(encodeURIComponent(t));for(var e=[],i=0;i>>24,i[1]=e>>>16&255,i[2]=e>>>8&255,i[3]=255&e,i[4]=(e=parseInt(t.slice(9,13),16))>>>8,i[5]=255&e,i[6]=(e=parseInt(t.slice(14,18),16))>>>8,i[7]=255&e,i[8]=(e=parseInt(t.slice(19,23),16))>>>8,i[9]=255&e,i[10]=(e=parseInt(t.slice(24,36),16))/1099511627776&255,i[11]=e/4294967296&255,i[12]=e>>>24&255,i[13]=e>>>16&255,i[14]=e>>>8&255,i[15]=255&e,i}(n)),16!==n.length)throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");var r=new Uint8Array(16+t.length);if(r.set(n),r.set(t,n.length),(r=i(r))[6]=15&r[6]|e,r[8]=63&r[8]|128,o){s=s||0;for(var a=0;a<16;++a)o[s+a]=r[a];return o}return on(r)}try{n.name=t}catch(t){}return n.DNS=sn,n.URL=rn,n}function ln(t){return 14+(t+64>>>9<<4)+1}function hn(t,e){var i=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(i>>16)<<16|65535&i}function dn(t,e,i,n,o,s){return hn((r=hn(hn(e,t),hn(n,s)))<<(a=o)|r>>>32-a,i);var r,a}function cn(t,e,i,n,o,s,r){return dn(e&i|~e&n,t,e,o,s,r)}function un(t,e,i,n,o,s,r){return dn(e&n|i&~n,t,e,o,s,r)}function pn(t,e,i,n,o,s,r){return dn(e^i^n,t,e,o,s,r)}function mn(t,e,i,n,o,s,r){return dn(i^(e|~n),t,e,o,s,r)}function fn(t,e,i){var n=(t=t||{}).random||(t.rng||Qi)();if(n[6]=15&n[6]|64,n[8]=63&n[8]|128,e){i=i||0;for(var o=0;o<16;++o)e[i+o]=n[o];return e}return on(n)}function gn(t,e,i,n){switch(t){case 0:return e&i^~e&n;case 1:case 3:return e^i^n;case 2:return e&i^e&n^i&n}}function vn(t,e){return t<>>32-e}an("v3",48,(function(t){if("string"==typeof t){var e=unescape(encodeURIComponent(t));t=new Uint8Array(e.length);for(var i=0;i>5]>>>o%32&255,r=parseInt(n.charAt(s>>>4&15)+n.charAt(15&s),16);e.push(r)}return e}(function(t,e){t[e>>5]|=128<>5]|=(255&t[n/8])<>>0;b=y,y=v,v=vn(g,30)>>>0,g=f,f=x}i[0]=i[0]+f>>>0,i[1]=i[1]+g>>>0,i[2]=i[2]+v>>>0,i[3]=i[3]+y>>>0,i[4]=i[4]+b>>>0}return[i[0]>>24&255,i[0]>>16&255,i[0]>>8&255,255&i[0],i[1]>>24&255,i[1]>>16&255,i[1]>>8&255,255&i[1],i[2]>>24&255,i[2]>>16&255,i[2]>>8&255,255&i[2],i[3]>>24&255,i[3]>>16&255,i[3]>>8&255,255&i[3],i[4]>>24&255,i[4]>>16&255,i[4]>>8&255,255&i[4]]}));class yn{_source;_transformers;_target;_listeners={add:this._add.bind(this),remove:this._remove.bind(this),update:this._update.bind(this)};constructor(t,e,i){this._source=t,this._transformers=e,this._target=i}all(){return this._target.update(this._transformItems(this._source.get())),this}start(){return this._source.on("add",this._listeners.add),this._source.on("remove",this._listeners.remove),this._source.on("update",this._listeners.update),this}stop(){return this._source.off("add",this._listeners.add),this._source.off("remove",this._listeners.remove),this._source.off("update",this._listeners.update),this}_transformItems(t){return this._transformers.reduce(((t,e)=>e(t)),t)}_add(t,e){null!=e&&this._target.add(this._transformItems(this._source.get(e.items)))}_update(t,e){null!=e&&this._target.update(this._transformItems(this._source.get(e.items)))}_remove(t,e){null!=e&&this._target.remove(this._transformItems(e.oldData))}}class bn{_source;_transformers=[];constructor(t){this._source=t}filter(t){return this._transformers.push((e=>e.filter(t))),this}map(t){return this._transformers.push((e=>e.map(t))),this}flatMap(t){return this._transformers.push((e=>e.flatMap(t))),this}to(t){return new yn(this._source,this._transformers,t)}}function wn(t){return"string"==typeof t||"number"==typeof t}class _n{delay;max;_queue=[];_timeout=null;_extended=null;constructor(t){this.delay=null,this.max=1/0,this.setOptions(t)}setOptions(t){t&&void 0!==t.delay&&(this.delay=t.delay),t&&void 0!==t.max&&(this.max=t.max),this._flushIfNeeded()}static extend(t,e){const i=new _n(e);if(void 0!==t.flush)throw new Error("Target object already has a property flush");t.flush=()=>{i.flush()};const n=[{name:"flush",original:void 0}];if(e&&e.replace)for(let o=0;othis.max&&this.flush(),null!=this._timeout&&(clearTimeout(this._timeout),this._timeout=null),this.queue.length>0&&"number"==typeof this.delay&&(this._timeout=setTimeout((()=>{this.flush()}),this.delay))}flush(){this._queue.splice(0).forEach((t=>{t.fn.apply(t.context||t.fn,t.args||[])}))}}class xn{_subscribers={"*":[],add:[],remove:[],update:[]};_trigger(t,e,i){if("*"===t)throw new Error("Cannot trigger event *");[...this._subscribers[t],...this._subscribers["*"]].forEach((n=>{n(t,e,null!=i?i:null)}))}on(t,e){"function"==typeof e&&this._subscribers[t].push(e)}off(t,e){this._subscribers[t]=this._subscribers[t].filter((t=>t!==e))}subscribe=xn.prototype.on;unsubscribe=xn.prototype.off}class kn{_pairs;constructor(t){this._pairs=t}*[Symbol.iterator](){for(const[t,e]of this._pairs)yield[t,e]}*entries(){for(const[t,e]of this._pairs)yield[t,e]}*keys(){for(const[t]of this._pairs)yield t}*values(){for(const[,t]of this._pairs)yield t}toIdArray(){return[...this._pairs].map((t=>t[0]))}toItemArray(){return[...this._pairs].map((t=>t[1]))}toEntryArray(){return[...this._pairs]}toObjectMap(){const t=Object.create(null);for(const[e,i]of this._pairs)t[e]=i;return t}toMap(){return new Map(this._pairs)}toIdSet(){return new Set(this.toIdArray())}toItemSet(){return new Set(this.toItemArray())}cache(){return new kn([...this._pairs])}distinct(t){const e=new Set;for(const[i,n]of this._pairs)e.add(t(n,i));return e}filter(t){const e=this._pairs;return new kn({*[Symbol.iterator](){for(const[i,n]of e)t(n,i)&&(yield[i,n])}})}forEach(t){for(const[e,i]of this._pairs)t(i,e)}map(t){const e=this._pairs;return new kn({*[Symbol.iterator](){for(const[i,n]of e)yield[i,t(n,i)]}})}max(t){const e=this._pairs[Symbol.iterator]();let i=e.next();if(i.done)return null;let n=i.value[1],o=t(i.value[1],i.value[0]);for(;!(i=e.next()).done;){const[e,s]=i.value,r=t(s,e);r>o&&(o=r,n=s)}return n}min(t){const e=this._pairs[Symbol.iterator]();let i=e.next();if(i.done)return null;let n=i.value[1],o=t(i.value[1],i.value[0]);for(;!(i=e.next()).done;){const[e,s]=i.value,r=t(s,e);r[...this._pairs].sort((([e,i],[n,o])=>t(i,o,e,n)))[Symbol.iterator]()})}}class Dn extends xn{flush;length;get idProp(){return this._idProp}_options;_data;_idProp;_queue=null;constructor(t,e){super(),t&&!Array.isArray(t)&&(e=t,t=[]),this._options=e||{},this._data=new Map,this.length=0,this._idProp=this._options.fieldId||"id",t&&t.length&&this.add(t),this.setOptions(e)}setOptions(t){t&&void 0!==t.queue&&(!1===t.queue?this._queue&&(this._queue.destroy(),this._queue=null):(this._queue||(this._queue=_n.extend(this,{replace:["add","update","remove"]})),t.queue&&"object"==typeof t.queue&&this._queue.setOptions(t.queue)))}add(t,e){const i=[];let n;if(Array.isArray(t)){if(t.map((t=>t[this._idProp])).some((t=>this._data.has(t))))throw new Error("A duplicate id was found in the parameter array.");for(let e=0,o=t.length;e{const e=t[r];if(null!=e&&this._data.has(e)){const i=t,r=Object.assign({},this._data.get(e)),a=this._updateItem(i);n.push(a),s.push(i),o.push(r)}else{const e=this._addItem(t);i.push(e)}};if(Array.isArray(t))for(let e=0,i=t.length;e{const e=this._data.get(t[this._idProp]);if(null==e)throw new Error("Updating non-existent items is not allowed.");return{oldData:e,update:t}})).map((({oldData:t,update:e})=>{const i=t[this._idProp],n=ti(t,e);return this._data.set(i,n),{id:i,oldData:t,updatedData:n}}));if(i.length){const t={items:i.map((t=>t.id)),oldData:i.map((t=>t.oldData)),data:i.map((t=>t.updatedData))};return this._trigger("update",t,e),t.items}return[]}get(t,e){let i,n,o;wn(t)?(i=t,o=e):Array.isArray(t)?(n=t,o=e):o=t;const s=o&&"Object"===o.returnType?"Object":"Array",r=o&&o.filter,a=[];let l,h,d;if(null!=i)l=this._data.get(i),l&&r&&!r(l)&&(l=void 0);else if(null!=n)for(let t=0,e=n.length;t(e[i]=t[i],e)),{}):t}_sort(t,e){if("string"==typeof e){const i=e;t.sort(((t,e)=>{const n=t[i],o=e[i];return n>o?1:ni)&&(e=n,i=o)}return e||null}min(t){let e=null,i=null;for(const n of this._data.values()){const o=n[t];"number"==typeof o&&(null==i||os(t)&&r(t)),null==n?this._data.get(o):this._data.get(n,o)}getIds(t){if(this._data.length){const e=this._options.filter,i=null!=t?t.filter:null;let n;return n=i?e?t=>e(t)&&i(t):i:e,this._data.getIds({filter:n,order:t&&t.order})}return[]}forEach(t,e){if(this._data){const i=this._options.filter,n=e&&e.filter;let o;o=n?i?function(t){return i(t)&&n(t)}:n:i,this._data.forEach(t,{filter:o,order:e&&e.order})}}map(t,e){if(this._data){const i=this._options.filter,n=e&&e.filter;let o;return o=n?i?t=>i(t)&&n(t):n:i,this._data.map(t,{filter:o,order:e&&e.order})}return[]}getDataSet(){return this._data.getDataSet()}stream(t){return this._data.stream(t||{[Symbol.iterator]:this._ids.keys.bind(this._ids)})}dispose(){this._data?.off&&this._data.off("*",this._listener);const t="This data view has already been disposed of.",e={get:()=>{throw new Error(t)},set:()=>{throw new Error(t)},configurable:!1};for(const t of Reflect.ownKeys(Cn.prototype))Object.defineProperty(this,t,e)}_onEvent(t,e,i){if(!e||!e.items||!this._data)return;const n=e.items,o=[],s=[],r=[],a=[],l=[],h=[];switch(t){case"add":for(let t=0,e=n.length;t>>0;for(e=0;e0)for(i=0;i=0?i?"+":"":"-")+Math.pow(10,Math.max(0,o)).toString().substr(1)+n}var F=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,R=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,L={},j={};function Y(t,e,i,n){var o=n;"string"==typeof n&&(o=function(){return this[n]()}),t&&(j[t]=o),e&&(j[e[0]]=function(){return N(o.apply(this,arguments),e[1],e[2])}),i&&(j[i]=function(){return this.localeData().ordinal(o.apply(this,arguments),t)})}function H(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function z(t){var e,i,n=t.match(F);for(e=0,i=n.length;e=0&&R.test(t);)t=t.replace(R,n),R.lastIndex=0,i-=1;return t}var G={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"};function V(t){var e=this._longDateFormat[t],i=this._longDateFormat[t.toUpperCase()];return e||!i?e:(this._longDateFormat[t]=i.match(F).map((function(t){return"MMMM"===t||"MM"===t||"DD"===t||"dddd"===t?t.slice(1):t})).join(""),this._longDateFormat[t])}var U="Invalid date";function $(){return this._invalidDate}var q="%d",X=/\d{1,2}/;function K(t){return this._ordinal.replace("%d",t)}var Z={future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function Q(t,e,i,n){var o=this._relativeTime[i];return E(o)?o(t,e,i,n):o.replace(/%d/i,t)}function J(t,e){var i=this._relativeTime[t>0?"future":"past"];return E(i)?i(e):i.replace(/%s/i,e)}var tt={};function et(t,e){var i=t.toLowerCase();tt[i]=tt[i+"s"]=tt[e]=t}function it(t){return"string"==typeof t?tt[t]||tt[t.toLowerCase()]:void 0}function nt(t){var e,i,n={};for(i in t)r(t,i)&&(e=it(i))&&(n[e]=t[i]);return n}var ot={};function st(t,e){ot[t]=e}function rt(t){var e,i=[];for(e in t)r(t,e)&&i.push({unit:e,priority:ot[e]});return i.sort((function(t,e){return t.priority-e.priority})),i}function at(t){return t%4==0&&t%100!=0||t%400==0}function lt(t){return t<0?Math.ceil(t)||0:Math.floor(t)}function ht(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=lt(e)),i}function dt(t,e){return function(n){return null!=n?(ut(this,t,n),i.updateOffset(this,e),this):ct(this,t)}}function ct(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function ut(t,e,i){t.isValid()&&!isNaN(i)&&("FullYear"===e&&at(t.year())&&1===t.month()&&29===t.date()?(i=ht(i),t._d["set"+(t._isUTC?"UTC":"")+e](i,t.month(),Qt(i,t.month()))):t._d["set"+(t._isUTC?"UTC":"")+e](i))}function pt(t){return E(this[t=it(t)])?this[t]():this}function mt(t,e){if("object"==typeof t){var i,n=rt(t=nt(t)),o=n.length;for(i=0;i68?1900:2e3)};var fe=dt("FullYear",!0);function ge(){return at(this.year())}function ve(t,e,i,n,o,s,r){var a;return t<100&&t>=0?(a=new Date(t+400,e,i,n,o,s,r),isFinite(a.getFullYear())&&a.setFullYear(t)):a=new Date(t,e,i,n,o,s,r),a}function ye(t){var e,i;return t<100&&t>=0?((i=Array.prototype.slice.call(arguments))[0]=t+400,e=new Date(Date.UTC.apply(null,i)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)):e=new Date(Date.UTC.apply(null,arguments)),e}function be(t,e,i){var n=7+e-i;return-(7+ye(t,0,n).getUTCDay()-e)%7+n-1}function we(t,e,i,n,o){var s,r,a=1+7*(e-1)+(7+i-n)%7+be(t,n,o);return a<=0?r=me(s=t-1)+a:a>me(t)?(s=t+1,r=a-me(t)):(s=t,r=a),{year:s,dayOfYear:r}}function _e(t,e,i){var n,o,s=be(t.year(),e,i),r=Math.floor((t.dayOfYear()-s-1)/7)+1;return r<1?n=r+xe(o=t.year()-1,e,i):r>xe(t.year(),e,i)?(n=r-xe(t.year(),e,i),o=t.year()+1):(o=t.year(),n=r),{week:n,year:o}}function xe(t,e,i){var n=be(t,e,i),o=be(t+1,e,i);return(me(t)-n+o)/7}function ke(t){return _e(t,this._week.dow,this._week.doy).week}Y("w",["ww",2],"wo","week"),Y("W",["WW",2],"Wo","isoWeek"),et("week","w"),et("isoWeek","W"),st("week",5),st("isoWeek",5),Pt("w",_t),Pt("ww",_t,vt),Pt("W",_t),Pt("WW",_t,vt),Yt(["w","ww","W","WW"],(function(t,e,i,n){e[n.substr(0,1)]=ht(t)}));var De={dow:0,doy:6};function Ce(){return this._week.dow}function Se(){return this._week.doy}function Te(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function Ee(t){var e=_e(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function Me(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}function Oe(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}function Ie(t,e){return t.slice(e,7).concat(t.slice(0,e))}Y("d",0,"do","day"),Y("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),Y("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),Y("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),Y("e",0,0,"weekday"),Y("E",0,0,"isoWeekday"),et("day","d"),et("weekday","e"),et("isoWeekday","E"),st("day",11),st("weekday",11),st("isoWeekday",11),Pt("d",_t),Pt("e",_t),Pt("E",_t),Pt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),Pt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),Pt("dddd",(function(t,e){return e.weekdaysRegex(t)})),Yt(["dd","ddd","dddd"],(function(t,e,i,n){var o=i._locale.weekdaysParse(t,n,i._strict);null!=o?e.d=o:f(i).invalidWeekday=t})),Yt(["d","e","E"],(function(t,e,i,n){e[n]=ht(t)}));var Ae="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Pe="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ne="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Fe=At,Re=At,Le=At;function je(t,e){var i=o(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?Ie(i,this._week.dow):t?i[t.day()]:i}function Ye(t){return!0===t?Ie(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort}function He(t){return!0===t?Ie(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin}function ze(t,e,i){var n,o,s,r=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],n=0;n<7;++n)s=p([2e3,1]).day(n),this._minWeekdaysParse[n]=this.weekdaysMin(s,"").toLocaleLowerCase(),this._shortWeekdaysParse[n]=this.weekdaysShort(s,"").toLocaleLowerCase(),this._weekdaysParse[n]=this.weekdays(s,"").toLocaleLowerCase();return i?"dddd"===e?-1!==(o=zt.call(this._weekdaysParse,r))?o:null:"ddd"===e?-1!==(o=zt.call(this._shortWeekdaysParse,r))?o:null:-1!==(o=zt.call(this._minWeekdaysParse,r))?o:null:"dddd"===e?-1!==(o=zt.call(this._weekdaysParse,r))||-1!==(o=zt.call(this._shortWeekdaysParse,r))||-1!==(o=zt.call(this._minWeekdaysParse,r))?o:null:"ddd"===e?-1!==(o=zt.call(this._shortWeekdaysParse,r))||-1!==(o=zt.call(this._weekdaysParse,r))||-1!==(o=zt.call(this._minWeekdaysParse,r))?o:null:-1!==(o=zt.call(this._minWeekdaysParse,r))||-1!==(o=zt.call(this._weekdaysParse,r))||-1!==(o=zt.call(this._shortWeekdaysParse,r))?o:null}function Be(t,e,i){var n,o,s;if(this._weekdaysParseExact)return ze.call(this,t,e,i);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),n=0;n<7;n++){if(o=p([2e3,1]).day(n),i&&!this._fullWeekdaysParse[n]&&(this._fullWeekdaysParse[n]=new RegExp("^"+this.weekdays(o,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[n]=new RegExp("^"+this.weekdaysShort(o,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[n]=new RegExp("^"+this.weekdaysMin(o,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[n]||(s="^"+this.weekdays(o,"")+"|^"+this.weekdaysShort(o,"")+"|^"+this.weekdaysMin(o,""),this._weekdaysParse[n]=new RegExp(s.replace(".",""),"i")),i&&"dddd"===e&&this._fullWeekdaysParse[n].test(t))return n;if(i&&"ddd"===e&&this._shortWeekdaysParse[n].test(t))return n;if(i&&"dd"===e&&this._minWeekdaysParse[n].test(t))return n;if(!i&&this._weekdaysParse[n].test(t))return n}}function We(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Me(t,this.localeData()),this.add(t-e,"d")):e}function Ge(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Ve(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=Oe(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7}function Ue(t){return this._weekdaysParseExact?(r(this,"_weekdaysRegex")||Xe.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(r(this,"_weekdaysRegex")||(this._weekdaysRegex=Fe),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)}function $e(t){return this._weekdaysParseExact?(r(this,"_weekdaysRegex")||Xe.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(r(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Re),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function qe(t){return this._weekdaysParseExact?(r(this,"_weekdaysRegex")||Xe.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(r(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Le),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Xe(){function t(t,e){return e.length-t.length}var e,i,n,o,s,r=[],a=[],l=[],h=[];for(e=0;e<7;e++)i=p([2e3,1]).day(e),n=Rt(this.weekdaysMin(i,"")),o=Rt(this.weekdaysShort(i,"")),s=Rt(this.weekdays(i,"")),r.push(n),a.push(o),l.push(s),h.push(n),h.push(o),h.push(s);r.sort(t),a.sort(t),l.sort(t),h.sort(t),this._weekdaysRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Ke(){return this.hours()%12||12}function Ze(){return this.hours()||24}function Qe(t,e){Y(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function Je(t,e){return e._meridiemParse}function ti(t){return"p"===(t+"").toLowerCase().charAt(0)}Y("H",["HH",2],0,"hour"),Y("h",["hh",2],0,Ke),Y("k",["kk",2],0,Ze),Y("hmm",0,0,(function(){return""+Ke.apply(this)+N(this.minutes(),2)})),Y("hmmss",0,0,(function(){return""+Ke.apply(this)+N(this.minutes(),2)+N(this.seconds(),2)})),Y("Hmm",0,0,(function(){return""+this.hours()+N(this.minutes(),2)})),Y("Hmmss",0,0,(function(){return""+this.hours()+N(this.minutes(),2)+N(this.seconds(),2)})),Qe("a",!0),Qe("A",!1),et("hour","h"),st("hour",13),Pt("a",Je),Pt("A",Je),Pt("H",_t),Pt("h",_t),Pt("k",_t),Pt("HH",_t,vt),Pt("hh",_t,vt),Pt("kk",_t,vt),Pt("hmm",xt),Pt("hmmss",kt),Pt("Hmm",xt),Pt("Hmmss",kt),jt(["H","HH"],Vt),jt(["k","kk"],(function(t,e,i){var n=ht(t);e[Vt]=24===n?0:n})),jt(["a","A"],(function(t,e,i){i._isPm=i._locale.isPM(t),i._meridiem=t})),jt(["h","hh"],(function(t,e,i){e[Vt]=ht(t),f(i).bigHour=!0})),jt("hmm",(function(t,e,i){var n=t.length-2;e[Vt]=ht(t.substr(0,n)),e[Ut]=ht(t.substr(n)),f(i).bigHour=!0})),jt("hmmss",(function(t,e,i){var n=t.length-4,o=t.length-2;e[Vt]=ht(t.substr(0,n)),e[Ut]=ht(t.substr(n,2)),e[$t]=ht(t.substr(o)),f(i).bigHour=!0})),jt("Hmm",(function(t,e,i){var n=t.length-2;e[Vt]=ht(t.substr(0,n)),e[Ut]=ht(t.substr(n))})),jt("Hmmss",(function(t,e,i){var n=t.length-4,o=t.length-2;e[Vt]=ht(t.substr(0,n)),e[Ut]=ht(t.substr(n,2)),e[$t]=ht(t.substr(o))}));var ei=/[ap]\.?m?\.?/i,ii=dt("Hours",!0);function ni(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"}var oi,si={calendar:A,longDateFormat:G,invalidDate:U,ordinal:q,dayOfMonthOrdinalParse:X,relativeTime:Z,months:Jt,monthsShort:te,week:De,weekdays:Ae,weekdaysMin:Ne,weekdaysShort:Pe,meridiemParse:ei},ri={},ai={};function li(t,e){var i,n=Math.min(t.length,e.length);for(i=0;i0;){if(n=ui(o.slice(0,e).join("-")))return n;if(i&&i.length>=e&&li(o,i)>=e-1)break;e--}s++}return oi}function ci(t){return null!=t.match("^[^/\\\\]*$")}function ui(t){var e=null;if(void 0===ri[t]&&Tn&&Tn.exports&&ci(t))try{e=oi._abbr,Sn("./locale/"+t),pi(e)}catch(e){ri[t]=null}return ri[t]}function pi(t,e){var i;return t&&((i=l(e)?gi(t):mi(t,e))?oi=i:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),oi._abbr}function mi(t,e){if(null!==e){var i,n=si;if(e.abbr=t,null!=ri[t])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),n=ri[t]._config;else if(null!=e.parentLocale)if(null!=ri[e.parentLocale])n=ri[e.parentLocale]._config;else{if(null==(i=ui(e.parentLocale)))return ai[e.parentLocale]||(ai[e.parentLocale]=[]),ai[e.parentLocale].push({name:t,config:e}),null;n=i._config}return ri[t]=new I(O(n,e)),ai[t]&&ai[t].forEach((function(t){mi(t.name,t.config)})),pi(t),ri[t]}return delete ri[t],null}function fi(t,e){if(null!=e){var i,n,o=si;null!=ri[t]&&null!=ri[t].parentLocale?ri[t].set(O(ri[t]._config,e)):(null!=(n=ui(t))&&(o=n._config),e=O(o,e),null==n&&(e.abbr=t),(i=new I(e)).parentLocale=ri[t],ri[t]=i),pi(t)}else null!=ri[t]&&(null!=ri[t].parentLocale?(ri[t]=ri[t].parentLocale,t===pi()&&pi(t)):null!=ri[t]&&delete ri[t]);return ri[t]}function gi(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return oi;if(!o(t)){if(e=ui(t))return e;t=[t]}return di(t)}function vi(){return C(ri)}function yi(t){var e,i=t._a;return i&&-2===f(t).overflow&&(e=i[Wt]<0||i[Wt]>11?Wt:i[Gt]<1||i[Gt]>Qt(i[Bt],i[Wt])?Gt:i[Vt]<0||i[Vt]>24||24===i[Vt]&&(0!==i[Ut]||0!==i[$t]||0!==i[qt])?Vt:i[Ut]<0||i[Ut]>59?Ut:i[$t]<0||i[$t]>59?$t:i[qt]<0||i[qt]>999?qt:-1,f(t)._overflowDayOfYear&&(eGt)&&(e=Gt),f(t)._overflowWeeks&&-1===e&&(e=Xt),f(t)._overflowWeekday&&-1===e&&(e=Kt),f(t).overflow=e),t}var bi=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,wi=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_i=/Z|[+-]\d\d(?::?\d\d)?/,xi=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],ki=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Di=/^\/?Date\((-?\d+)/i,Ci=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,Si={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Ti(t){var e,i,n,o,s,r,a=t._i,l=bi.exec(a)||wi.exec(a),h=xi.length,d=ki.length;if(l){for(f(t).iso=!0,e=0,i=h;eme(s)||0===t._dayOfYear)&&(f(t)._overflowDayOfYear=!0),i=ye(s,0,t._dayOfYear),t._a[Wt]=i.getUTCMonth(),t._a[Gt]=i.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=r[e]=n[e];for(;e<7;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[Vt]&&0===t._a[Ut]&&0===t._a[$t]&&0===t._a[qt]&&(t._nextDay=!0,t._a[Vt]=0),t._d=(t._useUTC?ye:ve).apply(null,r),o=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[Vt]=24),t._w&&void 0!==t._w.d&&t._w.d!==o&&(f(t).weekdayMismatch=!0)}}function ji(t){var e,i,n,o,s,r,a,l,h;null!=(e=t._w).GG||null!=e.W||null!=e.E?(s=1,r=4,i=Fi(e.GG,t._a[Bt],_e($i(),1,4).year),n=Fi(e.W,1),((o=Fi(e.E,1))<1||o>7)&&(l=!0)):(s=t._locale._week.dow,r=t._locale._week.doy,h=_e($i(),s,r),i=Fi(e.gg,t._a[Bt],h.year),n=Fi(e.w,h.week),null!=e.d?((o=e.d)<0||o>6)&&(l=!0):null!=e.e?(o=e.e+s,(e.e<0||e.e>6)&&(l=!0)):o=s),n<1||n>xe(i,s,r)?f(t)._overflowWeeks=!0:null!=l?f(t)._overflowWeekday=!0:(a=we(i,n,o,s,r),t._a[Bt]=a.year,t._dayOfYear=a.dayOfYear)}function Yi(t){if(t._f!==i.ISO_8601)if(t._f!==i.RFC_2822){t._a=[],f(t).empty=!0;var e,n,o,s,r,a,l,h=""+t._i,d=h.length,c=0;for(l=(o=W(t._f,t._locale).match(F)||[]).length,e=0;e0&&f(t).unusedInput.push(r),h=h.slice(h.indexOf(n)+n.length),c+=n.length),j[s]?(n?f(t).empty=!1:f(t).unusedTokens.push(s),Ht(s,n,t)):t._strict&&!n&&f(t).unusedTokens.push(s);f(t).charsLeftOver=d-c,h.length>0&&f(t).unusedInput.push(h),t._a[Vt]<=12&&!0===f(t).bigHour&&t._a[Vt]>0&&(f(t).bigHour=void 0),f(t).parsedDateParts=t._a.slice(0),f(t).meridiem=t._meridiem,t._a[Vt]=Hi(t._locale,t._a[Vt],t._meridiem),null!==(a=f(t).era)&&(t._a[Bt]=t._locale.erasConvertYear(a,t._a[Bt])),Li(t),yi(t)}else Pi(t);else Ti(t)}function Hi(t,e,i){var n;return null==i?e:null!=t.meridiemHour?t.meridiemHour(e,i):null!=t.isPM?((n=t.isPM(i))&&e<12&&(e+=12),n||12!==e||(e=0),e):e}function zi(t){var e,i,n,o,s,r,a=!1,l=t._f.length;if(0===l)return f(t).invalidFormat=!0,void(t._d=new Date(NaN));for(o=0;othis?this:t:v()}));function Ki(t,e){var i,n;if(1===e.length&&o(e[0])&&(e=e[0]),!e.length)return $i();for(i=e[0],n=1;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function _n(){if(!l(this._isDSTShifted))return this._isDSTShifted;var t,e={};return w(e,this),(e=Gi(e))._a?(t=e._isUTC?p(e._a):$i(e._a),this._isDSTShifted=this.isValid()&&ln(e._a,t.toArray())>0):this._isDSTShifted=!1,this._isDSTShifted}function xn(){return!!this.isValid()&&!this._isUTC}function kn(){return!!this.isValid()&&this._isUTC}function Dn(){return!!this.isValid()&&this._isUTC&&0===this._offset}i.updateOffset=function(){};var Cn=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,En=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Mn(t,e){var i,n,o,s=t,a=null;return rn(t)?s={ms:t._milliseconds,d:t._days,M:t._months}:h(t)||!isNaN(+t)?(s={},e?s[e]=+t:s.milliseconds=+t):(a=Cn.exec(t))?(i="-"===a[1]?-1:1,s={y:0,d:ht(a[Gt])*i,h:ht(a[Vt])*i,m:ht(a[Ut])*i,s:ht(a[$t])*i,ms:ht(an(1e3*a[qt]))*i}):(a=En.exec(t))?(i="-"===a[1]?-1:1,s={y:On(a[2],i),M:On(a[3],i),w:On(a[4],i),d:On(a[5],i),h:On(a[6],i),m:On(a[7],i),s:On(a[8],i)}):null==s?s={}:"object"==typeof s&&("from"in s||"to"in s)&&(o=An($i(s.from),$i(s.to)),(s={}).ms=o.milliseconds,s.M=o.months),n=new sn(s),rn(t)&&r(t,"_locale")&&(n._locale=t._locale),rn(t)&&r(t,"_isValid")&&(n._isValid=t._isValid),n}function On(t,e){var i=t&&parseFloat(t.replace(",","."));return(isNaN(i)?0:i)*e}function In(t,e){var i={};return i.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(i.months,"M").isAfter(e)&&--i.months,i.milliseconds=+e-+t.clone().add(i.months,"M"),i}function An(t,e){var i;return t.isValid()&&e.isValid()?(e=un(e,t),t.isBefore(e)?i=In(t,e):((i=In(e,t)).milliseconds=-i.milliseconds,i.months=-i.months),i):{milliseconds:0,months:0}}function Pn(t,e){return function(i,n){var o;return null===n||isNaN(+n)||(T(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),o=i,i=n,n=o),Nn(this,Mn(i,n),t),this}}function Nn(t,e,n,o){var s=e._milliseconds,r=an(e._days),a=an(e._months);t.isValid()&&(o=null==o||o,a&&le(t,ct(t,"Month")+a*n),r&&ut(t,"Date",ct(t,"Date")+r*n),s&&t._d.setTime(t._d.valueOf()+s*n),o&&i.updateOffset(t,r||a))}Mn.fn=sn.prototype,Mn.invalid=on;var Fn=Pn(1,"add"),Rn=Pn(-1,"subtract");function Ln(t){return"string"==typeof t||t instanceof String}function jn(t){return x(t)||d(t)||Ln(t)||h(t)||Hn(t)||Yn(t)||null==t}function Yn(t){var e,i,n=s(t)&&!a(t),o=!1,l=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"],h=l.length;for(e=0;ei.valueOf():i.valueOf()9999?B(i,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):E(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",B(i,"Z")):B(i,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")}function eo(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t,e,i,n,o="moment",s="";return this.isLocal()||(o=0===this.utcOffset()?"moment.utc":"moment.parseZone",s="Z"),t="["+o+'("]',e=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",i="-MM-DD[T]HH:mm:ss.SSS",n=s+'[")]',this.format(t+e+i+n)}function io(t){t||(t=this.isUtc()?i.defaultFormatUtc:i.defaultFormat);var e=B(this,t);return this.localeData().postformat(e)}function no(t,e){return this.isValid()&&(x(t)&&t.isValid()||$i(t).isValid())?Mn({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()}function oo(t){return this.from($i(),t)}function so(t,e){return this.isValid()&&(x(t)&&t.isValid()||$i(t).isValid())?Mn({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()}function ro(t){return this.to($i(),t)}function ao(t){var e;return void 0===t?this._locale._abbr:(null!=(e=gi(t))&&(this._locale=e),this)}i.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",i.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var lo=D("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",(function(t){return void 0===t?this.localeData():this.locale(t)}));function ho(){return this._locale}var co=1e3,uo=60*co,po=60*uo,mo=3506328*po;function fo(t,e){return(t%e+e)%e}function go(t,e,i){return t<100&&t>=0?new Date(t+400,e,i)-mo:new Date(t,e,i).valueOf()}function vo(t,e,i){return t<100&&t>=0?Date.UTC(t+400,e,i)-mo:Date.UTC(t,e,i)}function yo(t){var e,n;if(void 0===(t=it(t))||"millisecond"===t||!this.isValid())return this;switch(n=this._isUTC?vo:go,t){case"year":e=n(this.year(),0,1);break;case"quarter":e=n(this.year(),this.month()-this.month()%3,1);break;case"month":e=n(this.year(),this.month(),1);break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":e=n(this.year(),this.month(),this.date());break;case"hour":e=this._d.valueOf(),e-=fo(e+(this._isUTC?0:this.utcOffset()*uo),po);break;case"minute":e=this._d.valueOf(),e-=fo(e,uo);break;case"second":e=this._d.valueOf(),e-=fo(e,co)}return this._d.setTime(e),i.updateOffset(this,!0),this}function bo(t){var e,n;if(void 0===(t=it(t))||"millisecond"===t||!this.isValid())return this;switch(n=this._isUTC?vo:go,t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=po-fo(e+(this._isUTC?0:this.utcOffset()*uo),po)-1;break;case"minute":e=this._d.valueOf(),e+=uo-fo(e,uo)-1;break;case"second":e=this._d.valueOf(),e+=co-fo(e,co)-1}return this._d.setTime(e),i.updateOffset(this,!0),this}function wo(){return this._d.valueOf()-6e4*(this._offset||0)}function _o(){return Math.floor(this.valueOf()/1e3)}function xo(){return new Date(this.valueOf())}function ko(){var t=this;return[t.year(),t.month(),t.date(),t.hour(),t.minute(),t.second(),t.millisecond()]}function Do(){var t=this;return{years:t.year(),months:t.month(),date:t.date(),hours:t.hours(),minutes:t.minutes(),seconds:t.seconds(),milliseconds:t.milliseconds()}}function Co(){return this.isValid()?this.toISOString():null}function So(){return g(this)}function To(){return u({},f(this))}function Eo(){return f(this).overflow}function Mo(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Oo(t,e){var n,o,s,r=this._eras||gi("en")._eras;for(n=0,o=r.length;n=0)return l[n]}function Ao(t,e){var n=t.since<=t.until?1:-1;return void 0===e?i(t.since).year():i(t.since).year()+(e-t.offset)*n}function Po(){var t,e,i,n=this.localeData().eras();for(t=0,e=n.length;t(s=xe(t,n,o))&&(e=s),Jo.call(this,t,e,i,n,o))}function Jo(t,e,i,n,o){var s=we(t,e,i,n,o),r=ye(s.year,0,s.dayOfYear);return this.year(r.getUTCFullYear()),this.month(r.getUTCMonth()),this.date(r.getUTCDate()),this}function ts(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)}Y("N",0,0,"eraAbbr"),Y("NN",0,0,"eraAbbr"),Y("NNN",0,0,"eraAbbr"),Y("NNNN",0,0,"eraName"),Y("NNNNN",0,0,"eraNarrow"),Y("y",["y",1],"yo","eraYear"),Y("y",["yy",2],0,"eraYear"),Y("y",["yyy",3],0,"eraYear"),Y("y",["yyyy",4],0,"eraYear"),Pt("N",Ho),Pt("NN",Ho),Pt("NNN",Ho),Pt("NNNN",zo),Pt("NNNNN",Bo),jt(["N","NN","NNN","NNNN","NNNNN"],(function(t,e,i,n){var o=i._locale.erasParse(t,n,i._strict);o?f(i).era=o:f(i).invalidEra=t})),Pt("y",Tt),Pt("yy",Tt),Pt("yyy",Tt),Pt("yyyy",Tt),Pt("yo",Wo),jt(["y","yy","yyy","yyyy"],Bt),jt(["yo"],(function(t,e,i,n){var o;i._locale._eraYearOrdinalRegex&&(o=t.match(i._locale._eraYearOrdinalRegex)),i._locale.eraYearOrdinalParse?e[Bt]=i._locale.eraYearOrdinalParse(t,o):e[Bt]=parseInt(t,10)})),Y(0,["gg",2],0,(function(){return this.weekYear()%100})),Y(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),Vo("gggg","weekYear"),Vo("ggggg","weekYear"),Vo("GGGG","isoWeekYear"),Vo("GGGGG","isoWeekYear"),et("weekYear","gg"),et("isoWeekYear","GG"),st("weekYear",1),st("isoWeekYear",1),Pt("G",Et),Pt("g",Et),Pt("GG",_t,vt),Pt("gg",_t,vt),Pt("GGGG",Ct,bt),Pt("gggg",Ct,bt),Pt("GGGGG",St,wt),Pt("ggggg",St,wt),Yt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,i,n){e[n.substr(0,2)]=ht(t)})),Yt(["gg","GG"],(function(t,e,n,o){e[o]=i.parseTwoDigitYear(t)})),Y("Q",0,"Qo","quarter"),et("quarter","Q"),st("quarter",7),Pt("Q",gt),jt("Q",(function(t,e){e[Wt]=3*(ht(t)-1)})),Y("D",["DD",2],"Do","date"),et("date","D"),st("date",9),Pt("D",_t),Pt("DD",_t,vt),Pt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),jt(["D","DD"],Gt),jt("Do",(function(t,e){e[Gt]=ht(t.match(_t)[0])}));var es=dt("Date",!0);function is(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}Y("DDD",["DDDD",3],"DDDo","dayOfYear"),et("dayOfYear","DDD"),st("dayOfYear",4),Pt("DDD",Dt),Pt("DDDD",yt),jt(["DDD","DDDD"],(function(t,e,i){i._dayOfYear=ht(t)})),Y("m",["mm",2],0,"minute"),et("minute","m"),st("minute",14),Pt("m",_t),Pt("mm",_t,vt),jt(["m","mm"],Ut);var ns=dt("Minutes",!1);Y("s",["ss",2],0,"second"),et("second","s"),st("second",15),Pt("s",_t),Pt("ss",_t,vt),jt(["s","ss"],$t);var os,ss,rs=dt("Seconds",!1);for(Y("S",0,0,(function(){return~~(this.millisecond()/100)})),Y(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),Y(0,["SSS",3],0,"millisecond"),Y(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),Y(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),Y(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),Y(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),Y(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),Y(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),et("millisecond","ms"),st("millisecond",16),Pt("S",Dt,gt),Pt("SS",Dt,vt),Pt("SSS",Dt,yt),os="SSSS";os.length<=9;os+="S")Pt(os,Tt);function as(t,e){e[qt]=ht(1e3*("0."+t))}for(os="S";os.length<=9;os+="S")jt(os,as);function ls(){return this._isUTC?"UTC":""}function hs(){return this._isUTC?"Coordinated Universal Time":""}ss=dt("Milliseconds",!1),Y("z",0,0,"zoneAbbr"),Y("zz",0,0,"zoneName");var ds=_.prototype;function cs(t){return $i(1e3*t)}function us(){return $i.apply(null,arguments).parseZone()}function ps(t){return t}ds.add=Fn,ds.calendar=Wn,ds.clone=Gn,ds.diff=Zn,ds.endOf=bo,ds.format=io,ds.from=no,ds.fromNow=oo,ds.to=so,ds.toNow=ro,ds.get=pt,ds.invalidAt=Eo,ds.isAfter=Vn,ds.isBefore=Un,ds.isBetween=$n,ds.isSame=qn,ds.isSameOrAfter=Xn,ds.isSameOrBefore=Kn,ds.isValid=So,ds.lang=lo,ds.locale=ao,ds.localeData=ho,ds.max=Xi,ds.min=qi,ds.parsingFlags=To,ds.set=mt,ds.startOf=yo,ds.subtract=Rn,ds.toArray=ko,ds.toObject=Do,ds.toDate=xo,ds.toISOString=to,ds.inspect=eo,"undefined"!=typeof Symbol&&null!=Symbol.for&&(ds[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),ds.toJSON=Co,ds.toString=Jn,ds.unix=_o,ds.valueOf=wo,ds.creationData=Mo,ds.eraName=Po,ds.eraNarrow=No,ds.eraAbbr=Fo,ds.eraYear=Ro,ds.year=fe,ds.isLeapYear=ge,ds.weekYear=Uo,ds.isoWeekYear=$o,ds.quarter=ds.quarters=ts,ds.month=he,ds.daysInMonth=de,ds.week=ds.weeks=Te,ds.isoWeek=ds.isoWeeks=Ee,ds.weeksInYear=Ko,ds.weeksInWeekYear=Zo,ds.isoWeeksInYear=qo,ds.isoWeeksInISOWeekYear=Xo,ds.date=es,ds.day=ds.days=We,ds.weekday=Ge,ds.isoWeekday=Ve,ds.dayOfYear=is,ds.hour=ds.hours=ii,ds.minute=ds.minutes=ns,ds.second=ds.seconds=rs,ds.millisecond=ds.milliseconds=ss,ds.utcOffset=mn,ds.utc=gn,ds.local=vn,ds.parseZone=yn,ds.hasAlignedHourOffset=bn,ds.isDST=wn,ds.isLocal=xn,ds.isUtcOffset=kn,ds.isUtc=Dn,ds.isUTC=Dn,ds.zoneAbbr=ls,ds.zoneName=hs,ds.dates=D("dates accessor is deprecated. Use date instead.",es),ds.months=D("months accessor is deprecated. Use month instead",he),ds.years=D("years accessor is deprecated. Use year instead",fe),ds.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",fn),ds.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",_n);var ms=I.prototype;function fs(t,e,i,n){var o=gi(),s=p().set(n,e);return o[i](s,t)}function gs(t,e,i){if(h(t)&&(e=t,t=void 0),t=t||"",null!=e)return fs(t,e,i,"month");var n,o=[];for(n=0;n<12;n++)o[n]=fs(t,n,i,"month");return o}function vs(t,e,i,n){"boolean"==typeof t?(h(e)&&(i=e,e=void 0),e=e||""):(i=e=t,t=!1,h(e)&&(i=e,e=void 0),e=e||"");var o,s=gi(),r=t?s._week.dow:0,a=[];if(null!=i)return fs(e,(i+r)%7,n,"day");for(o=0;o<7;o++)a[o]=fs(e,(o+r)%7,n,"day");return a}function ys(t,e){return gs(t,e,"months")}function bs(t,e){return gs(t,e,"monthsShort")}function ws(t,e,i){return vs(t,e,i,"weekdays")}function _s(t,e,i){return vs(t,e,i,"weekdaysShort")}function xs(t,e,i){return vs(t,e,i,"weekdaysMin")}ms.calendar=P,ms.longDateFormat=V,ms.invalidDate=$,ms.ordinal=K,ms.preparse=ps,ms.postformat=ps,ms.relativeTime=Q,ms.pastFuture=J,ms.set=M,ms.eras=Oo,ms.erasParse=Io,ms.erasConvertYear=Ao,ms.erasAbbrRegex=jo,ms.erasNameRegex=Lo,ms.erasNarrowRegex=Yo,ms.months=oe,ms.monthsShort=se,ms.monthsParse=ae,ms.monthsRegex=ue,ms.monthsShortRegex=ce,ms.week=ke,ms.firstDayOfYear=Se,ms.firstDayOfWeek=Ce,ms.weekdays=je,ms.weekdaysMin=He,ms.weekdaysShort=Ye,ms.weekdaysParse=Be,ms.weekdaysRegex=Ue,ms.weekdaysShortRegex=$e,ms.weekdaysMinRegex=qe,ms.isPM=ti,ms.meridiem=ni,pi("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===ht(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),i.lang=D("moment.lang is deprecated. Use moment.locale instead.",pi),i.langData=D("moment.langData is deprecated. Use moment.localeData instead.",gi);var ks=Math.abs;function Ds(){var t=this._data;return this._milliseconds=ks(this._milliseconds),this._days=ks(this._days),this._months=ks(this._months),t.milliseconds=ks(t.milliseconds),t.seconds=ks(t.seconds),t.minutes=ks(t.minutes),t.hours=ks(t.hours),t.months=ks(t.months),t.years=ks(t.years),this}function Cs(t,e,i,n){var o=Mn(e,i);return t._milliseconds+=n*o._milliseconds,t._days+=n*o._days,t._months+=n*o._months,t._bubble()}function Ss(t,e){return Cs(this,t,e,1)}function Ts(t,e){return Cs(this,t,e,-1)}function Es(t){return t<0?Math.floor(t):Math.ceil(t)}function Ms(){var t,e,i,n,o,s=this._milliseconds,r=this._days,a=this._months,l=this._data;return s>=0&&r>=0&&a>=0||s<=0&&r<=0&&a<=0||(s+=864e5*Es(Is(a)+r),r=0,a=0),l.milliseconds=s%1e3,t=lt(s/1e3),l.seconds=t%60,e=lt(t/60),l.minutes=e%60,i=lt(e/60),l.hours=i%24,r+=lt(i/24),a+=o=lt(Os(r)),r-=Es(Is(o)),n=lt(a/12),a%=12,l.days=r,l.months=a,l.years=n,this}function Os(t){return 4800*t/146097}function Is(t){return 146097*t/4800}function As(t){if(!this.isValid())return NaN;var e,i,n=this._milliseconds;if("month"===(t=it(t))||"quarter"===t||"year"===t)switch(e=this._days+n/864e5,i=this._months+Os(e),t){case"month":return i;case"quarter":return i/3;case"year":return i/12}else switch(e=this._days+Math.round(Is(this._months)),t){case"week":return e/7+n/6048e5;case"day":return e+n/864e5;case"hour":return 24*e+n/36e5;case"minute":return 1440*e+n/6e4;case"second":return 86400*e+n/1e3;case"millisecond":return Math.floor(864e5*e)+n;default:throw new Error("Unknown unit "+t)}}function Ps(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*ht(this._months/12):NaN}function Ns(t){return function(){return this.as(t)}}var Fs=Ns("ms"),Rs=Ns("s"),Ls=Ns("m"),js=Ns("h"),Ys=Ns("d"),Hs=Ns("w"),zs=Ns("M"),Bs=Ns("Q"),Ws=Ns("y");function Gs(){return Mn(this)}function Vs(t){return t=it(t),this.isValid()?this[t+"s"]():NaN}function Us(t){return function(){return this.isValid()?this._data[t]:NaN}}var $s=Us("milliseconds"),qs=Us("seconds"),Xs=Us("minutes"),Ks=Us("hours"),Zs=Us("days"),Qs=Us("months"),Js=Us("years");function tr(){return lt(this.days()/7)}var er=Math.round,ir={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function nr(t,e,i,n,o){return o.relativeTime(e||1,!!i,t,n)}function or(t,e,i,n){var o=Mn(t).abs(),s=er(o.as("s")),r=er(o.as("m")),a=er(o.as("h")),l=er(o.as("d")),h=er(o.as("M")),d=er(o.as("w")),c=er(o.as("y")),u=s<=i.ss&&["s",s]||s0,u[4]=n,nr.apply(null,u)}function sr(t){return void 0===t?er:"function"==typeof t&&(er=t,!0)}function rr(t,e){return void 0!==ir[t]&&(void 0===e?ir[t]:(ir[t]=e,"s"===t&&(ir.ss=e-1),!0))}function ar(t,e){if(!this.isValid())return this.localeData().invalidDate();var i,n,o=!1,s=ir;return"object"==typeof t&&(e=t,t=!1),"boolean"==typeof t&&(o=t),"object"==typeof e&&(s=Object.assign({},ir,e),null!=e.s&&null==e.ss&&(s.ss=e.s-1)),n=or(this,!o,s,i=this.localeData()),o&&(n=i.pastFuture(+this,n)),i.postformat(n)}var lr=Math.abs;function hr(t){return(t>0)-(t<0)||+t}function dr(){if(!this.isValid())return this.localeData().invalidDate();var t,e,i,n,o,s,r,a,l=lr(this._milliseconds)/1e3,h=lr(this._days),d=lr(this._months),c=this.asSeconds();return c?(t=lt(l/60),e=lt(t/60),l%=60,t%=60,i=lt(d/12),d%=12,n=l?l.toFixed(3).replace(/\.?0+$/,""):"",o=c<0?"-":"",s=hr(this._months)!==hr(c)?"-":"",r=hr(this._days)!==hr(c)?"-":"",a=hr(this._milliseconds)!==hr(c)?"-":"",o+"P"+(i?s+i+"Y":"")+(d?s+d+"M":"")+(h?r+h+"D":"")+(e||t||l?"T":"")+(e?a+e+"H":"")+(t?a+t+"M":"")+(l?a+n+"S":"")):"P0D"}var cr=sn.prototype;return cr.isValid=nn,cr.abs=Ds,cr.add=Ss,cr.subtract=Ts,cr.as=As,cr.asMilliseconds=Fs,cr.asSeconds=Rs,cr.asMinutes=Ls,cr.asHours=js,cr.asDays=Ys,cr.asWeeks=Hs,cr.asMonths=zs,cr.asQuarters=Bs,cr.asYears=Ws,cr.valueOf=Ps,cr._bubble=Ms,cr.clone=Gs,cr.get=Vs,cr.milliseconds=$s,cr.seconds=qs,cr.minutes=Xs,cr.hours=Ks,cr.days=Zs,cr.weeks=tr,cr.months=Qs,cr.years=Js,cr.humanize=ar,cr.toISOString=dr,cr.toString=dr,cr.toJSON=dr,cr.locale=ao,cr.localeData=ho,cr.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",dr),cr.lang=lo,Y("X",0,0,"unix"),Y("x",0,0,"valueOf"),Pt("x",Et),Pt("X",It),jt("X",(function(t,e,i){i._d=new Date(1e3*parseFloat(t))})),jt("x",(function(t,e,i){i._d=new Date(ht(t))})), +//! moment.js +i.version="2.29.4",n($i),i.fn=ds,i.min=Zi,i.max=Qi,i.now=Ji,i.utc=p,i.unix=cs,i.months=ys,i.isDate=d,i.locale=pi,i.invalid=v,i.duration=Mn,i.isMoment=x,i.weekdays=ws,i.parseZone=us,i.localeData=gi,i.isDuration=rn,i.monthsShort=bs,i.weekdaysMin=xs,i.defineLocale=mi,i.updateLocale=fi,i.locales=vi,i.weekdaysShort=_s,i.normalizeUnits=it,i.relativeTimeRounding=sr,i.relativeTimeThreshold=rr,i.calendarFormat=Bn,i.prototype=ds,i.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},i}();var Mn=En.exports,On={exports:{}},In={},An={exports:{}},Pn={};function Nn(){var t={"align-content":!1,"align-items":!1,"align-self":!1,"alignment-adjust":!1,"alignment-baseline":!1,all:!1,"anchor-point":!1,animation:!1,"animation-delay":!1,"animation-direction":!1,"animation-duration":!1,"animation-fill-mode":!1,"animation-iteration-count":!1,"animation-name":!1,"animation-play-state":!1,"animation-timing-function":!1,azimuth:!1,"backface-visibility":!1,background:!0,"background-attachment":!0,"background-clip":!0,"background-color":!0,"background-image":!0,"background-origin":!0,"background-position":!0,"background-repeat":!0,"background-size":!0,"baseline-shift":!1,binding:!1,bleed:!1,"bookmark-label":!1,"bookmark-level":!1,"bookmark-state":!1,border:!0,"border-bottom":!0,"border-bottom-color":!0,"border-bottom-left-radius":!0,"border-bottom-right-radius":!0,"border-bottom-style":!0,"border-bottom-width":!0,"border-collapse":!0,"border-color":!0,"border-image":!0,"border-image-outset":!0,"border-image-repeat":!0,"border-image-slice":!0,"border-image-source":!0,"border-image-width":!0,"border-left":!0,"border-left-color":!0,"border-left-style":!0,"border-left-width":!0,"border-radius":!0,"border-right":!0,"border-right-color":!0,"border-right-style":!0,"border-right-width":!0,"border-spacing":!0,"border-style":!0,"border-top":!0,"border-top-color":!0,"border-top-left-radius":!0,"border-top-right-radius":!0,"border-top-style":!0,"border-top-width":!0,"border-width":!0,bottom:!1,"box-decoration-break":!0,"box-shadow":!0,"box-sizing":!0,"box-snap":!0,"box-suppress":!0,"break-after":!0,"break-before":!0,"break-inside":!0,"caption-side":!1,chains:!1,clear:!0,clip:!1,"clip-path":!1,"clip-rule":!1,color:!0,"color-interpolation-filters":!0,"column-count":!1,"column-fill":!1,"column-gap":!1,"column-rule":!1,"column-rule-color":!1,"column-rule-style":!1,"column-rule-width":!1,"column-span":!1,"column-width":!1,columns:!1,contain:!1,content:!1,"counter-increment":!1,"counter-reset":!1,"counter-set":!1,crop:!1,cue:!1,"cue-after":!1,"cue-before":!1,cursor:!1,direction:!1,display:!0,"display-inside":!0,"display-list":!0,"display-outside":!0,"dominant-baseline":!1,elevation:!1,"empty-cells":!1,filter:!1,flex:!1,"flex-basis":!1,"flex-direction":!1,"flex-flow":!1,"flex-grow":!1,"flex-shrink":!1,"flex-wrap":!1,float:!1,"float-offset":!1,"flood-color":!1,"flood-opacity":!1,"flow-from":!1,"flow-into":!1,font:!0,"font-family":!0,"font-feature-settings":!0,"font-kerning":!0,"font-language-override":!0,"font-size":!0,"font-size-adjust":!0,"font-stretch":!0,"font-style":!0,"font-synthesis":!0,"font-variant":!0,"font-variant-alternates":!0,"font-variant-caps":!0,"font-variant-east-asian":!0,"font-variant-ligatures":!0,"font-variant-numeric":!0,"font-variant-position":!0,"font-weight":!0,grid:!1,"grid-area":!1,"grid-auto-columns":!1,"grid-auto-flow":!1,"grid-auto-rows":!1,"grid-column":!1,"grid-column-end":!1,"grid-column-start":!1,"grid-row":!1,"grid-row-end":!1,"grid-row-start":!1,"grid-template":!1,"grid-template-areas":!1,"grid-template-columns":!1,"grid-template-rows":!1,"hanging-punctuation":!1,height:!0,hyphens:!1,icon:!1,"image-orientation":!1,"image-resolution":!1,"ime-mode":!1,"initial-letters":!1,"inline-box-align":!1,"justify-content":!1,"justify-items":!1,"justify-self":!1,left:!1,"letter-spacing":!0,"lighting-color":!0,"line-box-contain":!1,"line-break":!1,"line-grid":!1,"line-height":!1,"line-snap":!1,"line-stacking":!1,"line-stacking-ruby":!1,"line-stacking-shift":!1,"line-stacking-strategy":!1,"list-style":!0,"list-style-image":!0,"list-style-position":!0,"list-style-type":!0,margin:!0,"margin-bottom":!0,"margin-left":!0,"margin-right":!0,"margin-top":!0,"marker-offset":!1,"marker-side":!1,marks:!1,mask:!1,"mask-box":!1,"mask-box-outset":!1,"mask-box-repeat":!1,"mask-box-slice":!1,"mask-box-source":!1,"mask-box-width":!1,"mask-clip":!1,"mask-image":!1,"mask-origin":!1,"mask-position":!1,"mask-repeat":!1,"mask-size":!1,"mask-source-type":!1,"mask-type":!1,"max-height":!0,"max-lines":!1,"max-width":!0,"min-height":!0,"min-width":!0,"move-to":!1,"nav-down":!1,"nav-index":!1,"nav-left":!1,"nav-right":!1,"nav-up":!1,"object-fit":!1,"object-position":!1,opacity:!1,order:!1,orphans:!1,outline:!1,"outline-color":!1,"outline-offset":!1,"outline-style":!1,"outline-width":!1,overflow:!1,"overflow-wrap":!1,"overflow-x":!1,"overflow-y":!1,padding:!0,"padding-bottom":!0,"padding-left":!0,"padding-right":!0,"padding-top":!0,page:!1,"page-break-after":!1,"page-break-before":!1,"page-break-inside":!1,"page-policy":!1,pause:!1,"pause-after":!1,"pause-before":!1,perspective:!1,"perspective-origin":!1,pitch:!1,"pitch-range":!1,"play-during":!1,position:!1,"presentation-level":!1,quotes:!1,"region-fragment":!1,resize:!1,rest:!1,"rest-after":!1,"rest-before":!1,richness:!1,right:!1,rotation:!1,"rotation-point":!1,"ruby-align":!1,"ruby-merge":!1,"ruby-position":!1,"shape-image-threshold":!1,"shape-outside":!1,"shape-margin":!1,size:!1,speak:!1,"speak-as":!1,"speak-header":!1,"speak-numeral":!1,"speak-punctuation":!1,"speech-rate":!1,stress:!1,"string-set":!1,"tab-size":!1,"table-layout":!1,"text-align":!0,"text-align-last":!0,"text-combine-upright":!0,"text-decoration":!0,"text-decoration-color":!0,"text-decoration-line":!0,"text-decoration-skip":!0,"text-decoration-style":!0,"text-emphasis":!0,"text-emphasis-color":!0,"text-emphasis-position":!0,"text-emphasis-style":!0,"text-height":!0,"text-indent":!0,"text-justify":!0,"text-orientation":!0,"text-overflow":!0,"text-shadow":!0,"text-space-collapse":!0,"text-transform":!0,"text-underline-position":!0,"text-wrap":!0,top:!1,transform:!1,"transform-origin":!1,"transform-style":!1,transition:!1,"transition-delay":!1,"transition-duration":!1,"transition-property":!1,"transition-timing-function":!1,"unicode-bidi":!1,"vertical-align":!1,visibility:!1,"voice-balance":!1,"voice-duration":!1,"voice-family":!1,"voice-pitch":!1,"voice-range":!1,"voice-rate":!1,"voice-stress":!1,"voice-volume":!1,volume:!1,"white-space":!1,widows:!1,width:!0,"will-change":!1,"word-break":!0,"word-spacing":!0,"word-wrap":!0,"wrap-flow":!1,"wrap-through":!1,"writing-mode":!1,"z-index":!1};return t}var Fn=/javascript\s*\:/gim;Pn.whiteList=Nn(),Pn.getDefaultWhiteList=Nn,Pn.onAttr=function(t,e,i){},Pn.onIgnoreAttr=function(t,e,i){},Pn.safeAttrValue=function(t,e){return Fn.test(e)?"":e};var Rn={indexOf:function(t,e){var i,n;if(Array.prototype.indexOf)return t.indexOf(e);for(i=0,n=t.length;i/g,Qn=/"/g,Jn=/"/g,to=/&#([a-zA-Z0-9]*);?/gim,eo=/:?/gim,io=/&newline;?/gim,no=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a):/gi,oo=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,so=/u\s*r\s*l\s*\(.*/gi;function ro(t){return t.replace(Qn,""")}function ao(t){return t.replace(Jn,'"')}function lo(t){return t.replace(to,(function(t,e){return"x"===e[0]||"X"===e[0]?String.fromCharCode(parseInt(e.substr(1),16)):String.fromCharCode(parseInt(e,10))}))}function ho(t){return t.replace(eo,":").replace(io," ")}function co(t){for(var e="",i=0,n=t.length;i0;e--){var i=t[e];if(" "!==i)return"="===i?e:-1}}function xo(t){return function(t){return'"'===t[0]&&'"'===t[t.length-1]||"'"===t[0]&&"'"===t[t.length-1]}(t)?t.substr(1,t.length-2):t}mo.parseTag=function(t,e,i){var n="",o=0,s=!1,r=!1,a=0,l=t.length,h="",d="";t:for(a=0;a"===c||a===l-1){n+=i(t.slice(o,s)),h=go(d=t.slice(s,a+1)),n+=e(s,n.length,h,d,vo(d)),o=a+1,s=!1;continue}if('"'===c||"'"===c)for(var u=1,p=t.charAt(a-u);""===p.trim()||"="===p;){if("="===p){r=c;continue t}p=t.charAt(a-++u)}}else if(c===r){r=!1;continue}}return o";var f=function(t){var e=Eo.spaceIndex(t);if(-1===e)return{html:"",closing:"/"===t[t.length-2]};var i="/"===(t=Eo.trim(t.slice(e+1,-1)))[t.length-1];return i&&(t=Eo.trim(t.slice(0,-1))),{html:t,closing:i}}(c),g=i[d],v=To(f.html,(function(t,e){var i=-1!==Eo.indexOf(g,t),n=s(d,t,e,i);return Mo(n)?i?(e=a(d,t,e,h))?t+'="'+e+'"':t:Mo(n=r(d,t,e,i))?void 0:n:n}));return c="<"+d,v&&(c+=" "+v),f.closing&&(c+=" /"),c+=">"}return Mo(m=o(d,c,p))?l(c):m}),l);return d&&(c=d.remove(c)),c};var Io=Oo;!function(t,e){var i=In,n=mo,o=Io;function s(t,e){return new o(e).process(t)}(e=t.exports=s).filterXSS=s,e.FilterXSS=o,function(){for(var t in i)e[t]=i[t];for(var o in n)e[o]=n[o]}(),"undefined"!=typeof window&&(window.filterXSS=t.exports),"undefined"!=typeof self&&"undefined"!=typeof DedicatedWorkerGlobalScope&&self instanceof DedicatedWorkerGlobalScope&&(self.filterXSS=t.exports)}(On,On.exports);var Ao=On.exports,Po=null; +/** + * vis-timeline and vis-graph2d + * https://visjs.github.io/vis-timeline/ + * + * Create a fully customizable, interactive timeline with items and ranges. + * + * @version 7.7.0 + * @date 2022-07-10T21:34:08.601Z + * + * @copyright (c) 2011-2017 Almende B.V, http://almende.com + * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs + * + * @license + * vis.js is dual licensed under both + * + * 1. The Apache 2.0 License + * http://www.apache.org/licenses/LICENSE-2.0 + * + * and + * + * 2. The MIT License + * http://opensource.org/licenses/MIT + * + * vis.js may be distributed under either license. + */ +var No="undefined"!=typeof window&&window.moment||Mn;function Fo(t){if(!t)return!1;let e=t.idProp??t._idProp;return!!e&&function(t,e){return"object"==typeof e&&null!==e&&t===e.idProp&&"function"==typeof e.forEach&&"function"==typeof e.get&&"function"==typeof e.getDataSet&&"function"==typeof e.getIds&&"number"==typeof e.length&&"function"==typeof e.map&&"function"==typeof e.off&&"function"==typeof e.on&&"function"==typeof e.stream&&function(t,e){return"object"==typeof e&&null!==e&&t===e.idProp&&"function"==typeof e.add&&"function"==typeof e.clear&&"function"==typeof e.distinct&&"function"==typeof e.forEach&&"function"==typeof e.get&&"function"==typeof e.getDataSet&&"function"==typeof e.getIds&&"number"==typeof e.length&&"function"==typeof e.map&&"function"==typeof e.max&&"function"==typeof e.min&&"function"==typeof e.off&&"function"==typeof e.on&&"function"==typeof e.remove&&"function"==typeof e.setOptions&&"function"==typeof e.stream&&"function"==typeof e.update&&"function"==typeof e.updateOnly}(t,e.getDataSet())}(e,t)}const Ro=/^\/?Date\((-?\d+)/i,Lo=/^\d+$/;function jo(t,e){let i;if(void 0!==t){if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return pi(t)&&!isNaN(Date.parse(t))?Mn(t).valueOf():Number(t.valueOf());case"string":case"String":return String(t);case"Date":try{return jo(t,"Moment").toDate()}catch(i){throw i instanceof TypeError?new TypeError("Cannot convert object of type "+yi(t)+" to type "+e):i}case"Moment":if(ui(t))return Mn(t);if(t instanceof Date)return Mn(t.valueOf());if(Mn.isMoment(t))return Mn(t);if(pi(t))return i=Ro.exec(t),i?Mn(Number(i[1])):(i=Lo.exec(t),Mn(i?Number(t):t));throw new TypeError("Cannot convert object of type "+yi(t)+" to type "+e);case"ISODate":if(ui(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(Mn.isMoment(t))return t.toDate().toISOString();if(pi(t))return i=Ro.exec(t),i?new Date(Number(i[1])).toISOString():Mn(t).format();throw new Error("Cannot convert object of type "+yi(t)+" to type ISODate");case"ASPDate":if(ui(t))return"/Date("+t+")/";if(t instanceof Date||Mn.isMoment(t))return"/Date("+t.valueOf()+")/";if(pi(t)){let e;return i=Ro.exec(t),e=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+e+")/"}throw new Error("Cannot convert object of type "+yi(t)+" to type ASPDate");default:throw new Error(`Unknown type ${e}`)}}}function Yo(t,e={start:"Date",end:"Date"}){const i=t._idProp,n=new Dn({fieldId:i}),o=(s=t,new bn(s)).map((t=>Object.keys(t).reduce(((i,n)=>(i[n]=jo(t[n],e[n]),i)),{}))).to(n); +/** + * vis-data + * http://visjs.org/ + * + * Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data. + * + * @version 7.1.4 + * @date 2022-03-15T15:23:59.245Z + * + * @copyright (c) 2011-2017 Almende B.V, http://almende.com + * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs + * + * @license + * vis.js is dual licensed under both + * + * 1. The Apache 2.0 License + * http://www.apache.org/licenses/LICENSE-2.0 + * + * and + * + * 2. The MIT License + * http://opensource.org/licenses/MIT + * + * vis.js may be distributed under either license. + */ +var s;return o.all().start(),{add:(...e)=>t.getDataSet().add(...e),remove:(...e)=>t.getDataSet().remove(...e),update:(...e)=>t.getDataSet().update(...e),updateOnly:(...e)=>t.getDataSet().updateOnly(...e),clear:(...e)=>t.getDataSet().clear(...e),forEach:n.forEach.bind(n),get:n.get.bind(n),getIds:n.getIds.bind(n),off:n.off.bind(n),on:n.on.bind(n),get length(){return n.length},idProp:i,type:e,rawDS:t,coercedDS:n,dispose:()=>o.stop()}}const Ho=t=>{const e=new Ao.FilterXSS(t);return t=>e.process(t)},zo=t=>t;let Bo=Ho();const Wo={...Ki,convert:jo,setupXSSProtection:t=>{t&&(!0===t.disabled?(Bo=zo,console.warn("You disabled XSS protection for vis-Timeline. I sure hope you know what you're doing!")):t.filterOptions&&(Bo=Ho(t.filterOptions)))}};Object.defineProperty(Wo,"xss",{get:function(){return Bo}});class Go{constructor(t,e){this.options=null,this.props=null}setOptions(t){t&&Wo.extend(this.options,t)}redraw(){return!1}destroy(){}_isResized(){const t=this.props._previousWidth!==this.props.width||this.props._previousHeight!==this.props.height;return this.props._previousWidth=this.props.width,this.props._previousHeight=this.props.height,t}}function Vo(t,e,i){if(i&&!Array.isArray(i))return Vo(t,e,[i]);if(e.hiddenDates=[],i&&1==Array.isArray(i)){for(let n=0;nt.start-e.start))}}function Uo(t,e,i){if(i&&!Array.isArray(i))return Uo(t,e,[i]);if(i&&void 0!==e.domProps.centerContainer.width){Vo(t,e,i);const n=t(e.range.start),o=t(e.range.end),s=(e.range.end-e.range.start)/e.domProps.centerContainer.width;for(let r=0;r=4*s){let t=0;const s=o.clone();switch(i[r].repeat){case"daily":a.day()!=l.day()&&(t=1),a.dayOfYear(n.dayOfYear()),a.year(n.year()),a.subtract(7,"days"),l.dayOfYear(n.dayOfYear()),l.year(n.year()),l.subtract(7-t,"days"),s.add(1,"weeks");break;case"weekly":{const t=l.diff(a,"days"),e=a.day();a.date(n.date()),a.month(n.month()),a.year(n.year()),l=a.clone(),a.day(e),l.day(e),l.add(t,"days"),a.subtract(1,"weeks"),l.subtract(1,"weeks"),s.add(1,"weeks");break}case"monthly":a.month()!=l.month()&&(t=1),a.month(n.month()),a.year(n.year()),a.subtract(1,"months"),l.month(n.month()),l.year(n.year()),l.subtract(1,"months"),l.add(t,"months"),s.add(1,"months");break;case"yearly":a.year()!=l.year()&&(t=1),a.year(n.year()),a.subtract(1,"years"),l.year(n.year()),l.subtract(1,"years"),l.add(t,"years"),s.add(1,"years");break;default:return void console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:",i[r].repeat)}for(;a=e[n].start&&e[t].end<=e[n].end?e[t].remove=!0:e[t].start>=e[n].start&&e[t].start<=e[n].end?(e[n].end=e[t].end,e[t].remove=!0):e[t].end>=e[n].start&&e[t].end<=e[n].end&&(e[n].start=e[t].start,e[t].remove=!0));for(n=0;nt.start-e.start))}(e);const r=Jo(e.range.start,e.hiddenDates),a=Jo(e.range.end,e.hiddenDates);let l=e.range.start,h=e.range.end;1==r.hidden&&(l=1==e.range.startToFront?r.startDate-1:r.endDate+1),1==a.hidden&&(h=1==e.range.endToFront?a.startDate-1:a.endDate+1),1!=r.hidden&&1!=a.hidden||e.range._applyRange(l,h)}}function $o(t,e,i){let n;if(0==t.body.hiddenDates.length)return n=t.range.conversion(i),(e.valueOf()-n.offset)*n.scale;{const o=Jo(e,t.body.hiddenDates);1==o.hidden&&(e=o.startDate);const s=Xo(t.body.hiddenDates,t.range.start,t.range.end);if(e=e&&r<=i&&(n+=r-s)}return n}(t.body.hiddenDates,e,n.offset);return e=t.options.moment(e).toDate().valueOf(),e+=o,-(n.offset-e.valueOf())*n.scale}if(e>t.range.end){const o={start:t.range.start,end:e};return e=Ko(t.options.moment,t.body.hiddenDates,o,e),n=t.range.conversion(i,s),(e.valueOf()-n.offset)*n.scale}return e=Ko(t.options.moment,t.body.hiddenDates,t.range,e),n=t.range.conversion(i,s),(e.valueOf()-n.offset)*n.scale}}function qo(t,e,i){if(0==t.body.hiddenDates.length){const n=t.range.conversion(i);return new Date(e/n.scale+n.offset)}{const n=Xo(t.body.hiddenDates,t.range.start,t.range.end),o=(t.range.end-t.range.start-n)*e/i,s=function(t,e,i){let n=0,o=0,s=e.start;for(let r=0;r=e.start&&l=i)break;n+=l-a}}return n}(t.body.hiddenDates,t.range,o);return new Date(s+o+t.range.start)}}function Xo(t,e,i){let n=0;for(let o=0;o=e&&r=i.start&&r=r&&(o+=r-s)}return o}function Qo(t,e,i,n){const o=Jo(e,t);return 1==o.hidden?i<0?1==n?o.startDate-(o.endDate-e)-1:o.startDate-1:1==n?o.endDate+(e-o.startDate)+1:o.endDate+1:e}function Jo(t,e){for(let o=0;o=i&&t1e3&&(i=1e3),t.body.dom.rollingModeBtn.style.visibility="hidden",t.currentTimeTimer=setTimeout(e,i)}()}stopRolling(){void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),this.rolling=!1,this.body.dom.rollingModeBtn.style.visibility="visible")}setRange(t,e,i,n,o){i||(i={}),!0!==i.byUser&&(i.byUser=!1);const s=this,r=null!=t?Wo.convert(t,"Date").valueOf():null,a=null!=e?Wo.convert(e,"Date").valueOf():null;if(this._cancelAnimation(),this.millisecondsPerPixelCache=void 0,i.animation){const t=this.start,e=this.end,h="object"==typeof i.animation&&"duration"in i.animation?i.animation.duration:500,d="object"==typeof i.animation&&"easingFunction"in i.animation?i.animation.easingFunction:"easeInOutQuad",c=Wo.easingFunctions[d];if(!c)throw new Error(`Unknown easing function ${JSON.stringify(d)}. Choose from: ${Object.keys(Wo.easingFunctions).join(", ")}`);const u=Date.now();let p=!1;const m=()=>{if(!s.props.touch.dragging){const d=Date.now()-u,f=c(d/h),g=d>h,v=g||null===r?r:t+(r-t)*f,y=g||null===a?a:e+(a-e)*f;l=s._applyRange(v,y),Uo(s.options.moment,s.body,s.options.hiddenDates),p=p||l;const b={start:new Date(s.start),end:new Date(s.end),byUser:i.byUser,event:i.event};if(o&&o(f,l,g),l&&s.body.emitter.emit("rangechange",b),g){if(p&&(s.body.emitter.emit("rangechanged",b),n))return n()}else s.animationTimer=setTimeout(m,20)}};return m()}var l=this._applyRange(r,a);if(Uo(this.options.moment,this.body,this.options.hiddenDates),l){const t={start:new Date(this.start),end:new Date(this.end),byUser:i.byUser,event:i.event};if(this.body.emitter.emit("rangechange",t),clearTimeout(s.timeoutID),s.timeoutID=setTimeout((()=>{s.body.emitter.emit("rangechanged",t)}),200),n)return n()}}getMillisecondsPerPixel(){return void 0===this.millisecondsPerPixelCache&&(this.millisecondsPerPixelCache=(this.end-this.start)/this.body.dom.center.clientWidth),this.millisecondsPerPixelCache}_cancelAnimation(){this.animationTimer&&(clearTimeout(this.animationTimer),this.animationTimer=null)}_applyRange(t,e){let i=null!=t?Wo.convert(t,"Date").valueOf():this.start,n=null!=e?Wo.convert(e,"Date").valueOf():this.end;const o=null!=this.options.max?Wo.convert(this.options.max,"Date").valueOf():null,s=null!=this.options.min?Wo.convert(this.options.min,"Date").valueOf():null;let r;if(isNaN(i)||null===i)throw new Error(`Invalid start "${t}"`);if(isNaN(n)||null===n)throw new Error(`Invalid end "${e}"`);if(no&&(n=o)),null!==o&&n>o&&(r=n-o,i-=r,n-=r,null!=s&&i=this.start-e&&n<=this.end?(i=this.start,n=this.end):(r=t-(n-i),i-=r/2,n+=r/2)}}if(null!==this.options.zoomMax){let t=parseFloat(this.options.zoomMax);t<0&&(t=0),n-i>t&&(this.end-this.start===t&&ithis.end?(i=this.start,n=this.end):(r=n-i-t,i+=r/2,n-=r/2))}const a=this.start!=i||this.end!=n;return i>=this.start&&i<=this.end||n>=this.start&&n<=this.end||this.start>=i&&this.start<=n||this.end>=i&&this.end<=n||this.body.emitter.emit("checkRangedItems"),this.start=i,this.end=n,a}getRange(){return{start:this.start,end:this.end}}conversion(t,e){return ts.conversion(this.start,this.end,t,e)}static conversion(t,e,i,n){return void 0===n&&(n=0),0!=i&&e-t!=0?{offset:t,scale:i/(e-t-n)}:{offset:0,scale:1}}_onDragStart(t){this.deltaDifference=0,this.previousDelta=0,this.options.moveable&&this._isInsideRange(t)&&this.props.touch.allowDragging&&(this.stopRolling(),this.props.touch.start=this.start,this.props.touch.end=this.end,this.props.touch.dragging=!0,this.body.dom.root&&(this.body.dom.root.style.cursor="move"))}_onDrag(t){if(!t)return;if(!this.props.touch.dragging)return;if(!this.options.moveable)return;if(!this.props.touch.allowDragging)return;const e=this.options.direction;es(e);let i="horizontal"==e?t.deltaX:t.deltaY;i-=this.deltaDifference;let n=this.props.touch.end-this.props.touch.start;n-=Xo(this.body.hiddenDates,this.start,this.end);const o="horizontal"==e?this.body.domProps.center.width:this.body.domProps.center.height;let s;s=this.options.rtl?i/o*n:-i/o*n;const r=this.props.touch.start+s,a=this.props.touch.end+s,l=Qo(this.body.hiddenDates,r,this.previousDelta-i,!0),h=Qo(this.body.hiddenDates,a,this.previousDelta-i,!0);if(l!=r||h!=a)return this.deltaDifference+=i,this.props.touch.start=l,this.props.touch.end=h,void this._onDrag(t);this.previousDelta=i,this._applyRange(r,a);const d=new Date(this.start),c=new Date(this.end);this.body.emitter.emit("rangechange",{start:d,end:c,byUser:!0,event:t}),this.body.emitter.emit("panmove")}_onDragEnd(t){this.props.touch.dragging&&this.options.moveable&&this.props.touch.allowDragging&&(this.props.touch.dragging=!1,this.body.dom.root&&(this.body.dom.root.style.cursor="auto"),this.body.emitter.emit("rangechanged",{start:new Date(this.start),end:new Date(this.end),byUser:!0,event:t}))}_onMouseWheel(t){let e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail?e=-t.detail/3:t.deltaY&&(e=-t.deltaY/3),!(this.options.zoomKey&&!t[this.options.zoomKey]&&this.options.zoomable||!this.options.zoomable&&this.options.moveable)&&this.options.zoomable&&this.options.moveable&&this._isInsideRange(t)&&e){const i=this.options.zoomFriction||5;let n,o;if(n=e<0?1-e/i:1/(1+e/i),this.rolling){const t=this.options.rollingMode&&this.options.rollingMode.offset||.5;o=this.start+(this.end-this.start)*t}else{const e=this.getPointer({x:t.clientX,y:t.clientY},this.body.dom.center);o=this._pointerToDate(e)}this.zoom(n,o,e,t),t.preventDefault()}}_onTouch(t){this.props.touch.start=this.start,this.props.touch.end=this.end,this.props.touch.allowDragging=!0,this.props.touch.center=null,this.props.touch.centerDate=null,this.scaleOffset=0,this.deltaDifference=0,Wo.preventDefault(t)}_onPinch(t){if(!this.options.zoomable||!this.options.moveable)return;Wo.preventDefault(t),this.props.touch.allowDragging=!1,this.props.touch.center||(this.props.touch.center=this.getPointer(t.center,this.body.dom.center),this.props.touch.centerDate=this._pointerToDate(this.props.touch.center)),this.stopRolling();const e=1/(t.scale+this.scaleOffset),i=this.props.touch.centerDate,n=Xo(this.body.hiddenDates,this.start,this.end),o=Zo(this.options.moment,this.body.hiddenDates,this,i),s=n-o;let r=i-o+(this.props.touch.start-(i-o))*e,a=i+s+(this.props.touch.end-(i+s))*e;this.startToFront=1-e<=0,this.endToFront=e-1<=0;const l=Qo(this.body.hiddenDates,r,1-e,!0),h=Qo(this.body.hiddenDates,a,e-1,!0);l==r&&h==a||(this.props.touch.start=l,this.props.touch.end=h,this.scaleOffset=1-t.scale,r=l,a=h);const d={animation:!1,byUser:!0,event:t};this.setRange(r,a,d),this.startToFront=!1,this.endToFront=!0}_isInsideRange(t){const e=t.center?t.center.x:t.clientX,i=this.body.dom.centerContainer.getBoundingClientRect(),n=this.options.rtl?e-i.left:i.right-e,o=this.body.util.toTime(n);return o>=this.start&&o<=this.end}_pointerToDate(t){let e;const i=this.options.direction;if(es(i),"horizontal"==i)return this.body.util.toTime(t.x).valueOf();{const i=this.body.domProps.center.height;return e=this.conversion(i),t.y/e.scale+e.offset}}getPointer(t,e){const i=e.getBoundingClientRect();return this.options.rtl?{x:i.right-t.x,y:t.y-i.top}:{x:t.x-i.left,y:t.y-i.top}}zoom(t,e,i,n){null==e&&(e=(this.start+this.end)/2);const o=Xo(this.body.hiddenDates,this.start,this.end),s=Zo(this.options.moment,this.body.hiddenDates,this,e),r=o-s;let a=e-s+(this.start-(e-s))*t,l=e+r+(this.end-(e+r))*t;this.startToFront=!(i>0),this.endToFront=!(-i>0);const h=Qo(this.body.hiddenDates,a,i,!0),d=Qo(this.body.hiddenDates,l,-i,!0);h==a&&d==l||(a=h,l=d);const c={animation:!1,byUser:!0,event:n};this.setRange(a,l,c),this.startToFront=!1,this.endToFront=!0}move(t){const e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n}moveTo(t){const e=(this.start+this.end)/2-t,i=this.start-e,n=this.end-e;this.setRange(i,n,{animation:!1,byUser:!0,event:null})}}function es(t){if("horizontal"!=t&&"vertical"!=t)throw new TypeError(`Unknown direction "${t}". Choose "horizontal" or "vertical".`)}let is;if("undefined"!=typeof window){is=function t(e,i){var n=i||{preventDefault:!1};if(e.Manager){var o=e,s=function(e,i){var s=Object.create(n);return i&&o.assign(s,i),t(new o(e,s),s)};return o.assign(s,o),s.Manager=function(e,i){var s=Object.create(n);return i&&o.assign(s,i),t(new o.Manager(e,s),s)},s}var r=Object.create(e),a=e.element;function l(t){return t.match(/[^ ]+/g)}function h(t){if("hammer.input"!==t.type){if(t.srcEvent._handled||(t.srcEvent._handled={}),t.srcEvent._handled[t.type])return;t.srcEvent._handled[t.type]=!0}var e=!1;t.stopPropagation=function(){e=!0};var i=t.srcEvent.stopPropagation.bind(t.srcEvent);"function"==typeof i&&(t.srcEvent.stopPropagation=function(){i(),t.stopPropagation()}),t.firstTarget=Po;for(var n=Po;n&&!e;){var o=n.hammer;if(o)for(var s,r=0;r0?r._handlers[t]=n:(e.off(t,h),delete r._handlers[t]))})),r},r.emit=function(t,i){Po=i.target,e.emit(t,i)},r.destroy=function(){var t=e.element.hammer,i=t.indexOf(r);-1!==i&&t.splice(i,1),t.length||delete e.element.hammer,r._handlers={},e.destroy()},r}(window.Hammer||Qe,{preventDefault:"mouse"})}else is=()=>function(){const t=()=>{};return{on:t,off:t,destroy:t,emit:t,get:e=>({set:t})}}();var ns=is;function os(t,e){e.inputHandler=function(t){t.isFirst&&e(t)},t.on("hammer.input",e.inputHandler)}class ss{constructor(t,e,i,n,o){this.moment=o&&o.moment||No,this.options=o||{},this.current=this.moment(),this._start=this.moment(),this._end=this.moment(),this.autoScale=!0,this.scale="day",this.step=1,this.setRange(t,e,i),this.switchedDay=!1,this.switchedMonth=!1,this.switchedYear=!1,Array.isArray(n)?this.hiddenDates=n:this.hiddenDates=null!=n?[n]:[],this.format=ss.FORMAT}setMoment(t){this.moment=t,this.current=this.moment(this.current.valueOf()),this._start=this.moment(this._start.valueOf()),this._end=this.moment(this._end.valueOf())}setFormat(t){const e=Wo.deepExtend({},ss.FORMAT);this.format=Wo.deepExtend(e,t)}setRange(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=null!=t?this.moment(t.valueOf()):Date.now(),this._end=null!=e?this.moment(e.valueOf()):Date.now(),this.autoScale&&this.setMinimumStep(i)}start(){this.current=this._start.clone(),this.roundToMinor()}roundToMinor(){switch("week"==this.scale&&this.current.weekday(0),this.scale){case"year":this.current.year(this.step*Math.floor(this.current.year()/this.step)),this.current.month(0);case"month":this.current.date(1);case"week":case"day":case"weekday":this.current.hours(0);case"hour":this.current.minutes(0);case"minute":this.current.seconds(0);case"second":this.current.milliseconds(0)}if(1!=this.step){let t=this.current.clone();switch(this.scale){case"millisecond":this.current.subtract(this.current.milliseconds()%this.step,"milliseconds");break;case"second":this.current.subtract(this.current.seconds()%this.step,"seconds");break;case"minute":this.current.subtract(this.current.minutes()%this.step,"minutes");break;case"hour":this.current.subtract(this.current.hours()%this.step,"hours");break;case"weekday":case"day":this.current.subtract((this.current.date()-1)%this.step,"day");break;case"week":this.current.subtract(this.current.week()%this.step,"week");break;case"month":this.current.subtract(this.current.month()%this.step,"month");break;case"year":this.current.subtract(this.current.year()%this.step,"year")}t.isSame(this.current)||(this.current=this.moment(Qo(this.hiddenDates,this.current.valueOf(),-1,!0)))}}hasNext(){return this.current.valueOf()<=this._end.valueOf()}next(){const t=this.current.valueOf();switch(this.scale){case"millisecond":this.current.add(this.step,"millisecond");break;case"second":this.current.add(this.step,"second");break;case"minute":this.current.add(this.step,"minute");break;case"hour":this.current.add(this.step,"hour"),this.current.month()<6?this.current.subtract(this.current.hours()%this.step,"hour"):this.current.hours()%this.step!=0&&this.current.add(this.step-this.current.hours()%this.step,"hour");break;case"weekday":case"day":this.current.add(this.step,"day");break;case"week":if(0!==this.current.weekday())this.current.weekday(0),this.current.add(this.step,"week");else if(!1===this.options.showMajorLabels)this.current.add(this.step,"week");else{const t=this.current.clone();t.add(1,"week"),t.isSame(this.current,"month")?this.current.add(this.step,"week"):(this.current.add(this.step,"week"),this.current.date(1))}break;case"month":this.current.add(this.step,"month");break;case"year":this.current.add(this.step,"year")}if(1!=this.step)switch(this.scale){case"millisecond":this.current.milliseconds()>0&&this.current.milliseconds()0&&this.current.seconds()0&&this.current.minutes()0&&this.current.hours()=i&&o0?t.step:1,this.autoScale=!1)}setAutoScale(t){this.autoScale=t}setMinimumStep(t){if(null==t)return;const e=31104e6,i=2592e6,n=864e5,o=36e5,s=6e4,r=1e3;1e3*e>t&&(this.scale="year",this.step=1e3),500*e>t&&(this.scale="year",this.step=500),100*e>t&&(this.scale="year",this.step=100),50*e>t&&(this.scale="year",this.step=50),10*e>t&&(this.scale="year",this.step=10),5*e>t&&(this.scale="year",this.step=5),e>t&&(this.scale="year",this.step=1),7776e6>t&&(this.scale="month",this.step=3),i>t&&(this.scale="month",this.step=1),6048e5>t&&this.options.showWeekScale&&(this.scale="week",this.step=1),1728e5>t&&(this.scale="day",this.step=2),n>t&&(this.scale="day",this.step=1),432e5>t&&(this.scale="weekday",this.step=1),144e5>t&&(this.scale="hour",this.step=4),o>t&&(this.scale="hour",this.step=1),9e5>t&&(this.scale="minute",this.step=15),6e5>t&&(this.scale="minute",this.step=10),3e5>t&&(this.scale="minute",this.step=5),s>t&&(this.scale="minute",this.step=1),15e3>t&&(this.scale="second",this.step=15),1e4>t&&(this.scale="second",this.step=10),5e3>t&&(this.scale="second",this.step=5),r>t&&(this.scale="second",this.step=1),200>t&&(this.scale="millisecond",this.step=200),100>t&&(this.scale="millisecond",this.step=100),50>t&&(this.scale="millisecond",this.step=50),10>t&&(this.scale="millisecond",this.step=10),5>t&&(this.scale="millisecond",this.step=5),1>t&&(this.scale="millisecond",this.step=1)}static snap(t,e,i){const n=No(t);if("year"==e){const t=n.year()+Math.round(n.month()/12);n.year(Math.round(t/i)*i),n.month(0),n.date(0),n.hours(0),n.minutes(0),n.seconds(0),n.milliseconds(0)}else if("month"==e)n.date()>15?(n.date(1),n.add(1,"month")):n.date(1),n.hours(0),n.minutes(0),n.seconds(0),n.milliseconds(0);else if("week"==e)n.weekday()>2?(n.weekday(0),n.add(1,"week")):n.weekday(0),n.hours(0),n.minutes(0),n.seconds(0),n.milliseconds(0);else if("day"==e){switch(i){case 5:case 2:n.hours(24*Math.round(n.hours()/24));break;default:n.hours(12*Math.round(n.hours()/12))}n.minutes(0),n.seconds(0),n.milliseconds(0)}else if("weekday"==e){switch(i){case 5:case 2:n.hours(12*Math.round(n.hours()/12));break;default:n.hours(6*Math.round(n.hours()/6))}n.minutes(0),n.seconds(0),n.milliseconds(0)}else if("hour"==e){if(4===i)n.minutes(60*Math.round(n.minutes()/60));else n.minutes(30*Math.round(n.minutes()/30));n.seconds(0),n.milliseconds(0)}else if("minute"==e){switch(i){case 15:case 10:n.minutes(5*Math.round(n.minutes()/5)),n.seconds(0);break;case 5:n.seconds(60*Math.round(n.seconds()/60));break;default:n.seconds(30*Math.round(n.seconds()/30))}n.milliseconds(0)}else if("second"==e)switch(i){case 15:case 10:n.seconds(5*Math.round(n.seconds()/5)),n.milliseconds(0);break;case 5:n.milliseconds(1e3*Math.round(n.milliseconds()/1e3));break;default:n.milliseconds(500*Math.round(n.milliseconds()/500))}else if("millisecond"==e){const t=i>5?i/2:1;n.milliseconds(Math.round(n.milliseconds()/t)*t)}return n}isMajor(){if(1==this.switchedYear)switch(this.scale){case"year":case"month":case"week":case"weekday":case"day":case"hour":case"minute":case"second":case"millisecond":return!0;default:return!1}else if(1==this.switchedMonth)switch(this.scale){case"week":case"weekday":case"day":case"hour":case"minute":case"second":case"millisecond":return!0;default:return!1}else if(1==this.switchedDay)switch(this.scale){case"millisecond":case"second":case"minute":case"hour":return!0;default:return!1}const t=this.moment(this.current);switch(this.scale){case"millisecond":return 0==t.milliseconds();case"second":return 0==t.seconds();case"minute":return 0==t.hours()&&0==t.minutes();case"hour":return 0==t.hours();case"weekday":case"day":return this.options.showWeekScale?1==t.isoWeekday():1==t.date();case"week":return 1==t.date();case"month":return 0==t.month();default:return!1}}getLabelMinor(t){if(null==t&&(t=this.current),t instanceof Date&&(t=this.moment(t)),"function"==typeof this.format.minorLabels)return this.format.minorLabels(t,this.scale,this.step);const e=this.format.minorLabels[this.scale];return"week"===this.scale&&1===t.date()&&0!==t.weekday()?"":e&&e.length>0?this.moment(t).format(e):""}getLabelMajor(t){if(null==t&&(t=this.current),t instanceof Date&&(t=this.moment(t)),"function"==typeof this.format.majorLabels)return this.format.majorLabels(t,this.scale,this.step);const e=this.format.majorLabels[this.scale];return e&&e.length>0?this.moment(t).format(e):""}getClassName(){const t=this.moment,e=this.moment(this.current),i=e.locale?e.locale("en"):e.lang("en"),n=this.step,o=[];function s(t){return t/n%2==0?" vis-even":" vis-odd"}function r(e){return e.isSame(Date.now(),"day")?" vis-today":e.isSame(t().add(1,"day"),"day")?" vis-tomorrow":e.isSame(t().add(-1,"day"),"day")?" vis-yesterday":""}function a(t){return t.isSame(Date.now(),"week")?" vis-current-week":""}function l(t){return t.isSame(Date.now(),"month")?" vis-current-month":""}switch(this.scale){case"millisecond":o.push(r(i)),o.push(s(i.milliseconds()));break;case"second":o.push(r(i)),o.push(s(i.seconds()));break;case"minute":o.push(r(i)),o.push(s(i.minutes()));break;case"hour":o.push(`vis-h${i.hours()}${4==this.step?"-h"+(i.hours()+4):""}`),o.push(r(i)),o.push(s(i.hours()));break;case"weekday":o.push(`vis-${i.format("dddd").toLowerCase()}`),o.push(r(i)),o.push(a(i)),o.push(s(i.date()));break;case"day":o.push(`vis-day${i.date()}`),o.push(`vis-${i.format("MMMM").toLowerCase()}`),o.push(r(i)),o.push(l(i)),o.push(this.step<=2?r(i):""),o.push(this.step<=2?`vis-${i.format("dddd").toLowerCase()}`:""),o.push(s(i.date()-1));break;case"week":o.push(`vis-week${i.format("w")}`),o.push(a(i)),o.push(s(i.week()));break;case"month":o.push(`vis-${i.format("MMMM").toLowerCase()}`),o.push(l(i)),o.push(s(i.month()));break;case"year":o.push(`vis-year${i.year()}`),o.push(function(t){return t.isSame(Date.now(),"year")?" vis-current-year":""}(i)),o.push(s(i.year()))}return o.filter(String).join(" ")}}ss.FORMAT={minorLabels:{millisecond:"SSS",second:"s",minute:"HH:mm",hour:"HH:mm",weekday:"ddd D",day:"D",week:"w",month:"MMM",year:"YYYY"},majorLabels:{millisecond:"HH:mm:ss",second:"D MMMM HH:mm",minute:"ddd D MMMM",hour:"ddd D MMMM",weekday:"MMMM YYYY",day:"MMMM YYYY",week:"MMMM YYYY",month:"YYYY",year:""}};class rs extends Go{constructor(t,e){super(),this.dom={foreground:null,lines:[],majorTexts:[],minorTexts:[],redundant:{lines:[],majorTexts:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.defaultOptions={orientation:{axis:"bottom"},showMinorLabels:!0,showMajorLabels:!0,showWeekScale:!1,maxMinorChars:7,format:Wo.extend({},ss.FORMAT),moment:No,timeAxis:null},this.options=Wo.extend({},this.defaultOptions),this.body=t,this._create(),this.setOptions(e)}setOptions(t){t&&(Wo.selectiveExtend(["showMinorLabels","showMajorLabels","showWeekScale","maxMinorChars","hiddenDates","timeAxis","moment","rtl"],this.options,t),Wo.selectiveDeepExtend(["format"],this.options,t),"orientation"in t&&("string"==typeof t.orientation?this.options.orientation.axis=t.orientation:"object"==typeof t.orientation&&"axis"in t.orientation&&(this.options.orientation.axis=t.orientation.axis)),"locale"in t&&("function"==typeof No.locale?No.locale(t.locale):No.lang(t.locale)))}_create(){this.dom.foreground=document.createElement("div"),this.dom.background=document.createElement("div"),this.dom.foreground.className="vis-time-axis vis-foreground",this.dom.background.className="vis-time-axis vis-background"}destroy(){this.dom.foreground.parentNode&&this.dom.foreground.parentNode.removeChild(this.dom.foreground),this.dom.background.parentNode&&this.dom.background.parentNode.removeChild(this.dom.background),this.body=null}redraw(){const t=this.props,e=this.dom.foreground,i=this.dom.background,n="top"==this.options.orientation.axis?this.body.dom.top:this.body.dom.bottom,o=e.parentNode!==n;this._calculateCharSize();const s=this.options.showMinorLabels&&"none"!==this.options.orientation.axis,r=this.options.showMajorLabels&&"none"!==this.options.orientation.axis;t.minorLabelHeight=s?t.minorCharHeight:0,t.majorLabelHeight=r?t.majorCharHeight:0,t.height=t.minorLabelHeight+t.majorLabelHeight,t.width=e.offsetWidth,t.minorLineHeight=this.body.domProps.root.height-t.majorLabelHeight-("top"==this.options.orientation.axis?this.body.domProps.bottom.height:this.body.domProps.top.height),t.minorLineWidth=1,t.majorLineHeight=t.minorLineHeight+t.majorLabelHeight,t.majorLineWidth=1;const a=e.nextSibling,l=i.nextSibling;return e.parentNode&&e.parentNode.removeChild(e),i.parentNode&&i.parentNode.removeChild(i),e.style.height=`${this.props.height}px`,this._repaintLabels(),a?n.insertBefore(e,a):n.appendChild(e),l?this.body.dom.backgroundVertical.insertBefore(i,l):this.body.dom.backgroundVertical.appendChild(i),this._isResized()||o}_repaintLabels(){const t=this.options.orientation.axis,e=Wo.convert(this.body.range.start,"Number"),i=Wo.convert(this.body.range.end,"Number"),n=this.body.util.toTime((this.props.minorCharWidth||10)*this.options.maxMinorChars).valueOf();let o=n-Zo(this.options.moment,this.body.hiddenDates,this.body.range,n);o-=this.body.util.toTime(0).valueOf();const s=new ss(new Date(e),new Date(i),o,this.body.hiddenDates,this.options);s.setMoment(this.options.moment),this.options.format&&s.setFormat(this.options.format),this.options.timeAxis&&s.setScale(this.options.timeAxis),this.step=s;const r=this.dom;let a,l,h,d,c,u;r.redundant.lines=r.lines,r.redundant.majorTexts=r.majorTexts,r.redundant.minorTexts=r.minorTexts,r.lines=[],r.majorTexts=[],r.minorTexts=[];let p,m,f,g=0,v=0;const y=1e3;let b;for(s.start(),l=s.getCurrent(),d=this.body.util.toScreen(l);s.hasNext()&&v=.4*p;if(this.options.showMinorLabels&&u){var w=this._repaintMinorText(h,s.getLabelMinor(a),t,b);w.style.width=`${g}px`}c&&this.options.showMajorLabels?(h>0&&(null==f&&(f=h),w=this._repaintMajorText(h,s.getLabelMajor(a),t,b)),m=this._repaintMajorLine(h,g,t,b)):u?m=this._repaintMinorLine(h,g,t,b):m&&(m.style.width=`${parseInt(m.style.width)+g}px`)}if(v!==y||as||(console.warn("Something is wrong with the Timeline scale. Limited drawing of grid lines to 1000 lines."),as=!0),this.options.showMajorLabels){const e=this.body.util.toTime(0),i=s.getLabelMajor(e),n=i.length*(this.props.majorCharWidth||10)+10;(null==f||n{for(;t.length;){const e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}}))}_repaintMinorText(t,e,i,n){let o=this.dom.redundant.minorTexts.shift();if(!o){const t=document.createTextNode("");o=document.createElement("div"),o.appendChild(t),this.dom.foreground.appendChild(o)}this.dom.minorTexts.push(o),o.innerHTML=Wo.xss(e);let s="top"==i?this.props.majorLabelHeight:0;return this._setXY(o,t,s),o.className=`vis-text vis-minor ${n}`,o}_repaintMajorText(t,e,i,n){let o=this.dom.redundant.majorTexts.shift();if(!o){const t=document.createElement("div");o=document.createElement("div"),o.appendChild(t),this.dom.foreground.appendChild(o)}o.childNodes[0].innerHTML=Wo.xss(e),o.className=`vis-text vis-major ${n}`;let s="top"==i?0:this.props.minorLabelHeight;return this._setXY(o,t,s),this.dom.majorTexts.push(o),o}_setXY(t,e,i){const n=this.options.rtl?-1*e:e;t.style.transform=`translate(${n}px, ${i}px)`}_repaintMinorLine(t,e,i,n){let o=this.dom.redundant.lines.shift();o||(o=document.createElement("div"),this.dom.background.appendChild(o)),this.dom.lines.push(o);const s=this.props;o.style.width=`${e}px`,o.style.height=`${s.minorLineHeight}px`;let r="top"==i?s.majorLabelHeight:this.body.domProps.top.height,a=t-s.minorLineWidth/2;return this._setXY(o,a,r),o.className=`vis-grid ${this.options.rtl?"vis-vertical-rtl":"vis-vertical"} vis-minor ${n}`,o}_repaintMajorLine(t,e,i,n){let o=this.dom.redundant.lines.shift();o||(o=document.createElement("div"),this.dom.background.appendChild(o)),this.dom.lines.push(o);const s=this.props;o.style.width=`${e}px`,o.style.height=`${s.majorLineHeight}px`;let r="top"==i?0:this.body.domProps.top.height,a=t-s.majorLineWidth/2;return this._setXY(o,a,r),o.className=`vis-grid ${this.options.rtl?"vis-vertical-rtl":"vis-vertical"} vis-major ${n}`,o}_calculateCharSize(){this.dom.measureCharMinor||(this.dom.measureCharMinor=document.createElement("DIV"),this.dom.measureCharMinor.className="vis-text vis-minor vis-measure",this.dom.measureCharMinor.style.position="absolute",this.dom.measureCharMinor.appendChild(document.createTextNode("0")),this.dom.foreground.appendChild(this.dom.measureCharMinor)),this.props.minorCharHeight=this.dom.measureCharMinor.clientHeight,this.props.minorCharWidth=this.dom.measureCharMinor.clientWidth,this.dom.measureCharMajor||(this.dom.measureCharMajor=document.createElement("DIV"),this.dom.measureCharMajor.className="vis-text vis-major vis-measure",this.dom.measureCharMajor.style.position="absolute",this.dom.measureCharMajor.appendChild(document.createTextNode("0")),this.dom.foreground.appendChild(this.dom.measureCharMajor)),this.props.majorCharHeight=this.dom.measureCharMajor.clientHeight,this.props.majorCharWidth=this.dom.measureCharMajor.clientWidth}}var as=!1;function ls(t){this.active=!1,this.dom={container:t},this.dom.overlay=document.createElement("div"),this.dom.overlay.className="vis-overlay",this.dom.container.appendChild(this.dom.overlay),this.hammer=ns(this.dom.overlay),this.hammer.on("tap",this._onTapOverlay.bind(this));var e=this;["tap","doubletap","press","pinch","pan","panstart","panmove","panend"].forEach((function(t){e.hammer.on(t,(function(t){t.stopPropagation()}))})),document&&document.body&&(this.onClick=function(i){(function(t,e){for(;t;){if(t===e)return!0;t=t.parentNode}return!1})(i.target,t)||e.deactivate()},document.body.addEventListener("click",this.onClick)),void 0!==this.keycharm&&this.keycharm.destroy(),this.keycharm=function(t){var e,i=t&&t.preventDefault||!1,n=t&&t.container||window,o={},s={keydown:{},keyup:{}},r={};for(e=97;e<=122;e++)r[String.fromCharCode(e)]={code:e-97+65,shift:!1};for(e=65;e<=90;e++)r[String.fromCharCode(e)]={code:e,shift:!0};for(e=0;e<=9;e++)r[""+e]={code:48+e,shift:!1};for(e=1;e<=12;e++)r["F"+e]={code:111+e,shift:!1};for(e=0;e<=9;e++)r["num"+e]={code:96+e,shift:!1};r["num*"]={code:106,shift:!1},r["num+"]={code:107,shift:!1},r["num-"]={code:109,shift:!1},r["num/"]={code:111,shift:!1},r["num."]={code:110,shift:!1},r.left={code:37,shift:!1},r.up={code:38,shift:!1},r.right={code:39,shift:!1},r.down={code:40,shift:!1},r.space={code:32,shift:!1},r.enter={code:13,shift:!1},r.shift={code:16,shift:void 0},r.esc={code:27,shift:!1},r.backspace={code:8,shift:!1},r.tab={code:9,shift:!1},r.ctrl={code:17,shift:!1},r.alt={code:18,shift:!1},r.delete={code:46,shift:!1},r.pageup={code:33,shift:!1},r.pagedown={code:34,shift:!1},r["="]={code:187,shift:!1},r["-"]={code:189,shift:!1},r["]"]={code:221,shift:!1},r["["]={code:219,shift:!1};var a=function(t){h(t,"keydown")},l=function(t){h(t,"keyup")},h=function(t,e){if(void 0!==s[e][t.keyCode]){for(var n=s[e][t.keyCode],o=0;o{this.options.locales[t]=Wo.extend({},i,this.options.locales[t])})),e&&null!=e.time?this.customTime=e.time:this.customTime=new Date,this.eventParams={},this._create()}setOptions(t){t&&Wo.selectiveExtend(["moment","locale","locales","id","title","rtl","snap"],this.options,t)}_create(){const t=document.createElement("div");t["custom-time"]=this,t.className=`vis-custom-time ${this.options.id||""}`,t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t;const e=document.createElement("div");function i(t){this.body.range._onMouseWheel(t)}e.style.position="relative",e.style.top="0px",this.options.rtl?e.style.right="-10px":e.style.left="-10px",e.style.height="100%",e.style.width="20px",e.addEventListener?(e.addEventListener("mousewheel",i.bind(this),!1),e.addEventListener("DOMMouseScroll",i.bind(this),!1)):e.attachEvent("onmousewheel",i.bind(this)),t.appendChild(e),this.hammer=new ns(e),this.hammer.on("panstart",this._onDragStart.bind(this)),this.hammer.on("panmove",this._onDrag.bind(this)),this.hammer.on("panend",this._onDragEnd.bind(this)),this.hammer.get("pan").set({threshold:5,direction:ns.DIRECTION_ALL}),this.hammer.get("press").set({time:1e4})}destroy(){this.hide(),this.hammer.destroy(),this.hammer=null,this.body=null}redraw(){const t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar));const e=this.body.util.toScreen(this.customTime);let i=this.options.locales[this.options.locale];i||(this.warned||(console.warn(`WARNING: options.locales['${this.options.locale}'] not found. See https://visjs.github.io/vis-timeline/docs/timeline/#Localization`),this.warned=!0),i=this.options.locales.en);let n=this.options.title;return void 0===n?(n=`${i.time}: ${this.options.moment(this.customTime).format("dddd, MMMM Do YYYY, H:mm:ss")}`,n=n.charAt(0).toUpperCase()+n.substring(1)):"function"==typeof n&&(n=n.call(this,this.customTime)),this.options.rtl?this.bar.style.right=`${e}px`:this.bar.style.left=`${e}px`,this.bar.title=n,!1}hide(){this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar)}setCustomTime(t){this.customTime=Wo.convert(t,"Date"),this.redraw()}getCustomTime(){return new Date(this.customTime.valueOf())}setCustomMarker(t,e){const i=document.createElement("div");i.className="vis-custom-time-marker",i.innerHTML=Wo.xss(t),i.style.position="absolute",e&&(i.setAttribute("contenteditable","true"),i.addEventListener("pointerdown",(function(){i.focus()})),i.addEventListener("input",this._onMarkerChange.bind(this)),i.title=t,i.addEventListener("blur",function(t){this.title!=t.target.innerHTML&&(this._onMarkerChanged(t),this.title=t.target.innerHTML)}.bind(this))),this.bar.appendChild(i)}setCustomTitle(t){this.options.title=t}_onDragStart(t){this.eventParams.dragging=!0,this.eventParams.customTime=this.customTime,t.stopPropagation()}_onDrag(t){if(!this.eventParams.dragging)return;let e=this.options.rtl?-1*t.deltaX:t.deltaX;const i=this.body.util.toScreen(this.eventParams.customTime)+e,n=this.body.util.toTime(i),o=this.body.util.getScale(),s=this.body.util.getStep(),r=this.options.snap,a=r?r(n,o,s):n;this.setCustomTime(a),this.body.emitter.emit("timechange",{id:this.options.id,time:new Date(this.customTime.valueOf()),event:t}),t.stopPropagation()}_onDragEnd(t){this.eventParams.dragging&&(this.body.emitter.emit("timechanged",{id:this.options.id,time:new Date(this.customTime.valueOf()),event:t}),t.stopPropagation())}_onMarkerChange(t){this.body.emitter.emit("markerchange",{id:this.options.id,title:t.target.innerHTML,event:t}),t.stopPropagation()}_onMarkerChanged(t){this.body.emitter.emit("markerchanged",{id:this.options.id,title:t.target.innerHTML,event:t}),t.stopPropagation()}static customTimeFromTarget(t){let e=t.target;for(;e;){if(e.hasOwnProperty("custom-time"))return e["custom-time"];e=e.parentNode}return null}}class Cs{_create(t){this.dom={},this.dom.container=t,this.dom.container.style.position="relative",this.dom.root=document.createElement("div"),this.dom.background=document.createElement("div"),this.dom.backgroundVertical=document.createElement("div"),this.dom.backgroundHorizontal=document.createElement("div"),this.dom.centerContainer=document.createElement("div"),this.dom.leftContainer=document.createElement("div"),this.dom.rightContainer=document.createElement("div"),this.dom.center=document.createElement("div"),this.dom.left=document.createElement("div"),this.dom.right=document.createElement("div"),this.dom.top=document.createElement("div"),this.dom.bottom=document.createElement("div"),this.dom.shadowTop=document.createElement("div"),this.dom.shadowBottom=document.createElement("div"),this.dom.shadowTopLeft=document.createElement("div"),this.dom.shadowBottomLeft=document.createElement("div"),this.dom.shadowTopRight=document.createElement("div"),this.dom.shadowBottomRight=document.createElement("div"),this.dom.rollingModeBtn=document.createElement("div"),this.dom.loadingScreen=document.createElement("div"),this.dom.root.className="vis-timeline",this.dom.background.className="vis-panel vis-background",this.dom.backgroundVertical.className="vis-panel vis-background vis-vertical",this.dom.backgroundHorizontal.className="vis-panel vis-background vis-horizontal",this.dom.centerContainer.className="vis-panel vis-center",this.dom.leftContainer.className="vis-panel vis-left",this.dom.rightContainer.className="vis-panel vis-right",this.dom.top.className="vis-panel vis-top",this.dom.bottom.className="vis-panel vis-bottom",this.dom.left.className="vis-content",this.dom.center.className="vis-content",this.dom.right.className="vis-content",this.dom.shadowTop.className="vis-shadow vis-top",this.dom.shadowBottom.className="vis-shadow vis-bottom",this.dom.shadowTopLeft.className="vis-shadow vis-top",this.dom.shadowBottomLeft.className="vis-shadow vis-bottom",this.dom.shadowTopRight.className="vis-shadow vis-top",this.dom.shadowBottomRight.className="vis-shadow vis-bottom",this.dom.rollingModeBtn.className="vis-rolling-mode-btn",this.dom.loadingScreen.className="vis-loading-screen",this.dom.root.appendChild(this.dom.background),this.dom.root.appendChild(this.dom.backgroundVertical),this.dom.root.appendChild(this.dom.backgroundHorizontal),this.dom.root.appendChild(this.dom.centerContainer),this.dom.root.appendChild(this.dom.leftContainer),this.dom.root.appendChild(this.dom.rightContainer),this.dom.root.appendChild(this.dom.top),this.dom.root.appendChild(this.dom.bottom),this.dom.root.appendChild(this.dom.rollingModeBtn),this.dom.centerContainer.appendChild(this.dom.center),this.dom.leftContainer.appendChild(this.dom.left),this.dom.rightContainer.appendChild(this.dom.right),this.dom.centerContainer.appendChild(this.dom.shadowTop),this.dom.centerContainer.appendChild(this.dom.shadowBottom),this.dom.leftContainer.appendChild(this.dom.shadowTopLeft),this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft),this.dom.rightContainer.appendChild(this.dom.shadowTopRight),this.dom.rightContainer.appendChild(this.dom.shadowBottomRight),this.props={root:{},background:{},centerContainer:{},leftContainer:{},rightContainer:{},center:{},left:{},right:{},top:{},bottom:{},border:{},scrollTop:0,scrollTopMin:0},this.on("rangechange",(()=>{!0===this.initialDrawDone&&this._redraw()})),this.on("rangechanged",(()=>{this.initialRangeChangeDone||(this.initialRangeChangeDone=!0)})),this.on("touch",this._onTouch.bind(this)),this.on("panmove",this._onDrag.bind(this));const e=this;this._origRedraw=this._redraw.bind(this),this._redraw=Wo.throttle(this._origRedraw),this.on("_change",(t=>{e.itemSet&&e.itemSet.initialItemSetDrawn&&t&&1==t.queue?e._redraw():e._origRedraw()})),this.hammer=new ns(this.dom.root);const i=this.hammer.get("pinch").set({enable:!0});i&&function(t){t.getTouchAction=function(){return["pan-y"]}}(i),this.hammer.get("pan").set({threshold:5,direction:ns.DIRECTION_ALL}),this.timelineListeners={};var n,o;function s(t){this.isActive()&&this.emit("mousewheel",t);let e=0,i=0;if("detail"in t&&(i=-1*t.detail),"wheelDelta"in t&&(i=t.wheelDelta),"wheelDeltaY"in t&&(i=t.wheelDeltaY),"wheelDeltaX"in t&&(e=-1*t.wheelDeltaX),"axis"in t&&t.axis===t.HORIZONTAL_AXIS&&(e=-1*i,i=0),"deltaY"in t&&(i=-1*t.deltaY),"deltaX"in t&&(e=t.deltaX),t.deltaMode&&(1===t.deltaMode?(e*=40,i*=40):(e*=40,i*=800)),this.options.preferZoom){if(!this.options.zoomKey||t[this.options.zoomKey])return}else if(this.options.zoomKey&&t[this.options.zoomKey])return;if(this.options.verticalScroll||this.options.horizontalScroll)if(this.options.verticalScroll&&Math.abs(i)>=Math.abs(e)){const e=this.props.scrollTop,n=e+i;if(this.isActive()){this._setScrollTop(n)!==e&&(this._redraw(),this.emit("scroll",t),t.preventDefault())}}else if(this.options.horizontalScroll){const n=(Math.abs(e)>=Math.abs(i)?e:i)/120*(this.range.end-this.range.start)/20,o=this.range.start+n,s=this.range.end+n,r={animation:!1,byUser:!0,event:t};this.range.setRange(o,s,r),t.preventDefault()}}["tap","doubletap","press","pinch","pan","panstart","panmove","panend"].forEach((t=>{const i=i=>{e.isActive()&&e.emit(t,i)};e.hammer.on(t,i),e.timelineListeners[t]=i})),os(this.hammer,(t=>{e.emit("touch",t)})),n=this.hammer,(o=t=>{e.emit("release",t)}).inputHandler=function(t){t.isFinal&&o(t)},n.on("hammer.input",o.inputHandler);const r="onwheel"in document.createElement("div")?"wheel":void 0!==document.onmousewheel?"mousewheel":this.dom.centerContainer.addEventListener?"DOMMouseScroll":"onmousewheel";function a(t){if(e.options.verticalScroll&&(t.preventDefault(),e.isActive())){const i=-t.target.scrollTop;e._setScrollTop(i),e._redraw(),e.emit("scrollSide",t)}}this.dom.top.addEventListener,this.dom.bottom.addEventListener,this.dom.centerContainer.addEventListener(r,s.bind(this),!1),this.dom.top.addEventListener(r,s.bind(this),!1),this.dom.bottom.addEventListener(r,s.bind(this),!1),this.dom.left.parentNode.addEventListener("scroll",a.bind(this)),this.dom.right.parentNode.addEventListener("scroll",a.bind(this));let l=!1;if(this.dom.center.addEventListener("dragover",function(t){if(t.preventDefault&&(e.emit("dragover",e.getEventProperties(t)),t.preventDefault()),t.target.className.indexOf("timeline")>-1&&!l)return t.dataTransfer.dropEffect="move",l=!0,!1}.bind(this),!1),this.dom.center.addEventListener("drop",function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation();try{var i=JSON.parse(t.dataTransfer.getData("text"));if(!i||!i.content)return}catch(t){return!1}return l=!1,t.center={x:t.clientX,y:t.clientY},"item"!==i.target?e.itemSet._onAddItem(t):e.itemSet._onDropObjectOnItem(t),e.emit("drop",e.getEventProperties(t)),!1}.bind(this),!1),this.customTimes=[],this.touch={},this.redrawCount=0,this.initialDrawDone=!1,this.initialRangeChangeDone=!1,!t)throw new Error("No container provided");t.appendChild(this.dom.root),t.appendChild(this.dom.loadingScreen)}setOptions(t){if(t){const e=["width","height","minHeight","maxHeight","autoResize","start","end","clickToUse","dataAttributes","hiddenDates","locale","locales","moment","preferZoom","rtl","zoomKey","horizontalScroll","verticalScroll","longSelectPressTime","snap"];if(Wo.selectiveExtend(e,this.options,t),this.dom.rollingModeBtn.style.visibility="hidden",this.options.rtl&&(this.dom.container.style.direction="rtl",this.dom.backgroundVertical.className="vis-panel vis-background vis-vertical-rtl"),this.options.verticalScroll&&(this.options.rtl?this.dom.rightContainer.className="vis-panel vis-right vis-vertical-scroll":this.dom.leftContainer.className="vis-panel vis-left vis-vertical-scroll"),"object"!=typeof this.options.orientation&&(this.options.orientation={item:void 0,axis:void 0}),"orientation"in t&&("string"==typeof t.orientation?this.options.orientation={item:t.orientation,axis:t.orientation}:"object"==typeof t.orientation&&("item"in t.orientation&&(this.options.orientation.item=t.orientation.item),"axis"in t.orientation&&(this.options.orientation.axis=t.orientation.axis))),"both"===this.options.orientation.axis){if(!this.timeAxis2){const t=this.timeAxis2=new rs(this.body);t.setOptions=e=>{const i=e?Wo.extend({},e):{};i.orientation="top",rs.prototype.setOptions.call(t,i)},this.components.push(t)}}else if(this.timeAxis2){const t=this.components.indexOf(this.timeAxis2);-1!==t&&this.components.splice(t,1),this.timeAxis2.destroy(),this.timeAxis2=null}"function"==typeof t.drawPoints&&(t.drawPoints={onRender:t.drawPoints}),"hiddenDates"in this.options&&Vo(this.options.moment,this.body,this.options.hiddenDates),"clickToUse"in t&&(t.clickToUse?this.activator||(this.activator=new ls(this.dom.root)):this.activator&&(this.activator.destroy(),delete this.activator)),this._initAutoResize()}if(this.components.forEach((e=>e.setOptions(t))),"configure"in t){this.configurator||(this.configurator=this._createConfigurator()),this.configurator.setOptions(t.configure);const e=Wo.deepExtend({},this.options);this.components.forEach((t=>{Wo.deepExtend(e,t.options)})),this.configurator.setModuleOptions({global:e})}this._redraw()}isActive(){return!this.activator||this.activator.active}destroy(){this.setItems(null),this.setGroups(null),this.off(),this._stopAutoResize(),this.dom.root.parentNode&&this.dom.root.parentNode.removeChild(this.dom.root),this.dom=null,this.activator&&(this.activator.destroy(),delete this.activator);for(const t in this.timelineListeners)this.timelineListeners.hasOwnProperty(t)&&delete this.timelineListeners[t];this.timelineListeners=null,this.hammer&&this.hammer.destroy(),this.hammer=null,this.components.forEach((t=>t.destroy())),this.body=null}setCustomTime(t,e){const i=this.customTimes.filter((t=>e===t.options.id));if(0===i.length)throw new Error(`No custom time bar found with id ${JSON.stringify(e)}`);i.length>0&&i[0].setCustomTime(t)}getCustomTime(t){const e=this.customTimes.filter((e=>e.options.id===t));if(0===e.length)throw new Error(`No custom time bar found with id ${JSON.stringify(t)}`);return e[0].getCustomTime()}setCustomTimeMarker(t,e,i){const n=this.customTimes.filter((t=>t.options.id===e));if(0===n.length)throw new Error(`No custom time bar found with id ${JSON.stringify(e)}`);n.length>0&&n[0].setCustomMarker(t,i)}setCustomTimeTitle(t,e){const i=this.customTimes.filter((t=>t.options.id===e));if(0===i.length)throw new Error(`No custom time bar found with id ${JSON.stringify(e)}`);if(i.length>0)return i[0].setCustomTitle(t)}getEventProperties(t){return{event:t}}addCustomTime(t,e){const i=void 0!==t?Wo.convert(t,"Date"):new Date,n=this.customTimes.some((t=>t.options.id===e));if(n)throw new Error(`A custom time with id ${JSON.stringify(e)} already exists`);const o=new Ds(this.body,Wo.extend({},this.options,{time:i,id:e,snap:this.itemSet?this.itemSet.options.snap:this.options.snap}));return this.customTimes.push(o),this.components.push(o),this._redraw(),e}removeCustomTime(t){const e=this.customTimes.filter((e=>e.options.id===t));if(0===e.length)throw new Error(`No custom time bar found with id ${JSON.stringify(t)}`);e.forEach((t=>{this.customTimes.splice(this.customTimes.indexOf(t),1),this.components.splice(this.components.indexOf(t),1),t.destroy()}))}getVisibleItems(){return this.itemSet&&this.itemSet.getVisibleItems()||[]}getItemsAtCurrentTime(t){return this.time=t,this.itemSet&&this.itemSet.getItemsAtCurrentTime(this.time)||[]}getVisibleGroups(){return this.itemSet&&this.itemSet.getVisibleGroups()||[]}fit(t,e){const i=this.getDataRange();if(null===i.min&&null===i.max)return;const n=i.max-i.min,o=new Date(i.min.valueOf()-.01*n),s=new Date(i.max.valueOf()+.01*n),r=!t||void 0===t.animation||t.animation;this.range.setRange(o,s,{animation:r},e)}getDataRange(){throw new Error("Cannot invoke abstract method getDataRange")}setWindow(t,e,i,n){let o,s;"function"==typeof arguments[2]&&(n=arguments[2],i={}),1==arguments.length?(s=arguments[0],o=void 0===s.animation||s.animation,this.range.setRange(s.start,s.end,{animation:o})):2==arguments.length&&"function"==typeof arguments[1]?(s=arguments[0],n=arguments[1],o=void 0===s.animation||s.animation,this.range.setRange(s.start,s.end,{animation:o},n)):(o=!i||void 0===i.animation||i.animation,this.range.setRange(t,e,{animation:o},n))}moveTo(t,e,i){"function"==typeof arguments[1]&&(i=arguments[1],e={});const n=this.range.end-this.range.start,o=Wo.convert(t,"Date").valueOf(),s=o-n/2,r=o+n/2,a=!e||void 0===e.animation||e.animation;this.range.setRange(s,r,{animation:a},i)}getWindow(){const t=this.range.getRange();return{start:new Date(t.start),end:new Date(t.end)}}zoomIn(t,e,i){if(!t||t<0||t>1)return;"function"==typeof arguments[1]&&(i=arguments[1],e={});const n=this.getWindow(),o=n.start.valueOf(),s=n.end.valueOf(),r=s-o,a=(r-r/(1+t))/2,l=o+a,h=s-a;this.setWindow(l,h,e,i)}zoomOut(t,e,i){if(!t||t<0||t>1)return;"function"==typeof arguments[1]&&(i=arguments[1],e={});const n=this.getWindow(),o=n.start.valueOf(),s=n.end.valueOf(),r=s-o,a=o-r*t/2,l=s+r*t/2;this.setWindow(a,l,e,i)}redraw(){this._redraw()}_redraw(){this.redrawCount++;const t=this.dom;if(!t||!t.container||0==t.root.offsetWidth)return;let e=!1;const i=this.options,n=this.props;Uo(this.options.moment,this.body,this.options.hiddenDates),"top"==i.orientation?(Wo.addClassName(t.root,"vis-top"),Wo.removeClassName(t.root,"vis-bottom")):(Wo.removeClassName(t.root,"vis-top"),Wo.addClassName(t.root,"vis-bottom")),i.rtl?(Wo.addClassName(t.root,"vis-rtl"),Wo.removeClassName(t.root,"vis-ltr")):(Wo.addClassName(t.root,"vis-ltr"),Wo.removeClassName(t.root,"vis-rtl")),t.root.style.maxHeight=Wo.option.asSize(i.maxHeight,""),t.root.style.minHeight=Wo.option.asSize(i.minHeight,""),t.root.style.width=Wo.option.asSize(i.width,"");const o=t.root.offsetWidth;n.border.left=1,n.border.right=1,n.border.top=1,n.border.bottom=1,n.center.height=t.center.offsetHeight,n.left.height=t.left.offsetHeight,n.right.height=t.right.offsetHeight,n.top.height=t.top.clientHeight||-n.border.top,n.bottom.height=Math.round(t.bottom.getBoundingClientRect().height)||t.bottom.clientHeight||-n.border.bottom;const s=Math.max(n.left.height,n.center.height,n.right.height),r=n.top.height+s+n.bottom.height+n.border.top+n.border.bottom;t.root.style.height=Wo.option.asSize(i.height,`${r}px`),n.root.height=t.root.offsetHeight,n.background.height=n.root.height;const a=n.root.height-n.top.height-n.bottom.height;n.centerContainer.height=a,n.leftContainer.height=a,n.rightContainer.height=n.leftContainer.height,n.root.width=o,n.background.width=n.root.width,this.initialDrawDone||(n.scrollbarWidth=Wo.getScrollBarWidth());const l=t.leftContainer.clientWidth,h=t.rightContainer.clientWidth;i.verticalScroll?i.rtl?(n.left.width=l||-n.border.left,n.right.width=h+n.scrollbarWidth||-n.border.right):(n.left.width=l+n.scrollbarWidth||-n.border.left,n.right.width=h||-n.border.right):(n.left.width=l||-n.border.left,n.right.width=h||-n.border.right),this._setDOM();let d=this._updateScrollTop();"top"!=i.orientation.item&&(d+=Math.max(n.centerContainer.height-n.center.height-n.border.top-n.border.bottom,0)),t.center.style.transform=`translateY(${d}px)`;const c=0==n.scrollTop?"hidden":"",u=n.scrollTop==n.scrollTopMin?"hidden":"";t.shadowTop.style.visibility=c,t.shadowBottom.style.visibility=u,t.shadowTopLeft.style.visibility=c,t.shadowBottomLeft.style.visibility=u,t.shadowTopRight.style.visibility=c,t.shadowBottomRight.style.visibility=u,i.verticalScroll&&(t.rightContainer.className="vis-panel vis-right vis-vertical-scroll",t.leftContainer.className="vis-panel vis-left vis-vertical-scroll",t.shadowTopRight.style.visibility="hidden",t.shadowBottomRight.style.visibility="hidden",t.shadowTopLeft.style.visibility="hidden",t.shadowBottomLeft.style.visibility="hidden",t.left.style.top="0px",t.right.style.top="0px"),(!i.verticalScroll||n.center.heightn.centerContainer.height;this.hammer.get("pan").set({direction:p?ns.DIRECTION_ALL:ns.DIRECTION_HORIZONTAL}),this.hammer.get("press").set({time:this.options.longSelectPressTime}),this.components.forEach((t=>{e=t.redraw()||e}));if(e){if(this.redrawCount<5)return void this.body.emitter.emit("_change");console.log("WARNING: infinite loop in redraw?")}else this.redrawCount=0;this.body.emitter.emit("changed")}_setDOM(){const t=this.props,e=this.dom;t.leftContainer.width=t.left.width,t.rightContainer.width=t.right.width;const i=t.root.width-t.left.width-t.right.width;t.center.width=i,t.centerContainer.width=i,t.top.width=i,t.bottom.width=i,e.background.style.height=`${t.background.height}px`,e.backgroundVertical.style.height=`${t.background.height}px`,e.backgroundHorizontal.style.height=`${t.centerContainer.height}px`,e.centerContainer.style.height=`${t.centerContainer.height}px`,e.leftContainer.style.height=`${t.leftContainer.height}px`,e.rightContainer.style.height=`${t.rightContainer.height}px`,e.background.style.width=`${t.background.width}px`,e.backgroundVertical.style.width=`${t.centerContainer.width}px`,e.backgroundHorizontal.style.width=`${t.background.width}px`,e.centerContainer.style.width=`${t.center.width}px`,e.top.style.width=`${t.top.width}px`,e.bottom.style.width=`${t.bottom.width}px`,e.background.style.left="0",e.background.style.top="0",e.backgroundVertical.style.left=`${t.left.width+t.border.left}px`,e.backgroundVertical.style.top="0",e.backgroundHorizontal.style.left="0",e.backgroundHorizontal.style.top=`${t.top.height}px`,e.centerContainer.style.left=`${t.left.width}px`,e.centerContainer.style.top=`${t.top.height}px`,e.leftContainer.style.left="0",e.leftContainer.style.top=`${t.top.height}px`,e.rightContainer.style.left=`${t.left.width+t.center.width}px`,e.rightContainer.style.top=`${t.top.height}px`,e.top.style.left=`${t.left.width}px`,e.top.style.top="0",e.bottom.style.left=`${t.left.width}px`,e.bottom.style.top=`${t.top.height+t.centerContainer.height}px`,e.center.style.left="0",e.left.style.left="0",e.right.style.left="0"}setCurrentTime(t){if(!this.currentTime)throw new Error("Option showCurrentTime must be true");this.currentTime.setCurrentTime(t)}getCurrentTime(){if(!this.currentTime)throw new Error("Option showCurrentTime must be true");return this.currentTime.getCurrentTime()}_toTime(t){return qo(this,t,this.props.center.width)}_toGlobalTime(t){return qo(this,t,this.props.root.width)}_toScreen(t){return $o(this,t,this.props.center.width)}_toGlobalScreen(t){return $o(this,t,this.props.root.width)}_initAutoResize(){1==this.options.autoResize?this._startAutoResize():this._stopAutoResize()}_startAutoResize(){const t=this;this._stopAutoResize(),this._onResize=()=>{if(1==t.options.autoResize){if(t.dom.root){const e=t.dom.root.offsetHeight,i=t.dom.root.offsetWidth;i==t.props.lastWidth&&e==t.props.lastHeight||(t.props.lastWidth=i,t.props.lastHeight=e,t.props.scrollbarWidth=Wo.getScrollBarWidth(),t.body.emitter.emit("_change"))}}else t._stopAutoResize()},Wo.addEventListener(window,"resize",this._onResize),t.dom.root&&(t.props.lastWidth=t.dom.root.offsetWidth,t.props.lastHeight=t.dom.root.offsetHeight),this.watchTimer=setInterval(this._onResize,1e3)}_stopAutoResize(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0),this._onResize&&(Wo.removeEventListener(window,"resize",this._onResize),this._onResize=null)}_onTouch(t){this.touch.allowDragging=!0,this.touch.initialScrollTop=this.props.scrollTop}_onPinch(t){this.touch.allowDragging=!1}_onDrag(t){if(!t)return;if(!this.touch.allowDragging)return;const e=t.deltaY,i=this._getScrollTop(),n=this._setScrollTop(this.touch.initialScrollTop+e);this.options.verticalScroll&&(this.dom.left.parentNode.scrollTop=-this.props.scrollTop,this.dom.right.parentNode.scrollTop=-this.props.scrollTop),n!=i&&this.emit("verticalDrag")}_setScrollTop(t){return this.props.scrollTop=t,this._updateScrollTop(),this.props.scrollTop}_updateScrollTop(){const t=Math.min(this.props.centerContainer.height-this.props.border.top-this.props.border.bottom-this.props.center.height,0);return t!=this.props.scrollTopMin&&("top"!=this.options.orientation.item&&(this.props.scrollTop+=t-this.props.scrollTopMin),this.props.scrollTopMin=t),this.props.scrollTop>0&&(this.props.scrollTop=0),this.props.scrollTop{this.options.locales[t]=Wo.extend({},i,this.options.locales[t])})),this.offset=0,this._create()}_create(){const t=document.createElement("div");t.className="vis-current-time",t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t}destroy(){this.options.showCurrentTime=!1,this.redraw(),this.body=null}setOptions(t){t&&Wo.selectiveExtend(["rtl","showCurrentTime","alignCurrentTime","moment","locale","locales"],this.options,t)}redraw(){if(this.options.showCurrentTime){const t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar),this.start());let e=this.options.moment(Date.now()+this.offset);this.options.alignCurrentTime&&(e=e.startOf(this.options.alignCurrentTime));const i=this.body.util.toScreen(e);let n=this.options.locales[this.options.locale];n||(this.warned||(console.warn(`WARNING: options.locales['${this.options.locale}'] not found. See https://visjs.github.io/vis-timeline/docs/timeline/#Localization`),this.warned=!0),n=this.options.locales.en);let o=`${n.current} ${n.time}: ${e.format("dddd, MMMM Do YYYY, H:mm:ss")}`;o=o.charAt(0).toUpperCase()+o.substring(1),this.options.rtl?this.bar.style.transform=`translateX(${-1*i}px)`:this.bar.style.transform=`translateX(${i}px)`,this.bar.title=o}else this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),this.stop();return!1}start(){const t=this;!function e(){t.stop();let i=1/t.body.range.conversion(t.body.domProps.center.width).scale/10;i<30&&(i=30),i>1e3&&(i=1e3),t.redraw(),t.body.emitter.emit("currentTimeTick"),t.currentTimeTimer=setTimeout(e,i)}()}stop(){void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer)}setCurrentTime(t){const e=Wo.convert(t,"Date").valueOf(),i=Date.now();this.offset=e-i,this.redraw()}getCurrentTime(){return new Date(Date.now()+this.offset)}}const Ts=.001;function Es(t,e,i,n){return null===As(t,e.item,!1,(t=>t.stack&&(i||null===t.top)),(t=>t.stack),(t=>e.axis),n)}function Ms(t,e,i){const n=As(t,e.item,!1,(t=>t.stack),(t=>!0),(t=>t.baseTop));i.height=n-i.top+.5*e.item.vertical}function Os(t,e,i,n){for(let o=0;ot.index>e.index?1:t.index!0),(t=>!0),(t=>0));for(let n=0;ni[t].index&&(i[s].top+=i[t].height);const o=t[s];for(let t=0;tt.start,l=t=>t.end;if(!i){const i=!(!t[0]||!t[0].options.rtl);a=i?t=>t.right:t=>t.left,l=t=>a(t)+t.width+e.horizontal}const h=[],d=[];let c=null,u=0;for(const e of t)if(n(e))h.push(e);else if(o(e)){const t=a(e);null!==c&&ta(e)-Ts>t),u),d.splice(u,0,e),u++}c=null;let p=null;u=0;let m=0,f=0,g=0;for(;h.length>0;){const t=h.shift();t.top=s(t);const i=a(t),n=l(t);null!==c&&iinn&&(f=Ns(d,(t=>n+Ts>=a(t)),m,horizontalOVerlapEndIndex)+1);const w=d.slice(m,f).filter((t=>ia(t))).sort(((t,e)=>t.top-e.top));for(let i=0;iy.top&&(t.top=n.top+n.height+e.vertical)}o(t)&&(u=Ps(d,(t=>a(t)-Ts>i),u),d.splice(u,0,t),u++);const _=t.top+t.height;if(_>g&&(g=_),r&&r())return null}var v,y,b;return g}function Ps(t,e,i){i||(i=0);const n=t.slice(i).findIndex(e);return-1===n?t.length:n+i}function Ns(t,e,n,o){for(n||(n=0),o||(o=t.length),i=o-1;i>=n;i--)if(e(t[i]))return i;return n-1}const Fs="__background__";class Rs{constructor(t,e,i){if(this.groupId=t,this.subgroups={},this.subgroupStack={},this.subgroupStackAll=!1,this.subgroupVisibility={},this.doInnerStack=!1,this.shouldBailStackItems=!1,this.subgroupIndex=0,this.subgroupOrderer=e&&e.subgroupOrder,this.itemSet=i,this.isVisible=null,this.stackDirty=!0,this._disposeCallbacks=[],e&&e.nestedGroups&&(this.nestedGroups=e.nestedGroups,0==e.showNested?this.showNested=!1:this.showNested=!0),e&&e.subgroupStack)if("boolean"==typeof e.subgroupStack)this.doInnerStack=e.subgroupStack,this.subgroupStackAll=e.subgroupStack;else for(const t in e.subgroupStack)this.subgroupStack[t]=e.subgroupStack[t],this.doInnerStack=this.doInnerStack||e.subgroupStack[t];e&&e.heightMode?this.heightMode=e.heightMode:this.heightMode=i.options.groupHeightMode,this.nestedInGroup=null,this.dom={},this.props={label:{width:0,height:0}},this.className=null,this.items={},this.visibleItems=[],this.itemsInRange=[],this.orderedItems={byStart:[],byEnd:[]},this.checkRangedItems=!1;const n=()=>{this.checkRangedItems=!0};this.itemSet.body.emitter.on("checkRangedItems",n),this._disposeCallbacks.push((()=>{this.itemSet.body.emitter.off("checkRangedItems",n)})),this._create(),this.setData(e)}_create(){const t=document.createElement("div");this.itemSet.options.groupEditable.order?t.className="vis-label draggable":t.className="vis-label",this.dom.label=t;const e=document.createElement("div");e.className="vis-inner",t.appendChild(e),this.dom.inner=e;const i=document.createElement("div");i.className="vis-group",i["vis-group"]=this,this.dom.foreground=i,this.dom.background=document.createElement("div"),this.dom.background.className="vis-group",this.dom.axis=document.createElement("div"),this.dom.axis.className="vis-group",this.dom.marker=document.createElement("div"),this.dom.marker.style.visibility="hidden",this.dom.marker.style.position="absolute",this.dom.marker.innerHTML="",this.dom.background.appendChild(this.dom.marker)}setData(t){if(this.itemSet.groupTouchParams.isDragging)return;let e,i;if(t&&t.subgroupVisibility)for(const e in t.subgroupVisibility)this.subgroupVisibility[e]=t.subgroupVisibility[e];if(this.itemSet.options&&this.itemSet.options.groupTemplate?(i=this.itemSet.options.groupTemplate.bind(this),e=i(t,this.dom.inner)):e=t&&t.content,e instanceof Element){for(;this.dom.inner.firstChild;)this.dom.inner.removeChild(this.dom.inner.firstChild);this.dom.inner.appendChild(e)}else e instanceof Object&&e.isReactComponent||(e instanceof Object?i(t,this.dom.inner):this.dom.inner.innerHTML=null!=e?Wo.xss(e):Wo.xss(this.groupId||""));this.dom.label.title=t&&t.title||"",this.dom.inner.firstChild?Wo.removeClassName(this.dom.inner,"vis-hidden"):Wo.addClassName(this.dom.inner,"vis-hidden"),t&&t.nestedGroups?(this.nestedGroups&&this.nestedGroups==t.nestedGroups||(this.nestedGroups=t.nestedGroups),void 0===t.showNested&&void 0!==this.showNested||(0==t.showNested?this.showNested=!1:this.showNested=!0),Wo.addClassName(this.dom.label,"vis-nesting-group"),this.showNested?(Wo.removeClassName(this.dom.label,"collapsed"),Wo.addClassName(this.dom.label,"expanded")):(Wo.removeClassName(this.dom.label,"expanded"),Wo.addClassName(this.dom.label,"collapsed"))):this.nestedGroups&&(this.nestedGroups=null,Wo.removeClassName(this.dom.label,"collapsed"),Wo.removeClassName(this.dom.label,"expanded"),Wo.removeClassName(this.dom.label,"vis-nesting-group")),t&&(t.treeLevel||t.nestedInGroup)?(Wo.addClassName(this.dom.label,"vis-nested-group"),t.treeLevel?Wo.addClassName(this.dom.label,"vis-group-level-"+t.treeLevel):Wo.addClassName(this.dom.label,"vis-group-level-unknown-but-gte1")):Wo.addClassName(this.dom.label,"vis-group-level-0");const n=t&&t.className||null;n!=this.className&&(this.className&&(Wo.removeClassName(this.dom.label,this.className),Wo.removeClassName(this.dom.foreground,this.className),Wo.removeClassName(this.dom.background,this.className),Wo.removeClassName(this.dom.axis,this.className)),Wo.addClassName(this.dom.label,n),Wo.addClassName(this.dom.foreground,n),Wo.addClassName(this.dom.background,n),Wo.addClassName(this.dom.axis,n),this.className=n),this.style&&(Wo.removeCssText(this.dom.label,this.style),this.style=null),t&&t.style&&(Wo.addCssText(this.dom.label,t.style),this.style=t.style)}getLabelWidth(){return this.props.label.width}_didMarkerHeightChange(){const t=this.dom.marker.clientHeight;if(t!=this.lastMarkerHeight){this.lastMarkerHeight=t;const e={};let i=0;Wo.forEach(this.items,((t,n)=>{if(t.dirty=!0,t.displayed){const o=!0;e[n]=t.redraw(o),i=e[n].length}}));if(i>0)for(let t=0;t{e[t]()}));return!0}return!1}_calculateGroupSizeAndPosition(){const{offsetTop:t,offsetLeft:e,offsetWidth:i}=this.dom.foreground;this.top=t,this.right=e,this.width=i}_shouldBailItemsRedraw(){const t=this,e=this.itemSet.options.onTimeout,i={relativeBailingTime:this.itemSet.itemsSettingTime,bailTimeMs:e&&e.timeoutMs,userBailFunction:e&&e.callback,shouldBailStackItems:this.shouldBailStackItems};let n=null;if(!this.itemSet.initialDrawDone){if(i.shouldBailStackItems)return!0;Math.abs(Date.now()-new Date(i.relativeBailingTime))>i.bailTimeMs&&(i.userBailFunction&&null==this.itemSet.userContinueNotBail?i.userBailFunction((e=>{t.itemSet.userContinueNotBail=e,n=!e})):n=0==t.itemSet.userContinueNotBail)}return n}_redrawItems(t,e,i,n){if(t||this.stackDirty||this.isVisible&&!e){const t={byEnd:this.orderedItems.byEnd.filter((t=>!t.isCluster)),byStart:this.orderedItems.byStart.filter((t=>!t.isCluster))},e={byEnd:[...new Set(this.orderedItems.byEnd.map((t=>t.cluster)).filter((t=>!!t)))],byStart:[...new Set(this.orderedItems.byStart.map((t=>t.cluster)).filter((t=>!!t)))]},o=()=>[...this._updateItemsInRange(t,this.visibleItems.filter((t=>!t.isCluster)),n),...this._updateClustersInRange(e,this.visibleItems.filter((t=>t.isCluster)),n)],s=t=>{let e={};for(const i in this.subgroups){const n=this.visibleItems.filter((t=>t.data.subgroup===i));e[i]=t?n.sort(((e,i)=>t(e.data,i.data))):n}return e};if("function"==typeof this.itemSet.options.order){const t=this;if(this.doInnerStack&&this.itemSet.options.stackSubgroups){Is(s(this.itemSet.options.order),i,this.subgroups),this.visibleItems=o(),this._updateSubGroupHeights(i)}else{this.visibleItems=o(),this._updateSubGroupHeights(i);const e=this.visibleItems.slice().filter((t=>t.isCluster||!t.isCluster&&!t.cluster)).sort(((e,i)=>t.itemSet.options.order(e.data,i.data)));this.shouldBailStackItems=Es(e,i,!0,this._shouldBailItemsRedraw.bind(this))}}else if(this.visibleItems=o(),this._updateSubGroupHeights(i),this.itemSet.options.stack)if(this.doInnerStack&&this.itemSet.options.stackSubgroups){Is(s(),i,this.subgroups)}else this.shouldBailStackItems=Es(this.visibleItems,i,!0,this._shouldBailItemsRedraw.bind(this));else Os(this.visibleItems,i,this.subgroups,this.itemSet.options.stackSubgroups);for(let t=0;t{t.cluster&&t.displayed&&t.hide()})),this.shouldBailStackItems&&this.itemSet.body.emitter.emit("destroyTimeline"),this.stackDirty=!1}}_didResize(t,e){t=Wo.updateProperty(this,"height",e)||t;const i=this.dom.inner.clientWidth,n=this.dom.inner.clientHeight;return t=Wo.updateProperty(this.props.label,"width",i)||t,t=Wo.updateProperty(this.props.label,"height",n)||t}_applyGroupHeight(t){this.dom.background.style.height=`${t}px`,this.dom.foreground.style.height=`${t}px`,this.dom.label.style.height=`${t}px`}_updateItemsVerticalPosition(t){for(let e=0,i=this.visibleItems.length;e{i=this._didMarkerHeightChange.call(this)||i},this._updateSubGroupHeights.bind(this,e),this._calculateGroupSizeAndPosition.bind(this),()=>{this.isVisible=this._isGroupVisible.bind(this)(t,e)},()=>{this._redrawItems.bind(this)(i,s,e,t)},this._updateSubgroupsSizes.bind(this),()=>{r=this._calculateHeight.bind(this)(e)},this._calculateGroupSizeAndPosition.bind(this),()=>{o=this._didResize.bind(this)(o,r)},()=>{this._applyGroupHeight.bind(this)(r)},()=>{this._updateItemsVerticalPosition.bind(this)(e)},(()=>(!this.isVisible&&this.height&&(o=!1),o)).bind(this)];if(n)return a;{let t;return a.forEach((e=>{t=e()})),t}}_updateSubGroupHeights(t){if(Object.keys(this.subgroups).length>0){const e=this;this._resetSubgroups(),Wo.forEach(this.visibleItems,(i=>{void 0!==i.data.subgroup&&(e.subgroups[i.data.subgroup].height=Math.max(e.subgroups[i.data.subgroup].height,i.height+t.item.vertical),e.subgroups[i.data.subgroup].visible=void 0===this.subgroupVisibility[i.data.subgroup]||Boolean(this.subgroupVisibility[i.data.subgroup]))}))}}_isGroupVisible(t,e){return this.top<=t.body.domProps.centerContainer.height-t.body.domProps.scrollTop+e.axis&&this.top+this.height+e.axis>=-t.body.domProps.scrollTop}_calculateHeight(t){let e,i;if(i="fixed"===this.heightMode?Wo.toArray(this.items):this.visibleItems,i.length>0){let n=i[0].top,o=i[0].top+i[0].height;if(Wo.forEach(i,(t=>{n=Math.min(n,t.top),o=Math.max(o,t.top+t.height)})),n>t.axis){const e=n-t.axis;o-=e,Wo.forEach(i,(t=>{t.top-=e}))}e=Math.ceil(o+t.item.vertical/2),"fitItems"!==this.heightMode&&(e=Math.max(e,this.props.label.height))}else e=this.props.label.height;return e}show(){this.dom.label.parentNode||this.itemSet.dom.labelSet.appendChild(this.dom.label),this.dom.foreground.parentNode||this.itemSet.dom.foreground.appendChild(this.dom.foreground),this.dom.background.parentNode||this.itemSet.dom.background.appendChild(this.dom.background),this.dom.axis.parentNode||this.itemSet.dom.axis.appendChild(this.dom.axis)}hide(){const t=this.dom.label;t.parentNode&&t.parentNode.removeChild(t);const e=this.dom.foreground;e.parentNode&&e.parentNode.removeChild(e);const i=this.dom.background;i.parentNode&&i.parentNode.removeChild(i);const n=this.dom.axis;n.parentNode&&n.parentNode.removeChild(n)}add(t){if(this.items[t.id]=t,t.setParent(this),this.stackDirty=!0,void 0!==t.data.subgroup&&(this._addToSubgroup(t),this.orderSubgroups()),!this.visibleItems.includes(t)){const e=this.itemSet.body.range;this._checkIfVisible(t,this.visibleItems,e)}}_addToSubgroup(t,e=t.data.subgroup){null!=e&&void 0===this.subgroups[e]&&(this.subgroups[e]={height:0,top:0,start:t.data.start,end:t.data.end||t.data.start,visible:!1,index:this.subgroupIndex,items:[],stack:this.subgroupStackAll||this.subgroupStack[e]||!1},this.subgroupIndex++),new Date(t.data.start)new Date(this.subgroups[e].end)&&(this.subgroups[e].end=i),this.subgroups[e].items.push(t)}_updateSubgroupsSizes(){const t=this;if(t.subgroups)for(const e in t.subgroups){const i=t.subgroups[e].items[0].data.end||t.subgroups[e].items[0].data.start;let n=t.subgroups[e].items[0].data.start,o=i-1;t.subgroups[e].items.forEach((t=>{new Date(t.data.start)new Date(o)&&(o=e)})),t.subgroups[e].start=n,t.subgroups[e].end=new Date(o-1)}}orderSubgroups(){if(void 0!==this.subgroupOrderer){const t=[];if("string"==typeof this.subgroupOrderer){for(const e in this.subgroups)t.push({subgroup:e,sortField:this.subgroups[e].items[0].data[this.subgroupOrderer]});t.sort(((t,e)=>t.sortField-e.sortField))}else if("function"==typeof this.subgroupOrderer){for(const e in this.subgroups)t.push(this.subgroups[e].items[0].data);t.sort(this.subgroupOrderer)}if(t.length>0)for(let e=0;e=0&&(i.items.splice(n,1),i.items.length?this._updateSubgroupsSizes():delete this.subgroups[e])}}}removeFromDataSet(t){this.itemSet.removeItem(t.id)}order(){const t=Wo.toArray(this.items),e=[],i=[];for(let n=0;nt.data.start-e.data.start)),function(t){t.sort(((t,e)=>("end"in t.data?t.data.end:t.data.start)-("end"in e.data?e.data.end:e.data.start)))}(this.orderedItems.byEnd)}_updateItemsInRange(t,e,i){const n=[],o={};if(!this.isVisible&&this.groupId!=Fs){for(let t=0;t{const{start:e,end:i}=t;return i0)for(let t=0;ttt.data.startl)),1==this.checkRangedItems){this.checkRangedItems=!1;for(let e=0;et.data.endl))}const c={};let u=0;for(let t=0;t0)for(let t=0;t{e[t]()}));for(let t=0;t=0;s--){let t=e[s];if(o(t))break;t.isCluster&&!t.hasItems()||t.cluster||void 0===n[t.id]&&(n[t.id]=!0,i.push(t))}for(let s=t+1;s0)for(let t=0;t0)for(var a=0;a{this.options.locales[t]=Wo.extend({},n,this.options.locales[t])})),this.selected=!1,this.displayed=!1,this.groupShowing=!0,this.selectable=i&&i.selectable||!1,this.dirty=!0,this.top=null,this.right=null,this.left=null,this.width=null,this.height=null,this.setSelectability(t),this.editable=null,this._updateEditStatus()}select(){this.selectable&&(this.selected=!0,this.dirty=!0,this.displayed&&this.redraw())}unselect(){this.selected=!1,this.dirty=!0,this.displayed&&this.redraw()}setData(t){null!=t.group&&this.data.group!=t.group&&null!=this.parent&&this.parent.itemSet._moveToGroup(this,t.group),this.setSelectability(t),this.parent&&(this.parent.stackDirty=!0);null!=t.subgroup&&this.data.subgroup!=t.subgroup&&null!=this.parent&&this.parent.changeSubgroup(this,this.data.subgroup,t.subgroup),this.data=t,this._updateEditStatus(),this.dirty=!0,this.displayed&&this.redraw()}setSelectability(t){t&&(this.selectable=void 0===t.selectable||Boolean(t.selectable))}setParent(t){this.displayed?(this.hide(),this.parent=t,this.parent&&this.show()):this.parent=t}isVisible(t){return!1}show(){return!1}hide(){return!1}redraw(){}repositionX(){}repositionY(){}_repaintDragCenter(){if(this.selected&&this.editable.updateTime&&!this.dom.dragCenter){const t=this,e=document.createElement("div");e.className="vis-drag-center",e.dragCenterItem=this,this.hammerDragCenter=new ns(e),this.hammerDragCenter.on("tap",(e=>{t.parent.itemSet.body.emitter.emit("click",{event:e,item:t.id})})),this.hammerDragCenter.on("doubletap",(e=>{e.stopPropagation(),t.parent.itemSet._onUpdateItem(t),t.parent.itemSet.body.emitter.emit("doubleClick",{event:e,item:t.id})})),this.hammerDragCenter.on("panstart",(e=>{e.stopPropagation(),t.parent.itemSet._onDragStart(e)})),this.hammerDragCenter.on("panmove",t.parent.itemSet._onDrag.bind(t.parent.itemSet)),this.hammerDragCenter.on("panend",t.parent.itemSet._onDragEnd.bind(t.parent.itemSet)),this.hammerDragCenter.get("press").set({time:1e4}),this.dom.box?this.dom.dragLeft?this.dom.box.insertBefore(e,this.dom.dragLeft):this.dom.box.appendChild(e):this.dom.point&&this.dom.point.appendChild(e),this.dom.dragCenter=e}else!this.selected&&this.dom.dragCenter&&(this.dom.dragCenter.parentNode&&this.dom.dragCenter.parentNode.removeChild(this.dom.dragCenter),this.dom.dragCenter=null,this.hammerDragCenter&&(this.hammerDragCenter.destroy(),this.hammerDragCenter=null))}_repaintDeleteButton(t){const e=(this.options.editable.overrideItems||null==this.editable)&&this.options.editable.remove||!this.options.editable.overrideItems&&null!=this.editable&&this.editable.remove;if(this.selected&&e&&!this.dom.deleteButton){const e=this,i=document.createElement("div");this.options.rtl?i.className="vis-delete-rtl":i.className="vis-delete";let n=this.options.locales[this.options.locale];n||(this.warned||(console.warn(`WARNING: options.locales['${this.options.locale}'] not found. See https://visjs.github.io/vis-timeline/docs/timeline/#Localization`),this.warned=!0),n=this.options.locales.en),i.title=n.deleteSelected,this.hammerDeleteButton=new ns(i).on("tap",(t=>{t.stopPropagation(),e.parent.removeFromDataSet(e)})),t.appendChild(i),this.dom.deleteButton=i}else this.selected&&e||!this.dom.deleteButton||(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null,this.hammerDeleteButton&&(this.hammerDeleteButton.destroy(),this.hammerDeleteButton=null))}_repaintOnItemUpdateTimeTooltip(t){if(!this.options.tooltipOnItemUpdateTime)return;const e=(this.options.editable.updateTime||!0===this.data.editable)&&!1!==this.data.editable;if(this.selected&&e&&!this.dom.onItemUpdateTimeTooltip){const e=document.createElement("div");e.className="vis-onUpdateTime-tooltip",t.appendChild(e),this.dom.onItemUpdateTimeTooltip=e}else!this.selected&&this.dom.onItemUpdateTimeTooltip&&(this.dom.onItemUpdateTimeTooltip.parentNode&&this.dom.onItemUpdateTimeTooltip.parentNode.removeChild(this.dom.onItemUpdateTimeTooltip),this.dom.onItemUpdateTimeTooltip=null);if(this.dom.onItemUpdateTimeTooltip){this.dom.onItemUpdateTimeTooltip.style.visibility=this.parent.itemSet.touchParams.itemIsDragging?"visible":"hidden",this.dom.onItemUpdateTimeTooltip.style.transform="translateX(-50%)",this.dom.onItemUpdateTimeTooltip.style.left="50%";const t=50,e=this.parent.itemSet.body.domProps.scrollTop;let i;i="top"==this.options.orientation.item?this.top:this.parent.height-this.top-this.height;let n,o;i+this.parent.top-t<-e?(this.dom.onItemUpdateTimeTooltip.style.bottom="",this.dom.onItemUpdateTimeTooltip.style.top=`${this.height+2}px`):(this.dom.onItemUpdateTimeTooltip.style.top="",this.dom.onItemUpdateTimeTooltip.style.bottom=`${this.height+2}px`),this.options.tooltipOnItemUpdateTime&&this.options.tooltipOnItemUpdateTime.template?(o=this.options.tooltipOnItemUpdateTime.template.bind(this),n=o(this.data)):(n=`start: ${No(this.data.start).format("MM/DD/YYYY hh:mm")}`,this.data.end&&(n+=`
end: ${No(this.data.end).format("MM/DD/YYYY hh:mm")}`)),this.dom.onItemUpdateTimeTooltip.innerHTML=Wo.xss(n)}}_getItemData(){return this.parent.itemSet.itemsData.get(this.id)}_updateContents(t){let e,i,n,o,s;const r=this._getItemData(),a=(this.dom.box||this.dom.point).getElementsByClassName("vis-item-visible-frame")[0];if(this.options.visibleFrameTemplate?(s=this.options.visibleFrameTemplate.bind(this),o=Wo.xss(s(r,a))):o="",a)if(o instanceof Object&&!(o instanceof Element))s(r,a);else if(i=this._contentToString(this.itemVisibleFrameContent)!==this._contentToString(o),i){if(o instanceof Element)a.innerHTML="",a.appendChild(o);else if(null!=o)a.innerHTML=Wo.xss(o);else if("background"!=this.data.type||void 0!==this.data.content)throw new Error(`Property "content" missing in item ${this.id}`);this.itemVisibleFrameContent=o}if(this.options.template?(n=this.options.template.bind(this),e=n(r,t,this.data)):e=this.data.content,e instanceof Object&&!(e instanceof Element))n(r,t);else if(i=this._contentToString(this.content)!==this._contentToString(e),i){if(e instanceof Element)t.innerHTML="",t.appendChild(e);else if(null!=e)t.innerHTML=Wo.xss(e);else if("background"!=this.data.type||void 0!==this.data.content)throw new Error(`Property "content" missing in item ${this.id}`);this.content=e}}_updateDataAttributes(t){if(this.options.dataAttributes&&this.options.dataAttributes.length>0){let e=[];if(Array.isArray(this.options.dataAttributes))e=this.options.dataAttributes;else{if("all"!=this.options.dataAttributes)return;e=Object.keys(this.data)}for(const i of e){const e=this.data[i];null!=e?t.setAttribute(`data-${i}`,e):t.removeAttribute(`data-${i}`)}}}_updateStyle(t){this.style&&(Wo.removeCssText(t,this.style),this.style=null),this.data.style&&(Wo.addCssText(t,this.data.style),this.style=this.data.style)}_contentToString(t){return"string"==typeof t?t:t&&"outerHTML"in t?t.outerHTML:t}_updateEditStatus(){this.options&&("boolean"==typeof this.options.editable?this.editable={updateTime:this.options.editable,updateGroup:this.options.editable,remove:this.options.editable}:"object"==typeof this.options.editable&&(this.editable={},Wo.selectiveExtend(["updateTime","updateGroup","remove"],this.editable,this.options.editable))),this.options&&this.options.editable&&!0===this.options.editable.overrideItems||this.data&&("boolean"==typeof this.data.editable?this.editable={updateTime:this.data.editable,updateGroup:this.data.editable,remove:this.data.editable}:"object"==typeof this.data.editable&&(this.editable={},Wo.selectiveExtend(["updateTime","updateGroup","remove"],this.editable,this.data.editable)))}getWidthLeft(){return 0}getWidthRight(){return 0}getTitle(){if(this.options.tooltip&&this.options.tooltip.template){return this.options.tooltip.template.bind(this)(this._getItemData(),this.data)}return this.data.title}}js.prototype.stack=!0;class Ys extends js{constructor(t,e,i){if(super(t,e,i),this.props={content:{width:0}},this.overflow=!1,t){if(null==t.start)throw new Error(`Property "start" missing in item ${t.id}`);if(null==t.end)throw new Error(`Property "end" missing in item ${t.id}`)}}isVisible(t){return!this.cluster&&(this.data.startt.start)}_createDomElement(){this.dom||(this.dom={},this.dom.box=document.createElement("div"),this.dom.frame=document.createElement("div"),this.dom.frame.className="vis-item-overflow",this.dom.box.appendChild(this.dom.frame),this.dom.visibleFrame=document.createElement("div"),this.dom.visibleFrame.className="vis-item-visible-frame",this.dom.box.appendChild(this.dom.visibleFrame),this.dom.content=document.createElement("div"),this.dom.content.className="vis-item-content",this.dom.frame.appendChild(this.dom.content),this.dom.box["vis-item"]=this,this.dirty=!0)}_appendDomElement(){if(!this.parent)throw new Error("Cannot redraw item: no parent attached");if(!this.dom.box.parentNode){const t=this.parent.dom.foreground;if(!t)throw new Error("Cannot redraw item: parent has no foreground container element");t.appendChild(this.dom.box)}this.displayed=!0}_updateDirtyDomComponents(){if(this.dirty){this._updateContents(this.dom.content),this._updateDataAttributes(this.dom.box),this._updateStyle(this.dom.box);const t=this.editable.updateTime||this.editable.updateGroup,e=(this.data.className?" "+this.data.className:"")+(this.selected?" vis-selected":"")+(t?" vis-editable":" vis-readonly");this.dom.box.className=this.baseClassName+e,this.dom.content.style.maxWidth="none"}}_getDomComponentsSizes(){return this.overflow="hidden"!==window.getComputedStyle(this.dom.frame).overflow,this.whiteSpace="nowrap"!==window.getComputedStyle(this.dom.content).whiteSpace,{content:{width:this.dom.content.offsetWidth},box:{height:this.dom.box.offsetHeight}}}_updateDomComponentsSizes(t){this.props.content.width=t.content.width,this.height=t.box.height,this.dom.content.style.maxWidth="",this.dirty=!1}_repaintDomAdditionals(){this._repaintOnItemUpdateTimeTooltip(this.dom.box),this._repaintDeleteButton(this.dom.box),this._repaintDragCenter(),this._repaintDragLeft(),this._repaintDragRight()}redraw(t){let e;const i=[this._createDomElement.bind(this),this._appendDomElement.bind(this),this._updateDirtyDomComponents.bind(this),()=>{this.dirty&&(e=this._getDomComponentsSizes.bind(this)())},()=>{this.dirty&&this._updateDomComponentsSizes.bind(this)(e)},this._repaintDomAdditionals.bind(this)];if(t)return i;{let t;return i.forEach((e=>{t=e()})),t}}show(t){if(!this.displayed)return this.redraw(t)}hide(){if(this.displayed){const t=this.dom.box;t.parentNode&&t.parentNode.removeChild(t),this.displayed=!1}}repositionX(t){const e=this.parent.width;let i=this.conversion.toScreen(this.data.start),n=this.conversion.toScreen(this.data.end);const o=void 0===this.data.align?this.options.align:this.data.align;let s,r;!1===this.data.limitSize||void 0!==t&&!0!==t||(i<-e&&(i=-e),n>2*e&&(n=2*e));const a=Math.max(Math.round(1e3*(n-i))/1e3,1);switch(this.overflow?(this.options.rtl?this.right=i:this.left=i,this.width=a+this.props.content.width,r=this.props.content.width):(this.options.rtl?this.right=i:this.left=i,this.width=a,r=Math.min(n-i,this.props.content.width)),this.options.rtl?this.dom.box.style.transform=`translateX(${-1*this.right}px)`:this.dom.box.style.transform=`translateX(${this.left}px)`,this.dom.box.style.width=`${a}px`,this.whiteSpace&&(this.height=this.dom.box.offsetHeight),o){case"left":this.dom.content.style.transform="translateX(0)";break;case"right":if(this.options.rtl){const t=-1*Math.max(a-r,0);this.dom.content.style.transform=`translateX(${t}px)`}else this.dom.content.style.transform=`translateX(${Math.max(a-r,0)}px)`;break;case"center":if(this.options.rtl){const t=-1*Math.max((a-r)/2,0);this.dom.content.style.transform=`translateX(${t}px)`}else this.dom.content.style.transform=`translateX(${Math.max((a-r)/2,0)}px)`;break;default:if(s=this.overflow?n>0?Math.max(-i,0):-r:i<0?-i:0,this.options.rtl){const t=-1*s;this.dom.content.style.transform=`translateX(${t}px)`}else this.dom.content.style.transform=`translateX(${s}px)`}}repositionY(){const t=this.options.orientation.item,e=this.dom.box;e.style.top="top"==t?`${this.top}px`:this.parent.height-this.top-this.height+"px"}_repaintDragLeft(){if((this.selected||this.options.itemsAlwaysDraggable.range)&&this.editable.updateTime&&!this.dom.dragLeft){const t=document.createElement("div");t.className="vis-drag-left",t.dragLeftItem=this,this.dom.box.appendChild(t),this.dom.dragLeft=t}else this.selected||this.options.itemsAlwaysDraggable.range||!this.dom.dragLeft||(this.dom.dragLeft.parentNode&&this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft),this.dom.dragLeft=null)}_repaintDragRight(){if((this.selected||this.options.itemsAlwaysDraggable.range)&&this.editable.updateTime&&!this.dom.dragRight){const t=document.createElement("div");t.className="vis-drag-right",t.dragRightItem=this,this.dom.box.appendChild(t),this.dom.dragRight=t}else this.selected||this.options.itemsAlwaysDraggable.range||!this.dom.dragRight||(this.dom.dragRight.parentNode&&this.dom.dragRight.parentNode.removeChild(this.dom.dragRight),this.dom.dragRight=null)}}Ys.prototype.baseClassName="vis-item vis-range";class Hs extends js{constructor(t,e,i){if(super(t,e,i),this.props={content:{width:0}},this.overflow=!1,t){if(null==t.start)throw new Error(`Property "start" missing in item ${t.id}`);if(null==t.end)throw new Error(`Property "end" missing in item ${t.id}`)}}isVisible(t){return this.data.startt.start}_createDomElement(){this.dom||(this.dom={},this.dom.box=document.createElement("div"),this.dom.frame=document.createElement("div"),this.dom.frame.className="vis-item-overflow",this.dom.box.appendChild(this.dom.frame),this.dom.content=document.createElement("div"),this.dom.content.className="vis-item-content",this.dom.frame.appendChild(this.dom.content),this.dirty=!0)}_appendDomElement(){if(!this.parent)throw new Error("Cannot redraw item: no parent attached");if(!this.dom.box.parentNode){const t=this.parent.dom.background;if(!t)throw new Error("Cannot redraw item: parent has no background container element");t.appendChild(this.dom.box)}this.displayed=!0}_updateDirtyDomComponents(){if(this.dirty){this._updateContents(this.dom.content),this._updateDataAttributes(this.dom.content),this._updateStyle(this.dom.box);const t=(this.data.className?" "+this.data.className:"")+(this.selected?" vis-selected":"");this.dom.box.className=this.baseClassName+t}}_getDomComponentsSizes(){return this.overflow="hidden"!==window.getComputedStyle(this.dom.content).overflow,{content:{width:this.dom.content.offsetWidth}}}_updateDomComponentsSizes(t){this.props.content.width=t.content.width,this.height=0,this.dirty=!1}_repaintDomAdditionals(){}redraw(t){let e;const i=[this._createDomElement.bind(this),this._appendDomElement.bind(this),this._updateDirtyDomComponents.bind(this),()=>{this.dirty&&(e=this._getDomComponentsSizes.bind(this)())},()=>{this.dirty&&this._updateDomComponentsSizes.bind(this)(e)},this._repaintDomAdditionals.bind(this)];if(t)return i;{let t;return i.forEach((e=>{t=e()})),t}}repositionY(t){let e;const i=this.options.orientation.item;if(void 0!==this.data.subgroup){const t=this.data.subgroup;this.dom.box.style.height=`${this.parent.subgroups[t].height}px`,this.dom.box.style.top="top"==i?`${this.parent.top+this.parent.subgroups[t].top}px`:this.parent.top+this.parent.height-this.parent.subgroups[t].top-this.parent.subgroups[t].height+"px",this.dom.box.style.bottom=""}else this.parent instanceof Ls?(e=Math.max(this.parent.height,this.parent.itemSet.body.domProps.center.height,this.parent.itemSet.body.domProps.centerContainer.height),this.dom.box.style.bottom="bottom"==i?"0":"",this.dom.box.style.top="top"==i?"0":""):(e=this.parent.height,this.dom.box.style.top=`${this.parent.top}px`,this.dom.box.style.bottom="");this.dom.box.style.height=`${e}px`}}Hs.prototype.baseClassName="vis-item vis-background",Hs.prototype.stack=!1,Hs.prototype.show=Ys.prototype.show,Hs.prototype.hide=Ys.prototype.hide,Hs.prototype.repositionX=Ys.prototype.repositionX;class zs{constructor(t,e){this.container=t,this.overflowMethod=e||"cap",this.x=0,this.y=0,this.padding=5,this.hidden=!1,this.frame=document.createElement("div"),this.frame.className="vis-tooltip",this.container.appendChild(this.frame)}setPosition(t,e){this.x=parseInt(t),this.y=parseInt(e)}setText(t){t instanceof Element?(this.frame.innerHTML="",this.frame.appendChild(t)):this.frame.innerHTML=Wo.xss(t)}show(t){if(void 0===t&&(t=!0),!0===t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,o=this.frame.parentNode.clientWidth,s=0,r=0;if("flip"==this.overflowMethod||"none"==this.overflowMethod){let t=!1,n=!0;"flip"==this.overflowMethod&&(this.y-eo-this.padding&&(t=!0)),s=t?this.x-i:this.x,r=n?this.y-e:this.y}else(r=this.y-e)+e+this.padding>n&&(r=n-e-this.padding),ro&&(s=o-i-this.padding),st.start&&this.hasItems()}getData(){return{isCluster:!0,id:this.id,items:this.data.items||[],data:this.data}}redraw(t){var e,i,n=[this._createDomElement.bind(this),this._appendDomElement.bind(this),this._updateDirtyDomComponents.bind(this),function(){this.dirty&&(e=this._getDomComponentsSizes())}.bind(this),function(){this.dirty&&this._updateDomComponentsSizes.bind(this)(e)}.bind(this),this._repaintDomAdditionals.bind(this)];return t?n:(n.forEach((function(t){i=t()})),i)}show(){this.displayed||this.redraw()}hide(){if(this.displayed){var t=this.dom;t.box.parentNode&&t.box.parentNode.removeChild(t.box),this.options.showStipes&&(t.line.parentNode&&t.line.parentNode.removeChild(t.line),t.dot.parentNode&&t.dot.parentNode.removeChild(t.dot)),this.displayed=!1}}repositionX(){let t=this.conversion.toScreen(this.data.start),e=this.data.end?this.conversion.toScreen(this.data.end):0;if(e)this.repositionXWithRanges(t,e);else{let e=void 0===this.data.align?this.options.align:this.data.align;this.repositionXWithoutRanges(t,e)}this.options.showStipes&&(this.dom.line.style.display=this._isStipeVisible()?"block":"none",this.dom.dot.style.display=this._isStipeVisible()?"block":"none",this._isStipeVisible()&&this.repositionStype(t,e))}repositionStype(t,e){this.dom.line.style.display="block",this.dom.dot.style.display="block";const i=this.dom.line.offsetWidth,n=this.dom.dot.offsetWidth;if(e){const o=i+t+(e-t)/2,s=o-n/2,r=this.options.rtl?-1*o:o,a=this.options.rtl?-1*s:s;this.dom.line.style.transform=`translateX(${r}px)`,this.dom.dot.style.transform=`translateX(${a}px)`}else{const e=this.options.rtl?-1*t:t,i=this.options.rtl?-1*(t-n/2):t-n/2;this.dom.line.style.transform=`translateX(${e}px)`,this.dom.dot.style.transform=`translateX(${i}px)`}}repositionXWithoutRanges(t,e){"right"==e?this.options.rtl?(this.right=t-this.width,this.dom.box.style.right=this.right+"px"):(this.left=t-this.width,this.dom.box.style.left=this.left+"px"):"left"==e?this.options.rtl?(this.right=t,this.dom.box.style.right=this.right+"px"):(this.left=t,this.dom.box.style.left=this.left+"px"):this.options.rtl?(this.right=t-this.width/2,this.dom.box.style.right=this.right+"px"):(this.left=t-this.width/2,this.dom.box.style.left=this.left+"px")}repositionXWithRanges(t,e){let i=Math.round(Math.max(e-t+.5,1));this.options.rtl?this.right=t:this.left=t,this.width=Math.max(i,this.minWidth||0),this.options.rtl?this.dom.box.style.right=this.right+"px":this.dom.box.style.left=this.left+"px",this.dom.box.style.width=i+"px"}repositionY(){var t=this.options.orientation.item,e=this.dom.box;if(e.style.top="top"==t?(this.top||0)+"px":(this.parent.height-this.top-this.height||0)+"px",this.options.showStipes){if("top"==t)this.dom.line.style.top="0",this.dom.line.style.height=this.parent.top+this.top+1+"px",this.dom.line.style.bottom="";else{var i=this.parent.itemSet.props.height,n=i-this.parent.top-this.parent.height+this.top;this.dom.line.style.top=i-n+"px",this.dom.line.style.bottom="0"}this.dom.dot.style.top=-this.dom.dot.offsetHeight/2+"px"}}getWidthLeft(){return this.width/2}getWidthRight(){return this.width/2}move(){this.repositionX(),this.repositionY()}attach(){for(let t of this.data.uiItems)t.cluster=this;this.data.items=this.data.uiItems.map((t=>t.data)),this.attached=!0,this.dirty=!0}detach(t=!1){if(this.hasItems()){for(let t of this.data.uiItems)delete t.cluster;this.attached=!1,t&&this.group&&(this.group.remove(this),this.group=null),this.data.items=[],this.dirty=!0}}_onDoubleClick(){this._fit()}_setupRange(){const t=this.data.uiItems.map((t=>({start:t.data.start.valueOf(),end:t.data.end?t.data.end.valueOf():t.data.start.valueOf()})));this.data.min=Math.min(...t.map((t=>Math.min(t.start,t.end||t.start)))),this.data.max=Math.max(...t.map((t=>Math.max(t.start,t.end||t.start))));const e=this.data.uiItems.map((t=>t.center)).reduce(((t,e)=>t+e),0)/this.data.uiItems.length;this.data.uiItems.some((t=>t.data.end))?(this.data.start=new Date(this.data.min),this.data.end=new Date(this.data.max)):(this.data.start=new Date(e),this.data.end=null)}_getUiItems(){return this.data.uiItems&&this.data.uiItems.length?this.data.uiItems.filter((t=>t.cluster===this)):[]}_createDomElement(){this.dom||(this.dom={},this.dom.box=document.createElement("DIV"),this.dom.content=document.createElement("DIV"),this.dom.content.className="vis-item-content",this.dom.box.appendChild(this.dom.content),this.options.showStipes&&(this.dom.line=document.createElement("DIV"),this.dom.line.className="vis-cluster-line",this.dom.line.style.display="none",this.dom.dot=document.createElement("DIV"),this.dom.dot.className="vis-cluster-dot",this.dom.dot.style.display="none"),this.options.fitOnDoubleClick&&(this.dom.box.ondblclick=Bs.prototype._onDoubleClick.bind(this)),this.dom.box["vis-item"]=this,this.dirty=!0)}_appendDomElement(){if(!this.parent)throw new Error("Cannot redraw item: no parent attached");if(!this.dom.box.parentNode){const t=this.parent.dom.foreground;if(!t)throw new Error("Cannot redraw item: parent has no foreground container element");t.appendChild(this.dom.box)}const t=this.parent.dom.background;if(this.options.showStipes){if(!this.dom.line.parentNode){if(!t)throw new Error("Cannot redraw item: parent has no background container element");t.appendChild(this.dom.line)}if(!this.dom.dot.parentNode){var e=this.parent.dom.axis;if(!t)throw new Error("Cannot redraw item: parent has no axis container element");e.appendChild(this.dom.dot)}}this.displayed=!0}_updateDirtyDomComponents(){if(this.dirty){this._updateContents(this.dom.content),this._updateDataAttributes(this.dom.box),this._updateStyle(this.dom.box);const t=this.baseClassName+" "+(this.data.className?" "+this.data.className:"")+(this.selected?" vis-selected":"")+" vis-readonly";this.dom.box.className="vis-item "+t,this.options.showStipes&&(this.dom.line.className="vis-item vis-cluster-line "+(this.selected?" vis-selected":""),this.dom.dot.className="vis-item vis-cluster-dot "+(this.selected?" vis-selected":"")),this.data.end&&(this.dom.content.style.maxWidth="none")}}_getDomComponentsSizes(){const t={previous:{right:this.dom.box.style.right,left:this.dom.box.style.left},box:{width:this.dom.box.offsetWidth,height:this.dom.box.offsetHeight}};return this.options.showStipes&&(t.dot={height:this.dom.dot.offsetHeight,width:this.dom.dot.offsetWidth},t.line={width:this.dom.line.offsetWidth}),t}_updateDomComponentsSizes(t){this.options.rtl?this.dom.box.style.right="0px":this.dom.box.style.left="0px",this.data.end?this.minWidth=t.box.width:this.width=t.box.width,this.height=t.box.height,this.options.rtl?this.dom.box.style.right=t.previous.right:this.dom.box.style.left=t.previous.left,this.dirty=!1}_repaintDomAdditionals(){this._repaintOnItemUpdateTimeTooltip(this.dom.box)}_isStipeVisible(){return this.minWidth>=this.width||!this.data.end}_getFitRange(){const t=.05*(this.data.max-this.data.min)/2;return{fitStart:this.data.min-t,fitEnd:this.data.max+t}}_fit(){if(this.emitter){const{fitStart:t,fitEnd:e}=this._getFitRange(),i={start:new Date(t),end:new Date(e),animation:!0};this.emitter.emit("fit",i)}}_getItemData(){return this.data}}Bs.prototype.baseClassName="vis-item vis-range vis-cluster";const Ws="__ungrouped__";class Gs{constructor(t){this.itemSet=t,this.groups={},this.cache={},this.cache[-1]=[]}createClusterItem(t,e,i){return new Bs(t,e,i)}setItems(t,e){this.items=t||[],this.dataChanged=!0,this.applyOnChangedLevel=!1,e&&e.applyOnChangedLevel&&(this.applyOnChangedLevel=e.applyOnChangedLevel)}updateData(){this.dataChanged=!0,this.applyOnChangedLevel=!1}getClusters(t,e,i){let{maxItems:n,clusterCriteria:o}="boolean"==typeof i?{}:i;o||(o=()=>!0),n=n||1;let s=-1,r=0;if(e>0){if(e>=1)return[];s=Math.abs(Math.round(Math.log(100/e)/Math.log(2))),r=Math.abs(Math.pow(2,s))}if(this.dataChanged){const t=s!=this.cacheLevel;(!this.applyOnChangedLevel||t)&&(this._dropLevelsCache(),this._filterData())}this.cacheLevel=s;let a=this.cache[s];if(!a){a=[];for(let e in this.groups)if(this.groups.hasOwnProperty(e)){const s=this.groups[e],l=s.length;let h=0;for(;h=0&&e.center-s[d].center=0&&e.center-a[u].centern){const r=l-n+1,d=[];let c=h;for(;d.lengtht.center-e.center));this.dataChanged=!1}_getClusterForItems(t,e,i,n){const o=(i||[]).map((t=>({cluster:t,itemsIds:new Set(t.data.uiItems.map((t=>t.id)))})));let s;if(o.length)for(let e of o)if(e.itemsIds.size===t.length&&t.every((t=>e.itemsIds.has(t.id)))){s=e.cluster;break}if(s)return s.setUiItems(t),s.group!==e&&(s.group&&s.group.remove(s),e&&(e.add(s),s.group=e)),s;let r=n.titleTemplate||"";const a={toScreen:this.itemSet.body.util.toScreen,toTime:this.itemSet.body.util.toTime},l=r.replace(/{count}/,t.length),h='
'+t.length+"
",d=Object.assign({},n,this.itemSet.options),c={content:h,title:l,group:e,uiItems:t,eventEmitter:this.itemSet.body.emitter,range:this.itemSet.body.range};return s=this.createClusterItem(c,a,d),e&&(e.add(s),s.group=e),s.attach(),s}_dropLevelsCache(){this.cache={},this.cacheLevel=-1,this.cache[this.cacheLevel]=[]}}const Vs="__ungrouped__",Us="__background__";class $s extends Go{constructor(t,e){super(),this.body=t,this.defaultOptions={type:null,orientation:{item:"bottom"},align:"auto",stack:!0,stackSubgroups:!0,groupOrderSwap(t,e,i){const n=e.order;e.order=t.order,t.order=n},groupOrder:"order",selectable:!0,multiselect:!1,longSelectPressTime:251,itemsAlwaysDraggable:{item:!1,range:!1},editable:{updateTime:!1,updateGroup:!1,add:!1,remove:!1,overrideItems:!1},groupEditable:{order:!1,add:!1,remove:!1},snap:ss.snap,onDropObjectOnItem(t,e,i){i(e)},onAdd(t,e){e(t)},onUpdate(t,e){e(t)},onMove(t,e){e(t)},onRemove(t,e){e(t)},onMoving(t,e){e(t)},onAddGroup(t,e){e(t)},onMoveGroup(t,e){e(t)},onRemoveGroup(t,e){e(t)},margin:{item:{horizontal:10,vertical:10},axis:20},showTooltips:!0,tooltip:{followMouse:!1,overflowMethod:"flip",delay:500},tooltipOnItemUpdateTime:!1},this.options=Wo.extend({},this.defaultOptions),this.options.rtl=e.rtl,this.options.onTimeout=e.onTimeout,this.conversion={toScreen:t.util.toScreen,toTime:t.util.toTime},this.dom={},this.props={},this.hammer=null;const i=this;this.itemsData=null,this.groupsData=null,this.itemsSettingTime=null,this.initialItemSetDrawn=!1,this.userContinueNotBail=null,this.sequentialSelection=!1,this.itemListeners={add(t,e,n){i._onAdd(e.items),i.options.cluster&&i.clusterGenerator.setItems(i.items,{applyOnChangedLevel:!1}),i.redraw()},update(t,e,n){i._onUpdate(e.items),i.options.cluster&&i.clusterGenerator.setItems(i.items,{applyOnChangedLevel:!1}),i.redraw()},remove(t,e,n){i._onRemove(e.items),i.options.cluster&&i.clusterGenerator.setItems(i.items,{applyOnChangedLevel:!1}),i.redraw()}},this.groupListeners={add(t,e,n){if(i._onAddGroups(e.items),i.groupsData&&i.groupsData.length>0){const t=i.groupsData.getDataSet();t.get().forEach((e=>{if(e.nestedGroups){0!=e.showNested&&(e.showNested=!0);let i=[];e.nestedGroups.forEach((n=>{const o=t.get(n);o&&(o.nestedInGroup=e.id,0==e.showNested&&(o.visible=!1),i=i.concat(o))})),t.update(i,n)}}))}},update(t,e,n){i._onUpdateGroups(e.items)},remove(t,e,n){i._onRemoveGroups(e.items)}},this.items={},this.groups={},this.groupIds=[],this.selection=[],this.popup=null,this.popupTimer=null,this.touchParams={},this.groupTouchParams={group:null,isDragging:!1},this._create(),this.setOptions(e),this.clusters=[]}_create(){const t=document.createElement("div");t.className="vis-itemset",t["vis-itemset"]=this,this.dom.frame=t;const e=document.createElement("div");e.className="vis-background",t.appendChild(e),this.dom.background=e;const i=document.createElement("div");i.className="vis-foreground",t.appendChild(i),this.dom.foreground=i;const n=document.createElement("div");n.className="vis-axis",this.dom.axis=n;const o=document.createElement("div");o.className="vis-labelset",this.dom.labelSet=o,this._updateUngrouped();const s=new Ls(Us,null,this);s.show(),this.groups[Us]=s,this.hammer=new ns(this.body.dom.centerContainer),this.hammer.on("hammer.input",(t=>{t.isFirst&&this._onTouch(t)})),this.hammer.on("panstart",this._onDragStart.bind(this)),this.hammer.on("panmove",this._onDrag.bind(this)),this.hammer.on("panend",this._onDragEnd.bind(this)),this.hammer.get("pan").set({threshold:5,direction:ns.ALL}),this.hammer.get("press").set({time:1e4}),this.hammer.on("tap",this._onSelectItem.bind(this)),this.hammer.on("press",this._onMultiSelectItem.bind(this)),this.hammer.get("press").set({time:1e4}),this.hammer.on("doubletap",this._onAddItem.bind(this)),this.options.rtl?this.groupHammer=new ns(this.body.dom.rightContainer):this.groupHammer=new ns(this.body.dom.leftContainer),this.groupHammer.on("tap",this._onGroupClick.bind(this)),this.groupHammer.on("panstart",this._onGroupDragStart.bind(this)),this.groupHammer.on("panmove",this._onGroupDrag.bind(this)),this.groupHammer.on("panend",this._onGroupDragEnd.bind(this)),this.groupHammer.get("pan").set({threshold:5,direction:ns.DIRECTION_VERTICAL}),this.body.dom.centerContainer.addEventListener("mouseover",this._onMouseOver.bind(this)),this.body.dom.centerContainer.addEventListener("mouseout",this._onMouseOut.bind(this)),this.body.dom.centerContainer.addEventListener("mousemove",this._onMouseMove.bind(this)),this.body.dom.centerContainer.addEventListener("contextmenu",this._onDragEnd.bind(this)),this.body.dom.centerContainer.addEventListener("mousewheel",this._onMouseWheel.bind(this)),this.show()}setOptions(t){if(t){const e=["type","rtl","align","order","stack","stackSubgroups","selectable","multiselect","sequentialSelection","multiselectPerGroup","longSelectPressTime","groupOrder","dataAttributes","template","groupTemplate","visibleFrameTemplate","hide","snap","groupOrderSwap","showTooltips","tooltip","tooltipOnItemUpdateTime","groupHeightMode","onTimeout"];Wo.selectiveExtend(e,this.options,t),"itemsAlwaysDraggable"in t&&("boolean"==typeof t.itemsAlwaysDraggable?(this.options.itemsAlwaysDraggable.item=t.itemsAlwaysDraggable,this.options.itemsAlwaysDraggable.range=!1):"object"==typeof t.itemsAlwaysDraggable&&(Wo.selectiveExtend(["item","range"],this.options.itemsAlwaysDraggable,t.itemsAlwaysDraggable),this.options.itemsAlwaysDraggable.item||(this.options.itemsAlwaysDraggable.range=!1))),"sequentialSelection"in t&&"boolean"==typeof t.sequentialSelection&&(this.options.sequentialSelection=t.sequentialSelection),"orientation"in t&&("string"==typeof t.orientation?this.options.orientation.item="top"===t.orientation?"top":"bottom":"object"==typeof t.orientation&&"item"in t.orientation&&(this.options.orientation.item=t.orientation.item)),"margin"in t&&("number"==typeof t.margin?(this.options.margin.axis=t.margin,this.options.margin.item.horizontal=t.margin,this.options.margin.item.vertical=t.margin):"object"==typeof t.margin&&(Wo.selectiveExtend(["axis"],this.options.margin,t.margin),"item"in t.margin&&("number"==typeof t.margin.item?(this.options.margin.item.horizontal=t.margin.item,this.options.margin.item.vertical=t.margin.item):"object"==typeof t.margin.item&&Wo.selectiveExtend(["horizontal","vertical"],this.options.margin.item,t.margin.item)))),["locale","locales"].forEach((e=>{e in t&&(this.options[e]=t[e])})),"editable"in t&&("boolean"==typeof t.editable?(this.options.editable.updateTime=t.editable,this.options.editable.updateGroup=t.editable,this.options.editable.add=t.editable,this.options.editable.remove=t.editable,this.options.editable.overrideItems=!1):"object"==typeof t.editable&&Wo.selectiveExtend(["updateTime","updateGroup","add","remove","overrideItems"],this.options.editable,t.editable)),"groupEditable"in t&&("boolean"==typeof t.groupEditable?(this.options.groupEditable.order=t.groupEditable,this.options.groupEditable.add=t.groupEditable,this.options.groupEditable.remove=t.groupEditable):"object"==typeof t.groupEditable&&Wo.selectiveExtend(["order","add","remove"],this.options.groupEditable,t.groupEditable));["onDropObjectOnItem","onAdd","onUpdate","onRemove","onMove","onMoving","onAddGroup","onMoveGroup","onRemoveGroup"].forEach((e=>{const i=t[e];if(i){if("function"!=typeof i)throw new Error(`option ${e} must be a function ${e}(item, callback)`);this.options[e]=i}})),t.cluster?(Object.assign(this.options,{cluster:t.cluster}),this.clusterGenerator||(this.clusterGenerator=new Gs(this)),this.clusterGenerator.setItems(this.items,{applyOnChangedLevel:!1}),this.markDirty({refreshItems:!0,restackGroups:!0}),this.redraw()):this.clusterGenerator?(this._detachAllClusters(),this.clusters=[],this.clusterGenerator=null,this.options.cluster=void 0,this.markDirty({refreshItems:!0,restackGroups:!0}),this.redraw()):this.markDirty()}}markDirty(t){this.groupIds=[],t&&(t.refreshItems&&Wo.forEach(this.items,(t=>{t.dirty=!0,t.displayed&&t.redraw()})),t.restackGroups&&Wo.forEach(this.groups,((t,e)=>{e!==Us&&(t.stackDirty=!0)})))}destroy(){this.clearPopupTimer(),this.hide(),this.setItems(null),this.setGroups(null),this.hammer&&this.hammer.destroy(),this.groupHammer&&this.groupHammer.destroy(),this.hammer=null,this.body=null,this.conversion=null}hide(){this.dom.frame.parentNode&&this.dom.frame.parentNode.removeChild(this.dom.frame),this.dom.axis.parentNode&&this.dom.axis.parentNode.removeChild(this.dom.axis),this.dom.labelSet.parentNode&&this.dom.labelSet.parentNode.removeChild(this.dom.labelSet)}show(){this.dom.frame.parentNode||this.body.dom.center.appendChild(this.dom.frame),this.dom.axis.parentNode||this.body.dom.backgroundVertical.appendChild(this.dom.axis),this.dom.labelSet.parentNode||(this.options.rtl?this.body.dom.right.appendChild(this.dom.labelSet):this.body.dom.left.appendChild(this.dom.labelSet))}setPopupTimer(t){if(this.clearPopupTimer(),t){const e=this.options.tooltip.delay||"number"==typeof this.options.tooltip.delay?this.options.tooltip.delay:500;this.popupTimer=setTimeout((function(){t.show()}),e)}}clearPopupTimer(){null!=this.popupTimer&&(clearTimeout(this.popupTimer),this.popupTimer=null)}setSelection(t){null==t&&(t=[]),Array.isArray(t)||(t=[t]);const e=this.selection.filter((e=>-1===t.indexOf(e)));for(let t of e){const e=this.getItemById(t);e&&e.unselect()}this.selection=[...t];for(let e of t){const t=this.getItemById(e);t&&t.select()}}getSelection(){return this.selection.concat([])}getVisibleItems(){const t=this.body.range.getRange();let e,i;this.options.rtl?(e=this.body.util.toScreen(t.start),i=this.body.util.toScreen(t.end)):(i=this.body.util.toScreen(t.start),e=this.body.util.toScreen(t.end));const n=[];for(const t in this.groups)if(this.groups.hasOwnProperty(t)){const o=this.groups[t],s=o.isVisible?o.visibleItems:[];for(const t of s)this.options.rtl?t.righte&&n.push(t.id):t.lefti&&n.push(t.id)}return n}getItemsAtCurrentTime(t){let e,i;this.options.rtl?(e=this.body.util.toScreen(t),i=this.body.util.toScreen(t)):(i=this.body.util.toScreen(t),e=this.body.util.toScreen(t));const n=[];for(const t in this.groups)if(this.groups.hasOwnProperty(t)){const o=this.groups[t],s=o.isVisible?o.visibleItems:[];for(const t of s)this.options.rtl?t.righte&&n.push(t.id):t.lefti&&n.push(t.id)}return n}getVisibleGroups(){const t=[];for(const e in this.groups)if(this.groups.hasOwnProperty(e)){this.groups[e].isVisible&&t.push(e)}return t}getItemById(t){return this.items[t]||this.clusters.find((e=>e.id===t))}_deselect(t){const e=this.selection;for(let i=0,n=e.length;i{if(i===Us)return;const n=t==p?m:f;y[i]=t.redraw(e,n,u,!0),b=y[i].length}));if(b>0){const t={};for(let e=0;e{t[n]=i[e]()}));Wo.forEach(this.groups,((e,i)=>{if(i===Us)return;const n=t[i];s=n||s,g+=e.height})),g=Math.max(g,v)}return g=Math.max(g,v),r.style.height=i(g),this.props.width=r.offsetWidth,this.props.height=g,this.dom.axis.style.top=i("top"==o?this.body.domProps.top.height+this.body.domProps.border.top:this.body.domProps.top.height+this.body.domProps.centerContainer.height),this.options.rtl?this.dom.axis.style.right="0":this.dom.axis.style.left="0",this.hammer.get("press").set({time:this.options.longSelectPressTime}),this.initialItemSetDrawn=!0,s=this._isResized()||s,s}_firstGroup(){const t="top"==this.options.orientation.item?0:this.groupIds.length-1,e=this.groupIds[t];return this.groups[e]||this.groups[Vs]||null}_updateUngrouped(){let t,e,i=this.groups[Vs];if(this.groupsData){if(i)for(e in i.dispose(),delete this.groups[Vs],this.items)if(this.items.hasOwnProperty(e)){t=this.items[e],t.parent&&t.parent.remove(t);const i=this.getGroupId(t.data),n=this.groups[i];n&&n.add(t)||t.hide()}}else if(!i){const n=null,o=null;for(e in i=new Rs(n,o,this),this.groups[Vs]=i,this.items)this.items.hasOwnProperty(e)&&(t=this.items[e],i.add(t));i.show()}}getLabelSet(){return this.dom.labelSet}setItems(t){this.itemsSettingTime=new Date;const e=this;let i;const n=this.itemsData;if(t){if(!Fo(t))throw new TypeError("Data must implement the interface of DataSet or DataView");this.itemsData=Yo(t)}else this.itemsData=null;if(n&&(Wo.forEach(this.itemListeners,((t,e)=>{n.off(e,t)})),n.dispose(),i=n.getIds(),this._onRemove(i)),this.itemsData){const t=this.id;Wo.forEach(this.itemListeners,((i,n)=>{e.itemsData.on(n,i,t)})),i=this.itemsData.getIds(),this._onAdd(i),this._updateUngrouped()}this.body.emitter.emit("_change",{queue:!0})}getItems(){return null!=this.itemsData?this.itemsData.rawDS:null}setGroups(t){const e=this;let i;if(this.groupsData&&(Wo.forEach(this.groupListeners,((t,i)=>{e.groupsData.off(i,t)})),i=this.groupsData.getIds(),this.groupsData=null,this._onRemoveGroups(i)),t){if(!Fo(t))throw new TypeError("Data must implement the interface of DataSet or DataView");this.groupsData=t}else this.groupsData=null;if(this.groupsData){const t=this.groupsData.getDataSet();t.get().forEach((e=>{e.nestedGroups&&e.nestedGroups.forEach((i=>{const n=t.get(i);n.nestedInGroup=e.id,0==e.showNested&&(n.visible=!1),t.update(n)}))}));const n=this.id;Wo.forEach(this.groupListeners,((t,i)=>{e.groupsData.on(i,t,n)})),i=this.groupsData.getIds(),this._onAddGroups(i)}this._updateUngrouped(),this._order(),this.options.cluster&&(this.clusterGenerator.updateData(),this._clusterItems(),this.markDirty({refreshItems:!0,restackGroups:!0})),this.body.emitter.emit("_change",{queue:!0})}getGroups(){return this.groupsData}removeItem(t){const e=this.itemsData.get(t);e&&this.options.onRemove(e,(e=>{e&&this.itemsData.remove(t)}))}_getType(t){return t.type||this.options.type||(t.end?"range":"box")}getGroupId(t){return"background"==this._getType(t)&&null==t.group?Us:this.groupsData?t.group:Vs}_onUpdate(t){const e=this;t.forEach((t=>{const i=e.itemsData.get(t);let n=e.items[t];const o=i?e._getType(i):null,s=$s.types[o];let r;if(n&&(s&&n instanceof s?e._updateItem(n,i):(r=n.selected,e._removeItem(n),n=null)),!n&&i){if(!s)throw new TypeError(`Unknown item type "${o}"`);n=new s(i,e.conversion,e.options),n.id=t,e._addItem(n),r&&(this.selection.push(t),n.select())}})),this._order(),this.options.cluster&&(this.clusterGenerator.setItems(this.items,{applyOnChangedLevel:!1}),this._clusterItems()),this.body.emitter.emit("_change",{queue:!0})}_onRemove(t){let e=0;const i=this;t.forEach((t=>{const n=i.items[t];n&&(e++,i._removeItem(n))})),e&&(this._order(),this.body.emitter.emit("_change",{queue:!0}))}_order(){Wo.forEach(this.groups,(t=>{t.order()}))}_onUpdateGroups(t){this._onAddGroups(t)}_onAddGroups(t){const e=this;t.forEach((t=>{const i=e.groupsData.get(t);let n=e.groups[t];if(n)n.setData(i);else{if(t==Vs||t==Us)throw new Error(`Illegal group id. ${t} is a reserved id.`);const o=Object.create(e.options);Wo.extend(o,{height:null}),n=new Rs(t,i,e),e.groups[t]=n;for(const i in e.items)if(e.items.hasOwnProperty(i)){const o=e.items[i];o.data.group==t&&n.add(o)}n.order(),n.show()}})),this.body.emitter.emit("_change",{queue:!0})}_onRemoveGroups(t){t.forEach((t=>{const e=this.groups[t];e&&(e.dispose(),delete this.groups[t])})),this.options.cluster&&(this.clusterGenerator.updateData(),this._clusterItems()),this.markDirty({restackGroups:!!this.options.cluster}),this.body.emitter.emit("_change",{queue:!0})}_orderGroups(){if(this.groupsData){let t=this.groupsData.getIds({order:this.options.groupOrder});t=this._orderNestedGroups(t);const e=!Wo.equalArray(t,this.groupIds);if(e){const e=this.groups;t.forEach((t=>{e[t].hide()})),t.forEach((t=>{e[t].show()})),this.groupIds=t}return e}return!1}_orderNestedGroups(t){return function t(e,i){let n=[];return i.forEach((i=>{n.push(i);if(e.groupsData.get(i).nestedGroups){const o=e.groupsData.get({filter:t=>t.nestedInGroup==i,order:e.options.groupOrder}).map((t=>t.id));n=n.concat(t(e,o))}})),n}(this,t.filter((t=>!this.groupsData.get(t).nestedInGroup)))}_addItem(t){this.items[t.id]=t;const e=this.getGroupId(t.data),i=this.groups[e];i?i&&i.data&&i.data.showNested&&(t.groupShowing=!0):t.groupShowing=!1,i&&i.add(t)}_updateItem(t,e){t.setData(e);const i=this.getGroupId(t.data),n=this.groups[i];n?n&&n.data&&n.data.showNested&&(t.groupShowing=!0):t.groupShowing=!1}_removeItem(t){t.hide(),delete this.items[t.id];const e=this.selection.indexOf(t.id);-1!=e&&this.selection.splice(e,1),t.parent&&t.parent.remove(t),null!=this.popup&&this.popup.hide()}_constructByEndArray(t){const e=[];for(let i=0;i{const o=i.items[e],s=i._getGroupIndex(o.data.group);return{item:o,initialX:t.center.x,groupOffset:n-s,data:this._cloneItemData(o.data)}}))}t.stopPropagation()}else this.options.editable.add&&(t.srcEvent.ctrlKey||t.srcEvent.metaKey)&&this._onDragStartAddItem(t)}_onDragStartAddItem(t){const e=this.options.snap||null,i=this.dom.frame.getBoundingClientRect(),n=this.options.rtl?i.right-t.center.x+10:t.center.x-i.left-10,o=this.body.util.toTime(n),s=this.body.util.getScale(),r=this.body.util.getStep(),a=e?e(o,s,r):o,l={type:"range",start:a,end:a,content:"new item"},h=fn();l[this.itemsData.idProp]=h;const d=this.groupFromTarget(t);d&&(l.group=d.groupId);const c=new Ys(l,this.conversion,this.options);c.id=h,c.data=this._cloneItemData(l),this._addItem(c),this.touchParams.selectedItem=c;const u={item:c,initialX:t.center.x,data:c.data};this.options.rtl?u.dragLeft=!0:u.dragRight=!0,this.touchParams.itemProps=[u],t.stopPropagation()}_onDrag(t){if(null!=this.popup&&this.options.showTooltips&&!this.popup.hidden){const e=this.body.dom.centerContainer,i=e.getBoundingClientRect();this.popup.setPosition(t.center.x-i.left+e.offsetLeft,t.center.y-i.top+e.offsetTop),this.popup.show()}if(this.touchParams.itemProps){t.stopPropagation();const e=this,i=this.options.snap||null,n=this.body.dom.root.offsetLeft,o=this.options.rtl?n+this.body.domProps.right.width:n+this.body.domProps.left.width,s=this.body.util.getScale(),r=this.body.util.getStep(),a=this.touchParams.selectedItem,l=(this.options.editable.overrideItems||null==a.editable)&&this.options.editable.updateGroup||!this.options.editable.overrideItems&&null!=a.editable&&a.editable.updateGroup;let h=null;if(l&&a&&null!=a.data.group){const i=e.groupFromTarget(t);i&&(h=this._getGroupIndex(i.groupId))}this.touchParams.itemProps.forEach((n=>{const d=e.body.util.toTime(t.center.x-o),c=e.body.util.toTime(n.initialX-o);let u,p,m,f,g;u=this.options.rtl?-(d-c):d-c;let v=this._cloneItemData(n.item.data);if(null!=n.item.editable&&!n.item.editable.updateTime&&!n.item.editable.updateGroup&&!e.options.editable.overrideItems)return;if((this.options.editable.overrideItems||null==a.editable)&&this.options.editable.updateTime||!this.options.editable.overrideItems&&null!=a.editable&&a.editable.updateTime)if(n.dragLeft)this.options.rtl?null!=v.end&&(m=Wo.convert(n.data.end,"Date"),g=new Date(m.valueOf()+u),v.end=i?i(g,s,r):g):null!=v.start&&(p=Wo.convert(n.data.start,"Date"),f=new Date(p.valueOf()+u),v.start=i?i(f,s,r):f);else if(n.dragRight)this.options.rtl?null!=v.start&&(p=Wo.convert(n.data.start,"Date"),f=new Date(p.valueOf()+u),v.start=i?i(f,s,r):f):null!=v.end&&(m=Wo.convert(n.data.end,"Date"),g=new Date(m.valueOf()+u),v.end=i?i(g,s,r):g);else if(null!=v.start)if(p=Wo.convert(n.data.start,"Date").valueOf(),f=new Date(p+u),null!=v.end){m=Wo.convert(n.data.end,"Date");const t=m.valueOf()-p.valueOf();v.start=i?i(f,s,r):f,v.end=new Date(v.start.valueOf()+t)}else v.start=i?i(f,s,r):f;if(l&&!n.dragLeft&&!n.dragRight&&null!=h&&null!=v.group){let t=h-n.groupOffset;t=Math.max(0,t),t=Math.min(e.groupIds.length-1,t),v.group=e.groupIds[t]}v=this._cloneItemData(v),e.options.onMoving(v,(t=>{t&&n.item.setData(this._cloneItemData(t,"Date"))}))})),this.body.emitter.emit("_change")}}_moveToGroup(t,e){const i=this.groups[e];if(i&&i.groupId!=t.data.group){const e=t.parent;e.remove(t),e.order(),t.data.group=i.groupId,i.add(t),i.order()}}_onDragEnd(t){if(this.touchParams.itemIsDragging=!1,this.touchParams.itemProps){t.stopPropagation();const e=this,i=this.touchParams.itemProps;this.touchParams.itemProps=null,i.forEach((t=>{const i=t.item.id;if(null!=e.itemsData.get(i)){const n=this._cloneItemData(t.item.data);e.options.onMove(n,(n=>{n?(n[this.itemsData.idProp]=i,this.itemsData.update(n)):(t.item.setData(t.data),e.body.emitter.emit("_change"))}))}else e.options.onAdd(t.item.data,(i=>{e._removeItem(t.item),i&&e.itemsData.add(i),e.body.emitter.emit("_change")}))}))}}_onGroupClick(t){const e=this.groupFromTarget(t);setTimeout((()=>{this.toggleGroupShowNested(e)}),1)}toggleGroupShowNested(t,e){if(!t||!t.nestedGroups)return;const i=this.groupsData.getDataSet();t.showNested=null!=e?!!e:!t.showNested;let n=i.get(t.groupId);n.showNested=t.showNested;let o=t.nestedGroups,s=o;for(;s.length>0;){let t=s;s=[];for(let e=0;e0&&(o=o.concat(s))}let r=i.get(o).map((function(t){return null==t.visible&&(t.visible=!0),t.visible=!!n.showNested,t}));i.update(r.concat(n)),n.showNested?(Wo.removeClassName(t.dom.label,"collapsed"),Wo.addClassName(t.dom.label,"expanded")):(Wo.removeClassName(t.dom.label,"expanded"),Wo.addClassName(t.dom.label,"collapsed"))}toggleGroupDragClassName(t){t.dom.label.classList.toggle("vis-group-is-dragging"),t.dom.foreground.classList.toggle("vis-group-is-dragging")}_onGroupDragStart(t){this.groupTouchParams.isDragging||this.options.groupEditable.order&&(this.groupTouchParams.group=this.groupFromTarget(t),this.groupTouchParams.group&&(t.stopPropagation(),this.groupTouchParams.isDragging=!0,this.toggleGroupDragClassName(this.groupTouchParams.group),this.groupTouchParams.originalOrder=this.groupsData.getIds({order:this.options.groupOrder})))}_onGroupDrag(t){if(this.options.groupEditable.order&&this.groupTouchParams.group){t.stopPropagation();const e=this.groupsData.getDataSet(),i=this.groupFromTarget(t);if(i&&i.height!=this.groupTouchParams.group.height){const e=i.topn)return}}if(i&&i!=this.groupTouchParams.group){const t=e.get(i.groupId),n=e.get(this.groupTouchParams.group.groupId);n&&t&&(this.options.groupOrderSwap(n,t,e),e.update(n),e.update(t));const o=e.getIds({order:this.options.groupOrder});if(!Wo.equalArray(o,this.groupTouchParams.originalOrder)){const t=this.groupTouchParams.originalOrder,i=this.groupTouchParams.group.groupId,n=Math.min(t.length,o.length);let s=0,r=0,a=0;for(;s=n)break;if(o[s+r]==i)r=1;else if(t[s+a]==i)a=1;else{const i=o.indexOf(t[s+a]),n=e.get(o[s+r]),l=e.get(t[s+a]);this.options.groupOrderSwap(n,l,e),e.update(n),e.update(l);const h=o[s+r];o[s+r]=t[s+a],o[i]=h,s++}}}}}}_onGroupDragEnd(t){if(this.groupTouchParams.isDragging=!1,this.options.groupEditable.order&&this.groupTouchParams.group){t.stopPropagation();const e=this,i=e.groupTouchParams.group.groupId,n=e.groupsData.getDataSet(),o=Wo.extend({},n.get(i));e.options.onMoveGroup(o,(t=>{if(t)t[n._idProp]=i,n.update(t);else{const t=n.getIds({order:e.options.groupOrder});if(!Wo.equalArray(t,e.groupTouchParams.originalOrder)){const i=e.groupTouchParams.originalOrder,o=Math.min(i.length,t.length);let s=0;for(;s=o)break;const r=t.indexOf(i[s]),a=n.get(t[s]),l=n.get(i[s]);e.options.groupOrderSwap(a,l,n),n.update(a),n.update(l);const h=t[s];t[s]=i[s],t[r]=h,s++}}}})),e.body.emitter.emit("groupDragged",{groupId:i}),this.toggleGroupDragClassName(this.groupTouchParams.group),this.groupTouchParams.group=null}}_onSelectItem(t){if(!this.options.selectable)return;const e=t.srcEvent&&(t.srcEvent.ctrlKey||t.srcEvent.metaKey),i=t.srcEvent&&t.srcEvent.shiftKey;if(e||i)return void this._onMultiSelectItem(t);const n=this.getSelection(),o=this.itemFromTarget(t),s=o&&o.selectable?[o.id]:[];this.setSelection(s);const r=this.getSelection();(r.length>0||n.length>0)&&this.body.emitter.emit("select",{items:r,event:t})}_onMouseOver(t){const e=this.itemFromTarget(t);if(!e)return;if(e===this.itemFromRelatedTarget(t))return;const i=e.getTitle();if(this.options.showTooltips&&i){null==this.popup&&(this.popup=new zs(this.body.dom.root,this.options.tooltip.overflowMethod||"flip")),this.popup.setText(i);const e=this.body.dom.centerContainer,n=e.getBoundingClientRect();this.popup.setPosition(t.clientX-n.left+e.offsetLeft,t.clientY-n.top+e.offsetTop),this.setPopupTimer(this.popup)}else this.clearPopupTimer(),null!=this.popup&&this.popup.hide();this.body.emitter.emit("itemover",{item:e.id,event:t})}_onMouseOut(t){const e=this.itemFromTarget(t);if(!e)return;e!==this.itemFromRelatedTarget(t)&&(this.clearPopupTimer(),null!=this.popup&&this.popup.hide(),this.body.emitter.emit("itemout",{item:e.id,event:t}))}_onMouseMove(t){if(this.itemFromTarget(t)&&(null!=this.popupTimer&&this.setPopupTimer(this.popup),this.options.showTooltips&&this.options.tooltip.followMouse&&this.popup&&!this.popup.hidden)){const e=this.body.dom.centerContainer,i=e.getBoundingClientRect();this.popup.setPosition(t.clientX-i.left+e.offsetLeft,t.clientY-i.top+e.offsetTop),this.popup.show()}}_onMouseWheel(t){this.touchParams.itemIsDragging&&this._onDragEnd(t)}_onUpdateItem(t){if(!this.options.selectable)return;if(!this.options.editable.updateTime&&!this.options.editable.updateGroup)return;const e=this;if(t){const i=e.itemsData.get(t.id);this.options.onUpdate(i,(t=>{t&&e.itemsData.update(t)}))}}_onDropObjectOnItem(t){const e=this.itemFromTarget(t),i=JSON.parse(t.dataTransfer.getData("text"));this.options.onDropObjectOnItem(i,e)}_onAddItem(t){if(!this.options.selectable)return;if(!this.options.editable.add)return;const e=this,i=this.options.snap||null,n=this.dom.frame.getBoundingClientRect(),o=this.options.rtl?n.right-t.center.x:t.center.x-n.left,s=this.body.util.toTime(o),r=this.body.util.getScale(),a=this.body.util.getStep();let l,h;"drop"==t.type?(h=JSON.parse(t.dataTransfer.getData("text")),h.content=h.content?h.content:"new item",h.start=h.start?h.start:i?i(s,r,a):s,h.type=h.type||"box",h[this.itemsData.idProp]=h.id||fn(),"range"!=h.type||h.end||(l=this.body.util.toTime(o+this.props.width/5),h.end=i?i(l,r,a):l)):(h={start:i?i(s,r,a):s,content:"new item"},h[this.itemsData.idProp]=fn(),"range"===this.options.type&&(l=this.body.util.toTime(o+this.props.width/5),h.end=i?i(l,r,a):l));const d=this.groupFromTarget(t);d&&(h.group=d.groupId),h=this._cloneItemData(h),this.options.onAdd(h,(i=>{i&&(e.itemsData.add(i),"drop"==t.type&&e.setSelection([i.id]))}))}_onMultiSelectItem(t){if(!this.options.selectable)return;const e=this.itemFromTarget(t);if(e){let i=this.options.multiselect?this.getSelection():[];if((t.srcEvent&&t.srcEvent.shiftKey||!1||this.options.sequentialSelection)&&this.options.multiselect){const t=this.itemsData.get(e.id).group;let n;this.options.multiselectPerGroup&&i.length>0&&(n=this.itemsData.get(i[0]).group),this.options.multiselectPerGroup&&null!=n&&n!=t||i.push(e.id);const o=$s._getItemRange(this.itemsData.get(i));if(!this.options.multiselectPerGroup||n==t){i=[];for(const t in this.items)if(this.items.hasOwnProperty(t)){const e=this.items[t],s=e.data.start,r=void 0!==e.data.end?e.data.end:s;!(s>=o.min&&r<=o.max)||this.options.multiselectPerGroup&&n!=this.itemsData.get(e.id).group||e instanceof Hs||i.push(e.id)}}}else{const t=i.indexOf(e.id);-1==t?i.push(e.id):i.splice(t,1)}const n=i.filter((t=>this.getItemById(t).selectable));this.setSelection(n),this.body.emitter.emit("select",{items:this.getSelection(),event:t})}}static _getItemRange(t){let e=null,i=null;return t.forEach((t=>{(null==i||t.starte)&&(e=t.end):(null==e||t.start>e)&&(e=t.start)})),{min:i,max:e}}itemFromElement(t){let e=t;for(;e;){if(e.hasOwnProperty("vis-item"))return e["vis-item"];e=e.parentNode}return null}itemFromTarget(t){return this.itemFromElement(t.target)}itemFromRelatedTarget(t){return this.itemFromElement(t.relatedTarget)}groupFromTarget(t){const e=t.center?t.center.y:t.clientY;let i=this.groupIds;i.length<=0&&this.groupsData&&(i=this.groupsData.getIds({order:this.options.groupOrder}));for(let t=0;t=r.top&&er.top)return o}else if(0===t&&et.id))),i=this.clusters.filter((t=>!e.has(t.id)));let n=!1;for(let t of i){const e=this.selection.indexOf(t.id);-1!==e&&(t.unselect(),this.selection.splice(e,1),n=!0)}if(n){const t=this.getSelection();this.body.emitter.emit("select",{items:t,event:event})}}this.clusters=t||[]}}$s.types={background:Hs,box:class extends js{constructor(t,e,i){if(super(t,e,i),this.props={dot:{width:0,height:0},line:{width:0,height:0}},t&&null==t.start)throw new Error(`Property "start" missing in item ${t}`)}isVisible(t){if(this.cluster)return!1;let e;const i=this.data.align||this.options.align,n=this.width*t.getMillisecondsPerPixel();return e="right"==i?this.data.start.getTime()>t.start&&this.data.start.getTime()-nt.start&&this.data.start.getTime()t.start&&this.data.start.getTime()-n/2{this.dirty&&(e=this._getDomComponentsSizes())},()=>{this.dirty&&this._updateDomComponentsSizes.bind(this)(e)},this._repaintDomAdditionals.bind(this)];if(t)return i;{let t;return i.forEach((e=>{t=e()})),t}}show(t){if(!this.displayed)return this.redraw(t)}hide(){if(this.displayed){const t=this.dom;t.box.remove?t.box.remove():t.box.parentNode&&t.box.parentNode.removeChild(t.box),t.line.remove?t.line.remove():t.line.parentNode&&t.line.parentNode.removeChild(t.line),t.dot.remove?t.dot.remove():t.dot.parentNode&&t.dot.parentNode.removeChild(t.dot),this.displayed=!1}}repositionXY(){const t=this.options.rtl,e=(t,e,i,n=!1)=>{if(void 0===e&&void 0===i)return;const o=n?-1*e:e;t.style.transform=void 0!==i?void 0!==e?`translate(${o}px, ${i}px)`:`translateY(${i}px)`:`translateX(${o}px)`};e(this.dom.box,this.boxX,this.boxY,t),e(this.dom.dot,this.dotX,this.dotY,t),e(this.dom.line,this.lineX,this.lineY,t)}repositionX(){const t=this.conversion.toScreen(this.data.start),e=void 0===this.data.align?this.options.align:this.data.align,i=this.props.line.width,n=this.props.dot.width;"right"==e?(this.boxX=t-this.width,this.lineX=t-i,this.dotX=t-i/2-n/2):"left"==e?(this.boxX=t,this.lineX=t,this.dotX=t+i/2-n/2):(this.boxX=t-this.width/2,this.lineX=this.options.rtl?t-i:t-i/2,this.dotX=t-n/2),this.options.rtl?this.right=this.boxX:this.left=this.boxX,this.repositionXY()}repositionY(){const t=this.options.orientation.item,e=this.dom.line.style;if("top"==t){const t=this.parent.top+this.top+1;this.boxY=this.top||0,e.height=`${t}px`,e.bottom="",e.top="0"}else{const t=this.parent.itemSet.props.height-this.parent.top-this.parent.height+this.top;this.boxY=this.parent.height-this.top-(this.height||0),e.height=`${t}px`,e.top="",e.bottom="0"}this.dotY=-this.props.dot.height/2,this.repositionXY()}getWidthLeft(){return this.width/2}getWidthRight(){return this.width/2}},range:Ys,point:class extends js{constructor(t,e,i){if(super(t,e,i),this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0,marginRight:0}},t&&null==t.start)throw new Error(`Property "start" missing in item ${t}`)}isVisible(t){if(this.cluster)return!1;const e=this.width*t.getMillisecondsPerPixel();return this.data.start.getTime()+e>t.start&&this.data.start{this.dirty&&(e=this._getDomComponentsSizes())},()=>{this.dirty&&this._updateDomComponentsSizes.bind(this)(e)},this._repaintDomAdditionals.bind(this)];if(t)return i;{let t;return i.forEach((e=>{t=e()})),t}}repositionXY(){const t=this.options.rtl;((t,e,i,n=!1)=>{if(void 0===e&&void 0===i)return;const o=n?-1*e:e;t.style.transform=void 0!==i?void 0!==e?`translate(${o}px, ${i}px)`:`translateY(${i}px)`:`translateX(${o}px)`})(this.dom.point,this.pointX,this.pointY,t)}show(t){if(!this.displayed)return this.redraw(t)}hide(){this.displayed&&(this.dom.point.parentNode&&this.dom.point.parentNode.removeChild(this.dom.point),this.displayed=!1)}repositionX(){const t=this.conversion.toScreen(this.data.start);this.pointX=t,this.options.rtl?this.right=t-this.props.dot.width:this.left=t-this.props.dot.width,this.repositionXY()}repositionY(){const t=this.options.orientation.item;this.pointY="top"==t?this.top:this.parent.height-this.top-this.height,this.repositionXY()}getWidthLeft(){return this.props.dot.width}getWidthRight(){return this.props.dot.width}}},$s.prototype._onAdd=$s.prototype._onUpdate;let qs,Xs=!1,Ks="background: #FFeeee; color: #dd0000";class Zs{constructor(){}static validate(t,e,i){Xs=!1,qs=e;let n=e;return void 0!==i&&(n=e[i]),Zs.parse(t,n,[]),Xs}static parse(t,e,i){for(let n in t)t.hasOwnProperty(n)&&Zs.check(n,t,e,i)}static check(t,e,i,n){if(void 0===i[t]&&void 0===i.__any__)return void Zs.getSuggestion(t,i,n);let o=t,s=!0;void 0===i[t]&&void 0!==i.__any__&&(o="__any__",s="object"===Zs.getType(e[t]));let r=i[o];s&&void 0!==r.__type__&&(r=r.__type__),Zs.checkFields(t,e,i,o,r,n)}static checkFields(t,e,i,n,o,s){let r=function(e){console.log("%c"+e+Zs.printLocation(s,t),Ks)},a=Zs.getType(e[t]),l=o[a];void 0!==l?"array"===Zs.getType(l)&&-1===l.indexOf(e[t])?(r('Invalid option detected in "'+t+'". Allowed values are:'+Zs.print(l)+' not "'+e[t]+'". '),Xs=!0):"object"===a&&"__any__"!==n&&(s=Wo.copyAndExtendArray(s,t),Zs.parse(e[t],i[n],s)):void 0===o.any&&(r('Invalid type received for "'+t+'". Expected: '+Zs.print(Object.keys(o))+". Received ["+a+'] "'+e[t]+'"'),Xs=!0)}static getType(t){var e=typeof t;return"object"===e?null===t?"null":t instanceof Boolean?"boolean":t instanceof Number?"number":t instanceof String?"string":Array.isArray(t)?"array":t instanceof Date?"date":void 0!==t.nodeType?"dom":!0===t._isAMomentObject?"moment":"object":"number"===e?"number":"boolean"===e?"boolean":"string"===e?"string":void 0===e?"undefined":e}static getSuggestion(t,e,i){let n,o=Zs.findInOptions(t,e,i,!1),s=Zs.findInOptions(t,qs,[],!0);n=void 0!==o.indexMatch?" in "+Zs.printLocation(o.path,t,"")+'Perhaps it was incomplete? Did you mean: "'+o.indexMatch+'"?\n\n':s.distance<=4&&o.distance>s.distance?" in "+Zs.printLocation(o.path,t,"")+"Perhaps it was misplaced? Matching option found at: "+Zs.printLocation(s.path,s.closestMatch,""):o.distance<=8?'. Did you mean "'+o.closestMatch+'"?'+Zs.printLocation(o.path,t):". Did you mean one of these: "+Zs.print(Object.keys(e))+Zs.printLocation(i,t),console.log('%cUnknown option detected: "'+t+'"'+n,Ks),Xs=!0}static findInOptions(t,e,i,n=!1){let o,s=1e9,r="",a=[],l=t.toLowerCase();for(let h in e){let d;if(void 0!==e[h].__type__&&!0===n){let n=Zs.findInOptions(t,e[h],Wo.copyAndExtendArray(i,h));s>n.distance&&(r=n.closestMatch,a=n.path,s=n.distance,o=n.indexMatch)}else-1!==h.toLowerCase().indexOf(l)&&(o=h),d=Zs.levenshteinDistance(t,h),s>d&&(r=h,a=Wo.copyArray(i),s=d)}return{closestMatch:r,path:a,distance:s,indexMatch:o}}static printLocation(t,e,i="Problem value found at: \n"){let n="\n\n"+i+"options = {\n";for(let e=0;e{},this.closeCallback=()=>{},this._create()}insertTo(t){void 0!==this.hammer&&(this.hammer.destroy(),this.hammer=void 0),this.container=t,this.container.appendChild(this.frame),this._bindHammer(),this._setSize()}setUpdateCallback(t){if("function"!=typeof t)throw new Error("Function attempted to set as colorPicker update callback is not a function.");this.updateCallback=t}setCloseCallback(t){if("function"!=typeof t)throw new Error("Function attempted to set as colorPicker closing callback is not a function.");this.closeCallback=t}_isColorString(t){if("string"==typeof t)return ar[t]}setColor(t,e=!0){if("none"===t)return;let i;var n=this._isColorString(t);if(void 0!==n&&(t=n),!0===Wo.isString(t)){if(!0===Wo.isValidRGB(t)){let e=t.substr(4).substr(0,t.length-5).split(",");i={r:e[0],g:e[1],b:e[2],a:1}}else if(!0===Wo.isValidRGBA(t)){let e=t.substr(5).substr(0,t.length-6).split(",");i={r:e[0],g:e[1],b:e[2],a:e[3]}}else if(!0===Wo.isValidHex(t)){let e=Wo.hexToRGB(t);i={r:e.r,g:e.g,b:e.b,a:1}}}else if(t instanceof Object&&void 0!==t.r&&void 0!==t.g&&void 0!==t.b){let e=void 0!==t.a?t.a:"1.0";i={r:t.r,g:t.g,b:t.b,a:e}}if(void 0===i)throw new Error("Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: "+JSON.stringify(t));this._setColor(i,e)}show(){void 0!==this.closeCallback&&(this.closeCallback(),this.closeCallback=void 0),this.applied=!1,this.frame.style.display="block",this._generateHueCircle()}_hide(t=!0){!0===t&&(this.previousColor=Wo.extend({},this.color)),!0===this.applied&&this.updateCallback(this.initialColor),this.frame.style.display="none",setTimeout((()=>{void 0!==this.closeCallback&&(this.closeCallback(),this.closeCallback=void 0)}),0)}_save(){this.updateCallback(this.color),this.applied=!1,this._hide()}_apply(){this.applied=!0,this.updateCallback(this.color),this._updatePicker(this.color)}_loadLast(){void 0!==this.previousColor?this.setColor(this.previousColor,!1):alert("There is no last color to load...")}_setColor(t,e=!0){!0===e&&(this.initialColor=Wo.extend({},t)),this.color=t;let i=Wo.RGBToHSV(t.r,t.g,t.b),n=2*Math.PI,o=this.r*i.s,s=this.centerCoordinates.x+o*Math.sin(n*i.h),r=this.centerCoordinates.y+o*Math.cos(n*i.h);this.colorPickerSelector.style.left=s-.5*this.colorPickerSelector.clientWidth+"px",this.colorPickerSelector.style.top=r-.5*this.colorPickerSelector.clientHeight+"px",this._updatePicker(t)}_setOpacity(t){this.color.a=t/100,this._updatePicker(this.color)}_setBrightness(t){let e=Wo.RGBToHSV(this.color.r,this.color.g,this.color.b);e.v=t/100;let i=Wo.HSVToRGB(e.h,e.s,e.v);i.a=this.color.a,this.color=i,this._updatePicker()}_updatePicker(t=this.color){let e=Wo.RGBToHSV(t.r,t.g,t.b),i=this.colorPickerCanvas.getContext("2d");void 0===this.pixelRation&&(this.pixelRatio=(window.devicePixelRatio||1)/(i.webkitBackingStorePixelRatio||i.mozBackingStorePixelRatio||i.msBackingStorePixelRatio||i.oBackingStorePixelRatio||i.backingStorePixelRatio||1)),i.setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0);let n=this.colorPickerCanvas.clientWidth,o=this.colorPickerCanvas.clientHeight;i.clearRect(0,0,n,o),i.putImageData(this.hueCircle,0,0),i.fillStyle="rgba(0,0,0,"+(1-e.v)+")",i.circle(this.centerCoordinates.x,this.centerCoordinates.y,this.r),i.fill(),this.brightnessRange.value=100*e.v,this.opacityRange.value=100*t.a,this.initialColorDiv.style.backgroundColor="rgba("+this.initialColor.r+","+this.initialColor.g+","+this.initialColor.b+","+this.initialColor.a+")",this.newColorDiv.style.backgroundColor="rgba("+this.color.r+","+this.color.g+","+this.color.b+","+this.color.a+")"}_setSize(){this.colorPickerCanvas.style.width="100%",this.colorPickerCanvas.style.height="100%",this.colorPickerCanvas.width=289*this.pixelRatio,this.colorPickerCanvas.height=289*this.pixelRatio}_create(){if(this.frame=document.createElement("div"),this.frame.className="vis-color-picker",this.colorPickerDiv=document.createElement("div"),this.colorPickerSelector=document.createElement("div"),this.colorPickerSelector.className="vis-selector",this.colorPickerDiv.appendChild(this.colorPickerSelector),this.colorPickerCanvas=document.createElement("canvas"),this.colorPickerDiv.appendChild(this.colorPickerCanvas),this.colorPickerCanvas.getContext){let t=this.colorPickerCanvas.getContext("2d");this.pixelRatio=(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1),this.colorPickerCanvas.getContext("2d").setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0)}else{let t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.colorPickerCanvas.appendChild(t)}this.colorPickerDiv.className="vis-color",this.opacityDiv=document.createElement("div"),this.opacityDiv.className="vis-opacity",this.brightnessDiv=document.createElement("div"),this.brightnessDiv.className="vis-brightness",this.arrowDiv=document.createElement("div"),this.arrowDiv.className="vis-arrow",this.opacityRange=document.createElement("input");try{this.opacityRange.type="range",this.opacityRange.min="0",this.opacityRange.max="100"}catch(t){}this.opacityRange.value="100",this.opacityRange.className="vis-range",this.brightnessRange=document.createElement("input");try{this.brightnessRange.type="range",this.brightnessRange.min="0",this.brightnessRange.max="100"}catch(t){}this.brightnessRange.value="100",this.brightnessRange.className="vis-range",this.opacityDiv.appendChild(this.opacityRange),this.brightnessDiv.appendChild(this.brightnessRange);var t=this;this.opacityRange.onchange=function(){t._setOpacity(this.value)},this.opacityRange.oninput=function(){t._setOpacity(this.value)},this.brightnessRange.onchange=function(){t._setBrightness(this.value)},this.brightnessRange.oninput=function(){t._setBrightness(this.value)},this.brightnessLabel=document.createElement("div"),this.brightnessLabel.className="vis-label vis-brightness",this.brightnessLabel.innerHTML="brightness:",this.opacityLabel=document.createElement("div"),this.opacityLabel.className="vis-label vis-opacity",this.opacityLabel.innerHTML="opacity:",this.newColorDiv=document.createElement("div"),this.newColorDiv.className="vis-new-color",this.newColorDiv.innerHTML="new",this.initialColorDiv=document.createElement("div"),this.initialColorDiv.className="vis-initial-color",this.initialColorDiv.innerHTML="initial",this.cancelButton=document.createElement("div"),this.cancelButton.className="vis-button vis-cancel",this.cancelButton.innerHTML="cancel",this.cancelButton.onclick=this._hide.bind(this,!1),this.applyButton=document.createElement("div"),this.applyButton.className="vis-button vis-apply",this.applyButton.innerHTML="apply",this.applyButton.onclick=this._apply.bind(this),this.saveButton=document.createElement("div"),this.saveButton.className="vis-button vis-save",this.saveButton.innerHTML="save",this.saveButton.onclick=this._save.bind(this),this.loadButton=document.createElement("div"),this.loadButton.className="vis-button vis-load",this.loadButton.innerHTML="load last",this.loadButton.onclick=this._loadLast.bind(this),this.frame.appendChild(this.colorPickerDiv),this.frame.appendChild(this.arrowDiv),this.frame.appendChild(this.brightnessLabel),this.frame.appendChild(this.brightnessDiv),this.frame.appendChild(this.opacityLabel),this.frame.appendChild(this.opacityDiv),this.frame.appendChild(this.newColorDiv),this.frame.appendChild(this.initialColorDiv),this.frame.appendChild(this.cancelButton),this.frame.appendChild(this.applyButton),this.frame.appendChild(this.saveButton),this.frame.appendChild(this.loadButton)}_bindHammer(){this.drag={},this.pinch={},this.hammer=new ns(this.colorPickerCanvas),this.hammer.get("pinch").set({enable:!0}),os(this.hammer,(t=>{this._moveSelector(t)})),this.hammer.on("tap",(t=>{this._moveSelector(t)})),this.hammer.on("panstart",(t=>{this._moveSelector(t)})),this.hammer.on("panmove",(t=>{this._moveSelector(t)})),this.hammer.on("panend",(t=>{this._moveSelector(t)}))}_generateHueCircle(){if(!1===this.generated){let t=this.colorPickerCanvas.getContext("2d");void 0===this.pixelRation&&(this.pixelRatio=(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1)),t.setTransform(this.pixelRatio,0,0,this.pixelRatio,0,0);let e,i,n,o,s=this.colorPickerCanvas.clientWidth,r=this.colorPickerCanvas.clientHeight;t.clearRect(0,0,s,r),this.centerCoordinates={x:.5*s,y:.5*r},this.r=.49*s;let a,l=2*Math.PI/360,h=1/360,d=1/this.r;for(n=0;n<360;n++)for(o=0;o0&&this._makeItem([]),this._makeHeader(n),this._handleObject(this.configureOptions[n],[n])),e++);this._makeButton(),this._push()}_push(){this.wrapper=document.createElement("div"),this.wrapper.className="vis-configuration-wrapper",this.container.appendChild(this.wrapper);for(var t=0;t{i.appendChild(t)})),this.domElements.push(i),this.domElements.length}return 0}_makeHeader(t){let e=document.createElement("div");e.className="vis-configuration vis-config-header",e.innerHTML=Wo.xss(t),this._makeItem([],e)}_makeLabel(t,e,i=!1){let n=document.createElement("div");return n.className="vis-configuration vis-config-label vis-config-s"+e.length,n.innerHTML=!0===i?Wo.xss(""+t+":"):Wo.xss(t+":"),n}_makeDropdown(t,e,i){let n=document.createElement("select");n.className="vis-configuration vis-config-select";let o=0;void 0!==e&&-1!==t.indexOf(e)&&(o=t.indexOf(e));for(let e=0;es&&1!==s&&(a.max=Math.ceil(e*t),h=a.max,l="range increased"),a.value=e}else a.value=n;let d=document.createElement("input");d.className="vis-configuration vis-config-rangeinput",d.value=Number(a.value);var c=this;a.onchange=function(){d.value=this.value,c._update(Number(this.value),i)},a.oninput=function(){d.value=this.value};let u=this._makeLabel(i[i.length-1],i),p=this._makeItem(i,u,a,d);""!==l&&this.popupHistory[p]!==h&&(this.popupHistory[p]=h,this._setupPopup(l,p))}_makeButton(){if(!0===this.options.showButton){let t=document.createElement("div");t.className="vis-configuration vis-config-button",t.innerHTML="generate options",t.onclick=()=>{this._printOptions()},t.onmouseover=()=>{t.className="vis-configuration vis-config-button hover"},t.onmouseout=()=>{t.className="vis-configuration vis-config-button"},this.optionsContainer=document.createElement("div"),this.optionsContainer.className="vis-configuration vis-config-option-container",this.domElements.push(this.optionsContainer),this.domElements.push(t)}}_setupPopup(t,e){if(!0===this.initialized&&!0===this.allowCreation&&this.popupCounter{this._removePopup()},this.popupCounter+=1,this.popupDiv={html:i,index:e}}}_removePopup(){void 0!==this.popupDiv.html&&(this.popupDiv.html.parentNode.removeChild(this.popupDiv.html),clearTimeout(this.popupDiv.hideTimeout),clearTimeout(this.popupDiv.deleteTimeout),this.popupDiv={})}_showPopupIfNeeded(){if(void 0!==this.popupDiv.html){let t=this.domElements[this.popupDiv.index].getBoundingClientRect();this.popupDiv.html.style.left=t.left+"px",this.popupDiv.html.style.top=t.top-30+"px",document.body.appendChild(this.popupDiv.html),this.popupDiv.hideTimeout=setTimeout((()=>{this.popupDiv.html.style.opacity=0}),1500),this.popupDiv.deleteTimeout=setTimeout((()=>{this._removePopup()}),1800)}}_makeCheckbox(t,e,i){var n=document.createElement("input");n.type="checkbox",n.className="vis-configuration vis-config-checkbox",n.checked=t,void 0!==e&&(n.checked=e,e!==t&&("object"==typeof t?e!==t.enabled&&this.changedOptions.push({path:i,value:e}):this.changedOptions.push({path:i,value:e})));let o=this;n.onchange=function(){o._update(this.checked,i)};let s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,n)}_makeTextInput(t,e,i){var n=document.createElement("input");n.type="text",n.className="vis-configuration vis-config-text",n.value=e,e!==t&&this.changedOptions.push({path:i,value:e});let o=this;n.onchange=function(){o._update(this.value,i)};let s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,n)}_makeColorField(t,e,i){let n=t[1],o=document.createElement("div");"none"!==(e=void 0===e?n:e)?(o.className="vis-configuration vis-config-colorBlock",o.style.backgroundColor=e):o.className="vis-configuration vis-config-colorBlock none",e=void 0===e?n:e,o.onclick=()=>{this._showColorPicker(e,o,i)};let s=this._makeLabel(i[i.length-1],i);this._makeItem(i,s,o)}_showColorPicker(t,e,i){e.onclick=function(){},this.colorPicker.insertTo(e),this.colorPicker.show(),this.colorPicker.setColor(t),this.colorPicker.setUpdateCallback((t=>{let n="rgba("+t.r+","+t.g+","+t.b+","+t.a+")";e.style.backgroundColor=n,this._update(n,i)})),this.colorPicker.setCloseCallback((()=>{e.onclick=()=>{this._showColorPicker(t,e,i)}}))}_handleObject(t,e=[],i=!1){let n=!1,o=this.options.filter,s=!1;for(let r in t)if(t.hasOwnProperty(r)){n=!0;let a=t[r],l=Wo.copyAndExtendArray(e,r);if("function"==typeof o&&(n=o(r,e),!1===n&&!Array.isArray(a)&&"string"!=typeof a&&"boolean"!=typeof a&&a instanceof Object&&(this.allowCreation=!1,n=this._handleObject(a,l,!0),this.allowCreation=!1===i)),!1!==n){s=!0;let t=this._getValue(l);if(Array.isArray(a))this._handleArray(a,t,l);else if("string"==typeof a)this._makeTextInput(a,t,l);else if("boolean"==typeof a)this._makeCheckbox(a,t,l);else if(a instanceof Object){let t=!0;if(-1!==e.indexOf("physics")&&this.moduleOptions.physics.solver!==r&&(t=!1),!0===t)if(void 0!==a.enabled){let t=Wo.copyAndExtendArray(l,"enabled"),e=this._getValue(t);if(!0===e){let t=this._makeLabel(r,l,!0);this._makeItem(l,t),s=this._handleObject(a,l)||s}else this._makeCheckbox(a,e,l)}else{let t=this._makeLabel(r,l,!0);this._makeItem(l,t),s=this._handleObject(a,l)||s}}else console.error("dont know how to handle",a,r,l)}}return s}_handleArray(t,e,i){"string"==typeof t[0]&&"color"===t[0]?(this._makeColorField(t,e,i),t[1]!==e&&this.changedOptions.push({path:i,value:e})):"string"==typeof t[0]?(this._makeDropdown(t,e,i),t[0]!==e&&this.changedOptions.push({path:i,value:e})):"number"==typeof t[0]&&(this._makeRange(t,e,i),t[0]!==e&&this.changedOptions.push({path:i,value:Number(e)}))}_update(t,e){let i=this._constructOptions(t,e);this.parent.body&&this.parent.body.emitter&&this.parent.body.emitter.emit&&this.parent.body.emitter.emit("configChange",i),this.initialized=!0,this.parent.setOptions(i)}_constructOptions(t,e,i={}){let n=i;t="false"!==(t="true"===t||t)&&t;for(let i=0;ivar options = "+JSON.stringify(t,null,2)+""}getOptions(){let t={};for(var e=0;eo.timeAxis.step.scale,getStep:()=>o.timeAxis.step.step,toScreen:o._toScreen.bind(o),toGlobalScreen:o._toGlobalScreen.bind(o),toTime:o._toTime.bind(o),toGlobalTime:o._toGlobalTime.bind(o)}},this.range=new ts(this.body,this.options),this.components.push(this.range),this.body.range=this.range,this.timeAxis=new rs(this.body,this.options),this.timeAxis2=null,this.components.push(this.timeAxis),this.currentTime=new Ss(this.body,this.options),this.components.push(this.currentTime),this.itemSet=new $s(this.body,this.options),this.components.push(this.itemSet),this.itemsData=null,this.groupsData=null,this.dom.root.onclick=t=>{r("click",t)},this.dom.root.ondblclick=t=>{r("doubleClick",t)},this.dom.root.oncontextmenu=t=>{r("contextmenu",t)},this.dom.root.onmouseover=t=>{r("mouseOver",t)},window.PointerEvent?(this.dom.root.onpointerdown=t=>{r("mouseDown",t)},this.dom.root.onpointermove=t=>{r("mouseMove",t)},this.dom.root.onpointerup=t=>{r("mouseUp",t)}):(this.dom.root.onmousemove=t=>{r("mouseMove",t)},this.dom.root.onmousedown=t=>{r("mouseDown",t)},this.dom.root.onmouseup=t=>{r("mouseUp",t)}),this.initialFitDone=!1,this.on("changed",(()=>{if(null!=o.itemsData){if(!o.initialFitDone&&!o.options.rollingMode)if(o.initialFitDone=!0,null!=o.options.start||null!=o.options.end){if(null==o.options.start||null==o.options.end)var t=o.getItemRange();const e=null!=o.options.start?o.options.start:t.min,i=null!=o.options.end?o.options.end:t.max;o.setWindow(e,i,{animation:!1})}else o.fit({animation:!1});o.initialDrawDone||!o.initialRangeChangeDone&&(o.options.start||o.options.end)&&!o.options.rollingMode||(o.initialDrawDone=!0,o.itemSet.initialDrawDone=!0,o.dom.root.style.visibility="visible",o.dom.loadingScreen.parentNode.removeChild(o.dom.loadingScreen),o.options.onInitialDrawComplete&&setTimeout((()=>o.options.onInitialDrawComplete()),0))}})),this.on("destroyTimeline",(()=>{o.destroy()})),n&&this.setOptions(n),this.body.emitter.on("fit",(t=>{this._onFit(t),this.redraw()})),i&&this.setGroups(i),e&&this.setItems(e),this._redraw()}_createConfigurator(){return new hr(this,this.dom.container,rr)}redraw(){this.itemSet&&this.itemSet.markDirty({refreshItems:!0}),this._redraw()}setOptions(t){if(!0===Zs.validate(t,sr)&&console.log("%cErrors have been found in the supplied options object.",Ks),Cs.prototype.setOptions.call(this,t),"type"in t&&t.type!==this.options.type){this.options.type=t.type;const e=this.itemsData;if(e){const t=this.getSelection();this.setItems(null),this.setItems(e.rawDS),this.setSelection(t)}}}setItems(t){let e;this.itemsDone=!1,e=t?Fo(t)?Yo(t):Yo(new Dn(t)):null,this.itemsData&&this.itemsData.dispose(),this.itemsData=e,this.itemSet&&this.itemSet.setItems(null!=e?e.rawDS:null)}setGroups(t){let e;const i=t=>!1!==t.visible;t?(Array.isArray(t)&&(t=new Dn(t)),e=new Cn(t,{filter:i})):e=null,null!=this.groupsData&&"function"==typeof this.groupsData.setData&&this.groupsData.setData(null),this.groupsData=e,this.itemSet.setGroups(e)}setData(t){t&&t.groups&&this.setGroups(t.groups),t&&t.items&&this.setItems(t.items)}setSelection(t,e){this.itemSet&&this.itemSet.setSelection(t),e&&e.focus&&this.focus(t,e)}getSelection(){return this.itemSet&&this.itemSet.getSelection()||[]}focus(t,e){if(!this.itemsData||null==t)return;const i=Array.isArray(t)?t:[t],n=this.itemsData.get(i);let o=null,s=null;if(n.forEach((t=>{const e=t.start.valueOf(),i="end"in t?t.end.valueOf():t.start.valueOf();(null===o||es)&&(s=i)})),null!==o&&null!==s){const t=this,n=this.itemSet.items[i[0]];let r=-1*this._getScrollTop(),a=null;const l=(e,i,o)=>{const s=pr(t,n);if(!1===s)return;if(a||(a=s),a.itemTop==s.itemTop&&!a.shouldScroll)return;a.itemTop!=s.itemTop&&s.shouldScroll&&(a=s,r=-1*t._getScrollTop());const l=r,h=a.scrollOffset,d=o?h:l+(h-l)*e;t._setScrollTop(-d),i||t._redraw()},h=()=>{const e=pr(t,n);e.shouldScroll&&e.itemTop!=a.itemTop&&(t._setScrollTop(-e.scrollOffset),t._redraw())},d=()=>{h(),setTimeout(h,100)},c=!e||void 0===e.zoom||e.zoom,u=(o+s)/2,p=c?1.1*(s-o):Math.max(this.range.end-this.range.start,1.1*(s-o)),m=!e||void 0===e.animation||e.animation;m||(a={shouldScroll:!1,scrollOffset:-1,itemTop:-1}),this.range.setRange(u-p/2,u+p/2,{animation:m},d,l)}}fit(t,e){const i=!t||void 0===t.animation||t.animation;let n;1===this.itemsData.length&&void 0===this.itemsData.get()[0].end?(n=this.getDataRange(),this.moveTo(n.min.valueOf(),{animation:i},e)):(n=this.getItemRange(),this.range.setRange(n.min,n.max,{animation:i},e))}getItemRange(){const t=this.getDataRange();let e=null!==t.min?t.min.valueOf():null,i=null!==t.max?t.max.valueOf():null,n=null,o=null;if(null!=e&&null!=i){let t=i-e;t<=0&&(t=10);const s=t/this.props.center.width,r={};let a=0;Wo.forEach(this.itemSet.items,((t,e)=>{if(t.groupShowing){const i=!0;r[e]=t.redraw(i),a=r[e].length}}));if(a>0)for(let t=0;t{e[t]()}));if(Wo.forEach(this.itemSet.items,(t=>{const r=cr(t),a=ur(t);let l,h;this.options.rtl?(l=r-(t.getWidthRight()+10)*s,h=a+(t.getWidthLeft()+10)*s):(l=r-(t.getWidthLeft()+10)*s,h=a+(t.getWidthRight()+10)*s),li&&(i=h,o=t)})),n&&o){const s=n.getWidthLeft()+10,r=o.getWidthRight()+10,a=this.props.center.width-s-r;a>0&&(this.options.rtl?(e=cr(n)-r*t/a,i=ur(o)+s*t/a):(e=cr(n)-s*t/a,i=ur(o)+r*t/a))}}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}}getDataRange(){let t=null,e=null;return this.itemsData&&this.itemsData.forEach((i=>{const n=Wo.convert(i.start,"Date").valueOf(),o=Wo.convert(null!=i.end?i.end:i.start,"Date").valueOf();(null===t||ne)&&(e=o)})),{min:null!=t?new Date(t):null,max:null!=e?new Date(e):null}}getEventProperties(t){const e=t.center?t.center.x:t.clientX,i=t.center?t.center.y:t.clientY,n=this.dom.centerContainer.getBoundingClientRect(),o=this.options.rtl?n.right-e:e-n.left,s=i-n.top,r=this.itemSet.itemFromTarget(t),a=this.itemSet.groupFromTarget(t),l=Ds.customTimeFromTarget(t),h=this.itemSet.options.snap||null,d=this.body.util.getScale(),c=this.body.util.getStep(),u=this._toTime(o),p=h?h(u,d,c):u,m=Wo.getTarget(t);let f=null;return null!=r?f="item":null!=l?f="custom-time":Wo.hasParent(m,this.timeAxis.dom.foreground)||this.timeAxis2&&Wo.hasParent(m,this.timeAxis2.dom.foreground)?f="axis":Wo.hasParent(m,this.itemSet.dom.labelSet)?f="group-label":Wo.hasParent(m,this.currentTime.bar)?f="current-time":Wo.hasParent(m,this.dom.center)&&(f="background"),{event:t,item:r?r.id:null,isCluster:!!r&&!!r.isCluster,items:r?r.items||[]:null,group:a?a.groupId:null,customTime:l?l.options.id:null,what:f,pageX:t.srcEvent?t.srcEvent.pageX:t.pageX,pageY:t.srcEvent?t.srcEvent.pageY:t.pageY,x:o,y:s,time:u,snappedTime:p}}toggleRollingMode(){this.range.rolling?this.range.stopRolling():(null==this.options.rollingMode&&this.setOptions(this.options),this.range.startRolling())}_redraw(){Cs.prototype._redraw.call(this)}_onFit(t){const{start:e,end:i,animation:n}=t;i?this.range.setRange(e,i,{animation:n}):this.moveTo(e.valueOf(),{animation:n})}}function cr(t){return Wo.convert(t.data.start,"Date").valueOf()}function ur(t){const e=null!=t.data.end?t.data.end:t.data.start;return Wo.convert(e,"Date").valueOf()}function pr(t,e){if(!e.parent)return!1;const i=t.options.rtl?t.props.rightContainer.height:t.props.leftContainer.height,n=t.props.center.height,o=e.parent;let s=o.top,r=!0;const a=t.timeAxis.options.orientation.axis,l=()=>"bottom"==a?o.height-e.top-e.height:e.top,h=-1*t._getScrollTop(),d=s+l(),c=e.height;return dh+i?s+=l()+c-i+t.itemSet.options.margin.item.vertical:r=!1,s=Math.min(s,n-i),{shouldScroll:r,scrollOffset:s,itemTop:d}}function mr(t){for(var e in t)t.hasOwnProperty(e)&&(t[e].redundant=t[e].used,t[e].used=[])}function fr(t){for(var e in t)if(t.hasOwnProperty(e)&&t[e].redundant){for(var i=0;i0?(n=e[t].redundant[0],e[t].redundant.shift()):(n=document.createElementNS("http://www.w3.org/2000/svg",t),i.appendChild(n)):(n=document.createElementNS("http://www.w3.org/2000/svg",t),e[t]={used:[],redundant:[]},i.appendChild(n)),e[t].used.push(n),n}function vr(t,e,i,n){var o;return e.hasOwnProperty(t)?e[t].redundant.length>0?(o=e[t].redundant[0],e[t].redundant.shift()):(o=document.createElement(t),void 0!==n?i.insertBefore(o,n):i.appendChild(o)):(o=document.createElement(t),e[t]={used:[],redundant:[]},void 0!==n?i.insertBefore(o,n):i.appendChild(o)),e[t].used.push(o),o}function yr(t,e,i,n,o,s){var r;if("circle"==i.style?((r=gr("circle",n,o)).setAttributeNS(null,"cx",t),r.setAttributeNS(null,"cy",e),r.setAttributeNS(null,"r",.5*i.size)):((r=gr("rect",n,o)).setAttributeNS(null,"x",t-.5*i.size),r.setAttributeNS(null,"y",e-.5*i.size),r.setAttributeNS(null,"width",i.size),r.setAttributeNS(null,"height",i.size)),void 0!==i.styles&&r.setAttributeNS(null,"style",i.styles),r.setAttributeNS(null,"class",i.className+" vis-point"),s){var a=gr("text",n,o);s.xOffset&&(t+=s.xOffset),s.yOffset&&(e+=s.yOffset),s.content&&(a.textContent=s.content),s.className&&a.setAttributeNS(null,"class",s.className+" vis-label"),a.setAttributeNS(null,"x",t),a.setAttributeNS(null,"y",e)}return r}function br(t,e,i,n,o,s,r,a){if(0!=n){n<0&&(e-=n*=-1);var l=gr("rect",s,r);l.setAttributeNS(null,"x",t-.5*i),l.setAttributeNS(null,"y",e),l.setAttributeNS(null,"width",i),l.setAttributeNS(null,"height",n),l.setAttributeNS(null,"class",o),a&&l.setAttributeNS(null,"style",a)}}class wr{constructor(t,e,i,n,o,s,r=!1,a=!1){if(this.majorSteps=[1,2,5,10],this.minorSteps=[.25,.5,1,2],this.customLines=null,this.containerHeight=o,this.majorCharHeight=s,this._start=t,this._end=e,this.scale=1,this.minorStepIdx=-1,this.magnitudefactor=1,this.determineScale(),this.zeroAlign=r,this.autoScaleStart=i,this.autoScaleEnd=n,this.formattingFunction=a,i||n){const t=this,e=e=>{const i=e-e%(t.magnitudefactor*t.minorSteps[t.minorStepIdx]);return e%(t.magnitudefactor*t.minorSteps[t.minorStepIdx])>t.magnitudefactor*t.minorSteps[t.minorStepIdx]*.5?i+t.magnitudefactor*t.minorSteps[t.minorStepIdx]:i};i&&(this._start-=2*this.magnitudefactor*this.minorSteps[this.minorStepIdx],this._start=e(this._start)),n&&(this._end+=this.magnitudefactor*this.minorSteps[this.minorStepIdx],this._end=e(this._end)),this.determineScale()}}setCharHeight(t){this.majorCharHeight=t}setHeight(t){this.containerHeight=t}determineScale(){const t=this._end-this._start;this.scale=this.containerHeight/t;const e=this.majorCharHeight/this.scale,i=t>0?Math.round(Math.log(t)/Math.LN10):0;this.minorStepIdx=-1,this.magnitudefactor=Math.pow(10,i);let n=0;i<0&&(n=i);let o=!1;for(let t=n;Math.abs(t)<=Math.abs(i);t++){this.magnitudefactor=Math.pow(10,t);for(let t=0;t=e){o=!0,this.minorStepIdx=t;break}}if(!0===o)break}}is_major(t){return t%(this.magnitudefactor*this.majorSteps[this.minorStepIdx])==0}getStep(){return this.magnitudefactor*this.minorSteps[this.minorStepIdx]}getFirstMajor(){const t=this.magnitudefactor*this.majorSteps[this.minorStepIdx];return this.convertValue(this._start+(t-this._start%t)%t)}formatValue(t){let e=t.toPrecision(5);return"function"==typeof this.formattingFunction&&(e=this.formattingFunction(t)),"number"==typeof e?`${e}`:"string"==typeof e?e:t.toPrecision(5)}getLines(){const t=[],e=this.getStep(),i=(e-this._start%e)%e;for(let n=this._start+i;this._end-n>1e-5;n+=e)n!=this._start&&t.push({major:this.is_major(n),y:this.convertValue(n),val:this.formatValue(n)});return t}followScale(t){const e=this.minorStepIdx,i=this._start,n=this._end,o=this,s=()=>{o.magnitudefactor*=2},r=()=>{o.magnitudefactor/=2};t.minorStepIdx<=1&&this.minorStepIdx<=1||t.minorStepIdx>1&&this.minorStepIdx>1||(t.minorStepIdxn+1e-5)r(),h=!1;else{if(!this.autoScaleStart&&this._start=0)){r(),h=!1;continue}console.warn("Can't adhere to given 'min' range, due to zeroalign")}this.autoScaleStart&&this.autoScaleEnd&&e`${parseFloat(t.toPrecision(3))}`,title:{text:void 0,style:void 0}},right:{range:{min:void 0,max:void 0},format:t=>`${parseFloat(t.toPrecision(3))}`,title:{text:void 0,style:void 0}}},this.linegraphOptions=n,this.linegraphSVG=i,this.props={},this.DOMelements={lines:{},labels:{},title:{}},this.dom={},this.scale=void 0,this.range={start:0,end:0},this.options=Wo.extend({},this.defaultOptions),this.conversionFactor=1,this.setOptions(e),this.width=Number(`${this.options.width}`.replace("px","")),this.minWidth=this.width,this.height=this.linegraphSVG.getBoundingClientRect().height,this.hidden=!1,this.stepPixels=25,this.zeroCrossing=-1,this.amountOfSteps=-1,this.lineOffset=0,this.master=!0,this.masterAxis=null,this.svgElements={},this.iconsRemoved=!1,this.groups={},this.amountOfGroups=0,this._create(),null==this.scale&&this._redrawLabels(),this.framework={svg:this.svg,svgElements:this.svgElements,options:this.options,groups:this.groups};const o=this;this.body.emitter.on("verticalDrag",(()=>{o.dom.lineContainer.style.top=`${o.body.domProps.scrollTop}px`}))}addGroup(t,e){this.groups.hasOwnProperty(t)||(this.groups[t]=e),this.amountOfGroups+=1}updateGroup(t,e){this.groups.hasOwnProperty(t)||(this.amountOfGroups+=1),this.groups[t]=e}removeGroup(t){this.groups.hasOwnProperty(t)&&(delete this.groups[t],this.amountOfGroups-=1)}setOptions(t){if(t){let e=!1;this.options.orientation!=t.orientation&&void 0!==t.orientation&&(e=!0);const i=["orientation","showMinorLabels","showMajorLabels","icons","majorLinesOffset","minorLinesOffset","labelOffsetX","labelOffsetY","iconWidth","width","visible","left","right","alignZeros"];Wo.selectiveDeepExtend(i,this.options,t),this.minWidth=Number(`${this.options.width}`.replace("px","")),!0===e&&this.dom.frame&&(this.hide(),this.show())}}_create(){this.dom.frame=document.createElement("div"),this.dom.frame.style.width=this.options.width,this.dom.frame.style.height=this.height,this.dom.lineContainer=document.createElement("div"),this.dom.lineContainer.style.width="100%",this.dom.lineContainer.style.height=this.height,this.dom.lineContainer.style.position="relative",this.dom.lineContainer.style.visibility="visible",this.dom.lineContainer.style.display="block",this.svg=document.createElementNS("http://www.w3.org/2000/svg","svg"),this.svg.style.position="absolute",this.svg.style.top="0px",this.svg.style.height="100%",this.svg.style.width="100%",this.svg.style.display="block",this.dom.frame.appendChild(this.svg)}_redrawGroupIcons(){let t;mr(this.svgElements);const e=this.options.iconWidth;let i=11.5;t="left"===this.options.orientation?4:this.width-e-4;const n=Object.keys(this.groups);n.sort(((t,e)=>t{const i=t.y,n=t.major;this.options.showMinorLabels&&!1===n&&this._redrawLabel(i-2,t.val,e,"vis-y-axis vis-minor",this.props.minorCharHeight),n&&i>=0&&this._redrawLabel(i-2,t.val,e,"vis-y-axis vis-major",this.props.majorCharHeight),!0===this.master&&(n?this._redrawLine(i,e,"vis-grid vis-horizontal vis-major",this.options.majorLinesOffset,this.props.majorLineWidth):this._redrawLine(i,e,"vis-grid vis-horizontal vis-minor",this.options.minorLinesOffset,this.props.minorLineWidth))}));let s=0;void 0!==this.options[e].title&&void 0!==this.options[e].title.text&&(s=this.props.titleCharHeight);const r=!0===this.options.icons?Math.max(this.options.iconWidth,s)+this.options.labelOffsetX+15:s+this.options.labelOffsetX+15;return this.maxLabelSize>this.width-r&&!0===this.options.visible?(this.width=this.maxLabelSize+r,this.options.width=`${this.width}px`,fr(this.DOMelements.lines),fr(this.DOMelements.labels),this.redraw(),t=!0):this.maxLabelSizethis.minWidth?(this.width=Math.max(this.minWidth,this.maxLabelSize+r),this.options.width=`${this.width}px`,fr(this.DOMelements.lines),fr(this.DOMelements.labels),this.redraw(),t=!0):(fr(this.DOMelements.lines),fr(this.DOMelements.labels),t=!1),t}convertValue(t){return this.scale.convertValue(t)}screenToValue(t){return this.scale.screenToValue(t)}_redrawLabel(t,e,i,n,o){const s=vr("div",this.DOMelements.labels,this.dom.frame);s.className=n,s.innerHTML=Wo.xss(e),"left"===i?(s.style.left=`-${this.options.labelOffsetX}px`,s.style.textAlign="right"):(s.style.right=`-${this.options.labelOffsetX}px`,s.style.textAlign="left"),s.style.top=`${t-.5*o+this.options.labelOffsetY}px`,e+="";const r=Math.max(this.props.majorCharWidth,this.props.minorCharWidth);this.maxLabelSize0&&(i=Math.min(i,Math.abs(e[n-1].screen_x-e[n].screen_x))),0===i&&(void 0===t[e[n].screen_x]&&(t[e[n].screen_x]={amount:0,resolved:0,accumulatedPositive:0,accumulatedNegative:0}),t[e[n].screen_x].amount+=1)},Dr._getSafeDrawData=function(t,e,i){var n,o;return t0?(n=t0){t.sort((function(t,e){return t.screen_x===e.screen_x?t.groupIde[s].screen_y?e[s].screen_y:n,o=ot[r].accumulatedNegative?t[r].accumulatedNegative:n)>t[r].accumulatedPositive?t[r].accumulatedPositive:n,o=(o=o0){return 1==e.options.interpolation.enabled?Cr._catmullRom(t,e):Cr._linear(t)}},Cr.drawIcon=function(t,e,i,n,o,s){var r,a,l=.5*o,h=gr("rect",s.svgElements,s.svg);(h.setAttributeNS(null,"x",e),h.setAttributeNS(null,"y",i-l),h.setAttributeNS(null,"width",n),h.setAttributeNS(null,"height",2*l),h.setAttributeNS(null,"class","vis-outline"),(r=gr("path",s.svgElements,s.svg)).setAttributeNS(null,"class",t.className),void 0!==t.style&&r.setAttributeNS(null,"style",t.style),r.setAttributeNS(null,"d","M"+e+","+i+" L"+(e+n)+","+i),1==t.options.shaded.enabled&&(a=gr("path",s.svgElements,s.svg),"top"==t.options.shaded.orientation?a.setAttributeNS(null,"d","M"+e+", "+(i-l)+"L"+e+","+i+" L"+(e+n)+","+i+" L"+(e+n)+","+(i-l)):a.setAttributeNS(null,"d","M"+e+","+i+" L"+e+","+(i+l)+" L"+(e+n)+","+(i+l)+"L"+(e+n)+","+i),a.setAttributeNS(null,"class",t.className+" vis-icon-fill"),void 0!==t.options.shaded.style&&""!==t.options.shaded.style&&a.setAttributeNS(null,"style",t.options.shaded.style)),1==t.options.drawPoints.enabled)&&yr(e+.5*n,i,{style:t.options.drawPoints.style,styles:t.options.drawPoints.styles,size:t.options.drawPoints.size,className:t.className},s.svgElements,s.svg)},Cr.drawShading=function(t,e,i,n){if(1==e.options.shaded.enabled){var o,s=Number(n.svg.style.height.replace("px","")),r=gr("path",n.svgElements,n.svg),a="L";1==e.options.interpolation.enabled&&(a="C");var l=0;l="top"==e.options.shaded.orientation?0:"bottom"==e.options.shaded.orientation?s:Math.min(Math.max(0,e.zeroPosition),s),o="group"==e.options.shaded.orientation&&null!=i&&null!=i?"M"+t[0][0]+","+t[0][1]+" "+this.serializePath(t,a,!1)+" L"+i[i.length-1][0]+","+i[i.length-1][1]+" "+this.serializePath(i,a,!0)+i[0][0]+","+i[0][1]+" Z":"M"+t[0][0]+","+t[0][1]+" "+this.serializePath(t,a,!1)+" V"+l+" H"+t[0][0]+" Z",r.setAttributeNS(null,"class",e.className+" vis-fill"),void 0!==e.options.shaded.style&&r.setAttributeNS(null,"style",e.options.shaded.style),r.setAttributeNS(null,"d",o)}},Cr.draw=function(t,e,i){if(null!=t&&null!=t){var n=gr("path",i.svgElements,i.svg);n.setAttributeNS(null,"class",e.className),void 0!==e.style&&n.setAttributeNS(null,"style",e.style);var o="L";1==e.options.interpolation.enabled&&(o="C"),n.setAttributeNS(null,"d","M"+t[0][0]+","+t[0][1]+" "+this.serializePath(t,o,!1))}},Cr.serializePath=function(t,e,i){if(t.length<2)return"";var n,o=e;if(i)for(n=t.length-2;n>0;n--)o+=t[n][0]+","+t[n][1]+" ";else for(n=1;n0&&(m=1/m),(f=3*g*(g+v))>0&&(f=1/f),a={screen_x:(-b*n.screen_x+u*o.screen_x+w*s.screen_x)*m,screen_y:(-b*n.screen_y+u*o.screen_y+w*s.screen_y)*m},l={screen_x:(y*o.screen_x+p*s.screen_x-b*r.screen_x)*f,screen_y:(y*o.screen_y+p*s.screen_y-b*r.screen_y)*f},0==a.screen_x&&0==a.screen_y&&(a=o),0==l.screen_x&&0==l.screen_y&&(l=s),x.push([a.screen_x,a.screen_y]),x.push([l.screen_x,l.screen_y]),x.push([s.screen_x,s.screen_y]);return x},Cr._linear=function(t){for(var e=[],i=0;ie.x?1:-1}))):this.itemsData=[]},Sr.prototype.getItems=function(){return this.itemsData},Sr.prototype.setZeroPosition=function(t){this.zeroPosition=t},Sr.prototype.setOptions=function(t){if(void 0!==t){Wo.selectiveDeepExtend(["sampling","style","sort","yAxisOrientation","barChart","zIndex","excludeFromStacking","excludeFromLegend"],this.options,t),"function"==typeof t.drawPoints&&(t.drawPoints={onRender:t.drawPoints}),Wo.mergeOptions(this.options,t,"interpolation"),Wo.mergeOptions(this.options,t,"drawPoints"),Wo.mergeOptions(this.options,t,"shaded"),t.interpolation&&"object"==typeof t.interpolation&&t.interpolation.parametrization&&("uniform"==t.interpolation.parametrization?this.options.interpolation.alpha=0:"chordal"==t.interpolation.parametrization?this.options.interpolation.alpha=1:(this.options.interpolation.parametrization="centripetal",this.options.interpolation.alpha=.5))}},Sr.prototype.update=function(t){this.group=t,this.content=t.content||"graph",this.className=t.className||this.className||"vis-graph-group"+this.groupsUsingDefaultStyles[0]%10,this.visible=void 0===t.visible||t.visible,this.style=t.style,this.setOptions(t.options)},Sr.prototype.getLegend=function(t,e,i,n,o){null!=i&&null!=i||(i={svg:document.createElementNS("http://www.w3.org/2000/svg","svg"),svgElements:{},options:this.options,groups:[this]});switch(null!=n&&null!=n||(n=0),null!=o&&null!=o||(o=.5*e),this.options.style){case"line":Cr.drawIcon(this,n,o,t,e,i);break;case"points":case"point":xr.drawIcon(this,n,o,t,e,i);break;case"bar":Dr.drawIcon(this,n,o,t,e,i)}return{icon:i.svg,label:this.content,orientation:this.options.yAxisOrientation}},Sr.prototype.getYRange=function(t){for(var e=t[0].y,i=t[0].y,n=0;nt[n].y?t[n].y:e,i=i");this.dom.textArea.innerHTML=Wo.xss(s),this.dom.textArea.style.lineHeight=.75*this.options.iconSize+this.options.iconSpacing+"px"}},Tr.prototype.drawLegendIcons=function(){if(this.dom.frame.parentNode){var t=Object.keys(this.groups);t.sort((function(t,e){return t0){var r={};for(this._getRelevantData(s,r,n,o),this._applySampling(s,r),e=0;e0)switch(t.options.style){case"line":l.hasOwnProperty(s[e])||(l[s[e]]=Cr.calcPath(r[s[e]],t)),Cr.draw(l[s[e]],t,this.framework);case"point":case"points":"point"!=t.options.style&&"points"!=t.options.style&&1!=t.options.drawPoints.enabled||xr.draw(r[s[e]],t,this.framework)}}}return fr(this.svgElements),!1},Mr.prototype._stack=function(t,e){var i,n,o,s,r;i=0;for(var a=0;at[a].x){r=e[l],s=0==l?r:e[l-1],i=l;break}}void 0===r&&(s=e[e.length-1],r=e[e.length-1]),n=r.x-s.x,o=r.y-s.y,t[a].y=0==n?t[a].orginalY+r.y:t[a].orginalY+o/n*(t[a].x-s.x)+s.y}},Mr.prototype._getRelevantData=function(t,e,i,n){var o,s,r,a;if(t.length>0)for(s=0;s0)for(var i=0;i0){var o,s=n.length,r=s/(this.body.util.toGlobalScreen(n[n.length-1].x)-this.body.util.toGlobalScreen(n[0].x));o=Math.min(Math.ceil(.2*s),Math.max(1,Math.round(r)));for(var a=new Array(s),l=0;l0){for(s=0;s0&&(o=this.groups[t[s]],!0===r.stack&&"bar"===r.style?"left"===r.yAxisOrientation?a=a.concat(n):l=l.concat(n):i[t[s]]=o.getYRange(n,t[s]));Dr.getStackedYRange(a,i,t,"__barStackLeft","left"),Dr.getStackedYRange(l,i,t,"__barStackRight","right")}},Mr.prototype._updateYAxis=function(t,e){var i,n,o=!1,s=!1,r=!1,a=1e9,l=1e9,h=-1e9,d=-1e9;if(t.length>0){for(var c=0;ci?i:a,h=hi?i:l,d=ds.timeAxis.step.scale,getStep:()=>s.timeAxis.step.step,toScreen:s._toScreen.bind(s),toGlobalScreen:s._toGlobalScreen.bind(s),toTime:s._toTime.bind(s),toGlobalTime:s._toGlobalTime.bind(s)}},this.range=new ts(this.body),this.components.push(this.range),this.body.range=this.range,this.timeAxis=new rs(this.body),this.components.push(this.timeAxis),this.currentTime=new Ss(this.body),this.components.push(this.currentTime),this.linegraph=new Mr(this.body),this.components.push(this.linegraph),this.itemsData=null,this.groupsData=null,this.on("tap",(function(t){s.emit("click",s.getEventProperties(t))})),this.on("doubletap",(function(t){s.emit("doubleClick",s.getEventProperties(t))})),this.dom.root.oncontextmenu=function(t){s.emit("contextmenu",s.getEventProperties(t))},this.initialFitDone=!1,this.on("changed",(function(){if(null!=s.itemsData){if(!s.initialFitDone&&!s.options.rollingMode)if(s.initialFitDone=!0,null!=s.options.start||null!=s.options.end){if(null==s.options.start||null==s.options.end)var t=s.getItemRange();var e=null!=s.options.start?s.options.start:t.min,i=null!=s.options.end?s.options.end:t.max;s.setWindow(e,i,{animation:!1})}else s.fit({animation:!1});s.initialDrawDone||!s.initialRangeChangeDone&&(s.options.start||s.options.end)&&!s.options.rollingMode||(s.initialDrawDone=!0,s.dom.root.style.visibility="visible",s.dom.loadingScreen.parentNode.removeChild(s.dom.loadingScreen),s.options.onInitialDrawComplete&&setTimeout((()=>s.options.onInitialDrawComplete()),0))}})),n&&this.setOptions(n),i&&this.setGroups(i),e&&this.setItems(e),this._redraw()}jr.prototype=new Cs,jr.prototype.setOptions=function(t){!0===Zs.validate(t,Rr)&&console.log("%cErrors have been found in the supplied options object.",Ks),Cs.prototype.setOptions.call(this,t)},jr.prototype.setItems=function(t){var e,i=null==this.itemsData;if(e=t?Fo(t)?Yo(t):Yo(new Dn(t)):null,this.itemsData&&this.itemsData.dispose(),this.itemsData=e,this.linegraph&&this.linegraph.setItems(null!=e?e.rawDS:null),i)if(null!=this.options.start||null!=this.options.end){var n=null!=this.options.start?this.options.start:null,o=null!=this.options.end?this.options.end:null;this.setWindow(n,o,{animation:!1})}else this.fit({animation:!1})},jr.prototype.setGroups=function(t){var e;e=t?Fo(t)?t:new Dn(t):null,this.groupsData=e,this.linegraph.setGroups(e)},jr.prototype.getLegend=function(t,e,i){return void 0===e&&(e=15),void 0===i&&(i=15),void 0!==this.linegraph.groups[t]?this.linegraph.groups[t].getLegend(e,i):"cannot find group:'"+t+"'"},jr.prototype.isGroupVisible=function(t){return void 0!==this.linegraph.groups[t]&&(this.linegraph.groups[t].visible&&(void 0===this.linegraph.options.groups.visibility[t]||1==this.linegraph.options.groups.visibility[t]))},jr.prototype.getDataRange=function(){var t=null,e=null;for(var i in this.linegraph.groups)if(this.linegraph.groups.hasOwnProperty(i)&&1==this.linegraph.groups[i].visible)for(var n=0;ns?s:t,e=null==e||e0&&h.push(d.screenToValue(o)),!c.hidden&&this.itemsData.length>0&&h.push(c.screenToValue(o)),{event:t,customTime:r?r.options.id:null,what:l,pageX:t.srcEvent?t.srcEvent.pageX:t.pageX,pageY:t.srcEvent?t.srcEvent.pageY:t.pageY,x:n,y:o,time:s,value:h}},jr.prototype._createConfigurator=function(){return new hr(this,this.dom.container,Lr)};const Yr=function(){try{return navigator?navigator.languages&&navigator.languages.length?navigator.languages:navigator.userLanguage||navigator.language||navigator.browserLanguage||"en":"en"}catch(t){return"en"}}();Mn.locale(Yr);var Hr={exports:{}};!function(t){function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:void 0!==B?B:"undefined"!=typeof self?self:{},n={exports:{}},o=function(t){return t&&t.Math==Math&&t},s=o("object"==typeof globalThis&&globalThis)||o("object"==typeof window&&window)||o("object"==typeof self&&self)||o("object"==typeof i&&i)||function(){return this}()||Function("return this")(),r=function(t){try{return!!t()}catch(t){return!0}},a=!r((function(){var t=function(){}.bind();return"function"!=typeof t||t.hasOwnProperty("prototype")})),l=a,h=Function.prototype,d=h.apply,c=h.call,u="object"==typeof Reflect&&Reflect.apply||(l?c.bind(d):function(){return c.apply(d,arguments)}),p=a,m=Function.prototype,f=m.bind,g=m.call,v=p&&f.bind(g,g),y=p?function(t){return t&&v(t)}:function(t){return t&&function(){return g.apply(t,arguments)}},b=function(t){return"function"==typeof t},w={},_=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),x=a,k=Function.prototype.call,D=x?k.bind(k):function(){return k.apply(k,arguments)},C={},S={}.propertyIsEnumerable,T=Object.getOwnPropertyDescriptor,E=T&&!S.call({1:2},1);C.f=E?function(t){var e=T(this,t);return!!e&&e.enumerable}:S;var M,O,I=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},A=y,P=A({}.toString),N=A("".slice),F=function(t){return N(P(t),8,-1)},R=y,L=r,j=F,Y=s.Object,H=R("".split),z=L((function(){return!Y("z").propertyIsEnumerable(0)}))?function(t){return"String"==j(t)?H(t,""):Y(t)}:Y,W=s.TypeError,G=function(t){if(null==t)throw W("Can't call method on "+t);return t},V=z,U=G,$=function(t){return V(U(t))},q=b,X=function(t){return"object"==typeof t?null!==t:q(t)},K={},Z=K,Q=s,J=b,tt=function(t){return J(t)?t:void 0},et=function(t,e){return arguments.length<2?tt(Z[t])||tt(Q[t]):Z[t]&&Z[t][e]||Q[t]&&Q[t][e]},it=y({}.isPrototypeOf),nt=et("navigator","userAgent")||"",ot=s,st=nt,rt=ot.process,at=ot.Deno,lt=rt&&rt.versions||at&&at.version,ht=lt&<.v8;ht&&(O=(M=ht.split("."))[0]>0&&M[0]<4?1:+(M[0]+M[1])),!O&&st&&(!(M=st.match(/Edge\/(\d+)/))||M[1]>=74)&&(M=st.match(/Chrome\/(\d+)/))&&(O=+M[1]);var dt=O,ct=dt,ut=r,pt=!!Object.getOwnPropertySymbols&&!ut((function(){var t=Symbol();return!String(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&ct&&ct<41})),mt=pt&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,ft=et,gt=b,vt=it,yt=mt,bt=s.Object,wt=yt?function(t){return"symbol"==typeof t}:function(t){var e=ft("Symbol");return gt(e)&&vt(e.prototype,bt(t))},_t=s.String,xt=function(t){try{return _t(t)}catch(t){return"Object"}},kt=b,Dt=xt,Ct=s.TypeError,St=function(t){if(kt(t))return t;throw Ct(Dt(t)+" is not a function")},Tt=St,Et=function(t,e){var i=t[e];return null==i?void 0:Tt(i)},Mt=D,Ot=b,It=X,At=s.TypeError,Pt={exports:{}},Nt=s,Ft=Object.defineProperty,Rt=function(t,e){try{Ft(Nt,t,{value:e,configurable:!0,writable:!0})}catch(i){Nt[t]=e}return e},Lt="__core-js_shared__",jt=s[Lt]||Rt(Lt,{}),Yt=jt;(Pt.exports=function(t,e){return Yt[t]||(Yt[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.21.1",mode:"pure",copyright:"© 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.21.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Ht=G,zt=s.Object,Bt=function(t){return zt(Ht(t))},Wt=Bt,Gt=y({}.hasOwnProperty),Vt=Object.hasOwn||function(t,e){return Gt(Wt(t),e)},Ut=y,$t=0,qt=Math.random(),Xt=Ut(1..toString),Kt=function(t){return"Symbol("+(void 0===t?"":t)+")_"+Xt(++$t+qt,36)},Zt=s,Qt=Pt.exports,Jt=Vt,te=Kt,ee=pt,ie=mt,ne=Qt("wks"),oe=Zt.Symbol,se=oe&&oe.for,re=ie?oe:oe&&oe.withoutSetter||te,ae=function(t){if(!Jt(ne,t)||!ee&&"string"!=typeof ne[t]){var e="Symbol."+t;ee&&Jt(oe,t)?ne[t]=oe[t]:ne[t]=ie&&se?se(e):re(e)}return ne[t]},le=D,he=X,de=wt,ce=Et,ue=function(t,e){var i,n;if("string"===e&&Ot(i=t.toString)&&!It(n=Mt(i,t)))return n;if(Ot(i=t.valueOf)&&!It(n=Mt(i,t)))return n;if("string"!==e&&Ot(i=t.toString)&&!It(n=Mt(i,t)))return n;throw At("Can't convert object to primitive value")},pe=ae,me=s.TypeError,fe=pe("toPrimitive"),ge=function(t,e){if(!he(t)||de(t))return t;var i,n=ce(t,fe);if(n){if(void 0===e&&(e="default"),i=le(n,t,e),!he(i)||de(i))return i;throw me("Can't convert object to primitive value")}return void 0===e&&(e="number"),ue(t,e)},ve=wt,ye=function(t){var e=ge(t,"string");return ve(e)?e:e+""},be=X,we=s.document,_e=be(we)&&be(we.createElement),xe=function(t){return _e?we.createElement(t):{}},ke=xe,De=!_&&!r((function(){return 7!=Object.defineProperty(ke("div"),"a",{get:function(){return 7}}).a})),Ce=_,Se=D,Te=C,Ee=I,Me=$,Oe=ye,Ie=Vt,Ae=De,Pe=Object.getOwnPropertyDescriptor;w.f=Ce?Pe:function(t,e){if(t=Me(t),e=Oe(e),Ae)try{return Pe(t,e)}catch(t){}if(Ie(t,e))return Ee(!Se(Te.f,t,e),t[e])};var Ne=r,Fe=b,Re=/#|\.prototype\./,Le=function(t,e){var i=Ye[je(t)];return i==ze||i!=He&&(Fe(e)?Ne(e):!!e)},je=Le.normalize=function(t){return String(t).replace(Re,".").toLowerCase()},Ye=Le.data={},He=Le.NATIVE="N",ze=Le.POLYFILL="P",Be=Le,We=St,Ge=a,Ve=y(y.bind),Ue=function(t,e){return We(t),void 0===e?t:Ge?Ve(t,e):function(){return t.apply(e,arguments)}},$e={},qe=_&&r((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Xe=s,Ke=X,Ze=Xe.String,Qe=Xe.TypeError,Je=function(t){if(Ke(t))return t;throw Qe(Ze(t)+" is not an object")},ti=_,ei=De,ii=qe,ni=Je,oi=ye,si=s.TypeError,ri=Object.defineProperty,ai=Object.getOwnPropertyDescriptor,li="enumerable",hi="configurable",di="writable";$e.f=ti?ii?function(t,e,i){if(ni(t),e=oi(e),ni(i),"function"==typeof t&&"prototype"===e&&"value"in i&&di in i&&!i.writable){var n=ai(t,e);n&&n.writable&&(t[e]=i.value,i={configurable:hi in i?i.configurable:n.configurable,enumerable:li in i?i.enumerable:n.enumerable,writable:!1})}return ri(t,e,i)}:ri:function(t,e,i){if(ni(t),e=oi(e),ni(i),ei)try{return ri(t,e,i)}catch(t){}if("get"in i||"set"in i)throw si("Accessors not supported");return"value"in i&&(t[e]=i.value),t};var ci=$e,ui=I,pi=_?function(t,e,i){return ci.f(t,e,ui(1,i))}:function(t,e,i){return t[e]=i,t},mi=s,fi=u,gi=y,vi=b,yi=w.f,bi=Be,wi=K,_i=Ue,xi=pi,ki=Vt,Di=function(t){var e=function(i,n,o){if(this instanceof e){switch(arguments.length){case 0:return new t;case 1:return new t(i);case 2:return new t(i,n)}return new t(i,n,o)}return fi(t,this,arguments)};return e.prototype=t.prototype,e},Ci=function(t,e){var i,n,o,s,r,a,l,h,d=t.target,c=t.global,u=t.stat,p=t.proto,m=c?mi:u?mi[d]:(mi[d]||{}).prototype,f=c?wi:wi[d]||xi(wi,d,{})[d],g=f.prototype;for(o in e)i=!bi(c?o:d+(u?".":"#")+o,t.forced)&&m&&ki(m,o),r=f[o],i&&(a=t.noTargetGet?(h=yi(m,o))&&h.value:m[o]),s=i&&a?a:e[o],i&&typeof r==typeof s||(l=t.bind&&i?_i(s,mi):t.wrap&&i?Di(s):p&&vi(s)?gi(s):s,(t.sham||s&&s.sham||r&&r.sham)&&xi(l,"sham",!0),xi(f,o,l),p&&(ki(wi,n=d+"Prototype")||xi(wi,n,{}),xi(wi[n],o,s),t.real&&g&&!g[o]&&xi(g,o,s)))},Si=Ci,Ti=_,Ei=$e.f;Si({target:"Object",stat:!0,forced:Object.defineProperty!==Ei,sham:!Ti},{defineProperty:Ei});var Mi=K.Object,Oi=n.exports=function(t,e,i){return Mi.defineProperty(t,e,i)};Mi.defineProperty.sham&&(Oi.sham=!0);var Ii=n.exports,Ai=Ii;function Pi(t,e){for(var i=0;i0?sn:on)(e)},an=rn,ln=Math.min,hn=function(t){return t>0?ln(an(t),9007199254740991):0},dn=function(t){return hn(t.length)},cn=St,un=Bt,pn=z,mn=dn,fn=s.TypeError,gn=function(t){return function(e,i,n,o){cn(i);var s=un(e),r=pn(s),a=mn(s),l=t?a-1:0,h=t?-1:1;if(n<2)for(;;){if(l in r){o=r[l],l+=h;break}if(l+=h,t?l<0:a<=l)throw fn("Reduce of empty array with no initial value")}for(;t?l>=0:a>l;l+=h)l in r&&(o=i(o,r[l],l,s));return o}},vn={left:gn(!1),right:gn(!0)},yn=r,bn=function(t,e){var i=[][t];return!!i&&yn((function(){i.call(null,e||function(){return 1},1)}))},wn="process"==F(s.process),_n=vn.left,xn=dt,kn=wn;Ci({target:"Array",proto:!0,forced:!bn("reduce")||!kn&&xn>79&&xn<83},{reduce:function(t){var e=arguments.length;return _n(this,t,e,e>1?arguments[1]:void 0)}});var Dn=Zi("Array").reduce,Cn=it,Sn=Dn,Tn=Array.prototype,En=function(t){var e=t.reduce;return t===Tn||Cn(Tn,t)&&e===Tn.reduce?Sn:e},Mn=F,On=Array.isArray||function(t){return"Array"==Mn(t)},In={};In[ae("toStringTag")]="z";var An="[object z]"===String(In),Pn=s,Nn=An,Fn=b,Rn=F,Ln=ae("toStringTag"),jn=Pn.Object,Yn="Arguments"==Rn(function(){return arguments}()),Hn=Nn?Rn:function(t){var e,i,n;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(i=function(t,e){try{return t[e]}catch(t){}}(e=jn(t),Ln))?i:Yn?Rn(e):"Object"==(n=Rn(e))&&Fn(e.callee)?"Arguments":n},zn=b,Bn=jt,Wn=y(Function.toString);zn(Bn.inspectSource)||(Bn.inspectSource=function(t){return Wn(t)});var Gn=Bn.inspectSource,Vn=y,Un=r,$n=b,qn=Hn,Xn=Gn,Kn=function(){},Zn=[],Qn=et("Reflect","construct"),Jn=/^\s*(?:class|function)\b/,to=Vn(Jn.exec),eo=!Jn.exec(Kn),io=function(t){if(!$n(t))return!1;try{return Qn(Kn,Zn,t),!0}catch(t){return!1}},no=function(t){if(!$n(t))return!1;switch(qn(t)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return eo||!!to(Jn,Xn(t))}catch(t){return!0}};no.sham=!0;var oo=!Qn||Un((function(){var t;return io(io.call)||!io(Object)||!io((function(){t=!0}))||t}))?no:io,so=s,ro=On,ao=oo,lo=X,ho=ae("species"),co=so.Array,uo=function(t){var e;return ro(t)&&(e=t.constructor,(ao(e)&&(e===co||ro(e.prototype))||lo(e)&&null===(e=e[ho]))&&(e=void 0)),void 0===e?co:e},po=function(t,e){return new(uo(t))(0===e?0:e)},mo=Ue,fo=z,go=Bt,vo=dn,yo=po,bo=y([].push),wo=function(t){var e=1==t,i=2==t,n=3==t,o=4==t,s=6==t,r=7==t,a=5==t||s;return function(l,h,d,c){for(var u,p,m=go(l),f=fo(m),g=mo(h,d),v=vo(f),y=0,b=c||yo,w=e?b(l,v):i||r?b(l,0):void 0;v>y;y++)if((a||y in f)&&(p=g(u=f[y],y,m),t))if(e)w[y]=p;else if(p)switch(t){case 3:return!0;case 5:return u;case 6:return y;case 2:bo(w,u)}else switch(t){case 4:return!1;case 7:bo(w,u)}return s?-1:n||o?o:w}},_o={forEach:wo(0),map:wo(1),filter:wo(2),some:wo(3),every:wo(4),find:wo(5),findIndex:wo(6),filterReject:wo(7)},xo=r,ko=dt,Do=ae("species"),Co=function(t){return ko>=51||!xo((function(){var e=[];return(e.constructor={})[Do]=function(){return{foo:1}},1!==e[t](Boolean).foo}))},So=_o.filter;Ci({target:"Array",proto:!0,forced:!Co("filter")},{filter:function(t){return So(this,t,arguments.length>1?arguments[1]:void 0)}});var To=Zi("Array").filter,Eo=it,Mo=To,Oo=Array.prototype,Io=function(t){var e=t.filter;return t===Oo||Eo(Oo,t)&&e===Oo.filter?Mo:e},Ao=_o.map;Ci({target:"Array",proto:!0,forced:!Co("map")},{map:function(t){return Ao(this,t,arguments.length>1?arguments[1]:void 0)}});var Po=Zi("Array").map,No=it,Fo=Po,Ro=Array.prototype,Lo=function(t){var e=t.map;return t===Ro||No(Ro,t)&&e===Ro.map?Fo:e},jo=On,Yo=dn,Ho=Ue,zo=s.TypeError,Bo=function(t,e,i,n,o,s,r,a){for(var l,h,d=o,c=0,u=!!r&&Ho(r,a);c0&&jo(l))h=Yo(l),d=Bo(t,e,l,h,d,s-1)-1;else{if(d>=9007199254740991)throw zo("Exceed the acceptable array length");t[d]=l}d++}c++}return d},Wo=Bo,Go=St,Vo=Bt,Uo=dn,$o=po;Ci({target:"Array",proto:!0},{flatMap:function(t){var e,i=Vo(this),n=Uo(i);return Go(t),(e=$o(i,0)).length=Wo(e,i,i,n,0,1,t,arguments.length>1?arguments[1]:void 0),e}});var qo,Xo,Ko,Zo=Zi("Array").flatMap,Qo=it,Jo=Zo,ts=Array.prototype,es=function(t){var e=t.flatMap;return t===ts||Qo(ts,t)&&e===ts.flatMap?Jo:e},is=function(){function t(i,n,o){var s,r,a;e(this,t),Fi(this,"_source",void 0),Fi(this,"_transformers",void 0),Fi(this,"_target",void 0),Fi(this,"_listeners",{add:nn(s=this._add).call(s,this),remove:nn(r=this._remove).call(r,this),update:nn(a=this._update).call(a,this)}),this._source=i,this._transformers=n,this._target=o}return Ni(t,[{key:"all",value:function(){return this._target.update(this._transformItems(this._source.get())),this}},{key:"start",value:function(){return this._source.on("add",this._listeners.add),this._source.on("remove",this._listeners.remove),this._source.on("update",this._listeners.update),this}},{key:"stop",value:function(){return this._source.off("add",this._listeners.add),this._source.off("remove",this._listeners.remove),this._source.off("update",this._listeners.update),this}},{key:"_transformItems",value:function(t){var e;return En(e=this._transformers).call(e,(function(t,e){return e(t)}),t)}},{key:"_add",value:function(t,e){null!=e&&this._target.add(this._transformItems(this._source.get(e.items)))}},{key:"_update",value:function(t,e){null!=e&&this._target.update(this._transformItems(this._source.get(e.items)))}},{key:"_remove",value:function(t,e){null!=e&&this._target.remove(this._transformItems(e.oldData))}}]),t}(),ns=function(){function t(i){e(this,t),Fi(this,"_source",void 0),Fi(this,"_transformers",[]),this._source=i}return Ni(t,[{key:"filter",value:function(t){return this._transformers.push((function(e){return Io(e).call(e,t)})),this}},{key:"map",value:function(t){return this._transformers.push((function(e){return Lo(e).call(e,t)})),this}},{key:"flatMap",value:function(t){return this._transformers.push((function(e){return es(e).call(e,t)})),this}},{key:"to",value:function(t){return new is(this._source,this._transformers,t)}}]),t}(),os=Hn,ss=s.String,rs=function(t){if("Symbol"===os(t))throw TypeError("Cannot convert a Symbol value to a string");return ss(t)},as=y,ls=rn,hs=rs,ds=G,cs=as("".charAt),us=as("".charCodeAt),ps=as("".slice),ms=function(t){return function(e,i){var n,o,s=hs(ds(e)),r=ls(i),a=s.length;return r<0||r>=a?t?"":void 0:(n=us(s,r))<55296||n>56319||r+1===a||(o=us(s,r+1))<56320||o>57343?t?cs(s,r):n:t?ps(s,r,r+2):o-56320+(n-55296<<10)+65536}},fs={codeAt:ms(!1),charAt:ms(!0)},gs=b,vs=Gn,ys=s.WeakMap,bs=gs(ys)&&/native code/.test(vs(ys)),ws=Pt.exports,_s=Kt,xs=ws("keys"),ks=function(t){return xs[t]||(xs[t]=_s(t))},Ds={},Cs=bs,Ss=s,Ts=y,Es=X,Ms=pi,Os=Vt,Is=jt,As=ks,Ps=Ds,Ns="Object already initialized",Fs=Ss.TypeError,Rs=Ss.WeakMap;if(Cs||Is.state){var Ls=Is.state||(Is.state=new Rs),js=Ts(Ls.get),Ys=Ts(Ls.has),Hs=Ts(Ls.set);qo=function(t,e){if(Ys(Ls,t))throw new Fs(Ns);return e.facade=t,Hs(Ls,t,e),e},Xo=function(t){return js(Ls,t)||{}},Ko=function(t){return Ys(Ls,t)}}else{var zs=As("state");Ps[zs]=!0,qo=function(t,e){if(Os(t,zs))throw new Fs(Ns);return e.facade=t,Ms(t,zs,e),e},Xo=function(t){return Os(t,zs)?t[zs]:{}},Ko=function(t){return Os(t,zs)}}var Bs={set:qo,get:Xo,has:Ko,enforce:function(t){return Ko(t)?Xo(t):qo(t,{})},getterFor:function(t){return function(e){var i;if(!Es(e)||(i=Xo(e)).type!==t)throw Fs("Incompatible receiver, "+t+" required");return i}}},Ws=_,Gs=Vt,Vs=Function.prototype,Us=Ws&&Object.getOwnPropertyDescriptor,$s=Gs(Vs,"name"),qs={EXISTS:$s,PROPER:$s&&"something"===function(){}.name,CONFIGURABLE:$s&&(!Ws||Ws&&Us(Vs,"name").configurable)},Xs={},Ks=rn,Zs=Math.max,Qs=Math.min,Js=function(t,e){var i=Ks(t);return i<0?Zs(i+e,0):Qs(i,e)},tr=$,er=Js,ir=dn,nr=function(t){return function(e,i,n){var o,s=tr(e),r=ir(s),a=er(n,r);if(t&&i!=i){for(;r>a;)if((o=s[a++])!=o)return!0}else for(;r>a;a++)if((t||a in s)&&s[a]===i)return t||a||0;return!t&&-1}},or={includes:nr(!0),indexOf:nr(!1)},sr=Vt,rr=$,ar=or.indexOf,lr=Ds,hr=y([].push),dr=function(t,e){var i,n=rr(t),o=0,s=[];for(i in n)!sr(lr,i)&&sr(n,i)&&hr(s,i);for(;e.length>o;)sr(n,i=e[o++])&&(~ar(s,i)||hr(s,i));return s},cr=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],ur=dr,pr=cr,mr=Object.keys||function(t){return ur(t,pr)},fr=_,gr=qe,vr=$e,yr=Je,br=$,wr=mr;Xs.f=fr&&!gr?Object.defineProperties:function(t,e){yr(t);for(var i,n=br(e),o=wr(e),s=o.length,r=0;s>r;)vr.f(t,i=o[r++],n[i]);return t};var _r,xr=et("document","documentElement"),kr=Je,Dr=Xs,Cr=cr,Sr=Ds,Tr=xr,Er=xe,Mr=ks("IE_PROTO"),Or=function(){},Ir=function(t){return"