From 609fece2b5b3f5bb3562ee9b29e7446c0ef98406 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Oct 2022 13:00:02 +0100 Subject: [PATCH 001/158] First Bluetooth proxy --- esphome/ble_proxy_1.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 esphome/ble_proxy_1.yaml diff --git a/esphome/ble_proxy_1.yaml b/esphome/ble_proxy_1.yaml new file mode 100644 index 00000000..05a831ec --- /dev/null +++ b/esphome/ble_proxy_1.yaml @@ -0,0 +1,33 @@ +--- +packages: + common: !include common/common.yaml + +substitutions: + device_name: bluetooth_proxy_1 + device_description: Bluetooth Proxy 1 + friendly_name: Bluetooth Proxy 1 + +esphome: + name: ${device_name} + comment: ${device_description} + platform: ESP32 + board: nodemcu-32s + +captive_portal: + +logger: + +web_server: + +time: + - platform: homeassistant + id: esptime + timezone: Europe/London + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true + +bluetooth_proxy: From 50e28812cb8428d3bc9922423d0b645996f3954b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Oct 2022 13:04:43 +0100 Subject: [PATCH 002/158] BLE device tracker --- device_trackers.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/device_trackers.yaml b/device_trackers.yaml index f6ee2616..250d7671 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 From cae97587e83b8d9c97e99f70dec9cb32f2f60bfa Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 21 Nov 2022 12:31:45 +0000 Subject: [PATCH 003/158] Better Thermostat 1.0.0-beta45 --- .../better_thermostat/__init__.py | 56 + .../better_thermostat/adapters/deconz.py | 57 ++ .../better_thermostat/adapters/generic.py | 56 + .../better_thermostat/adapters/mqtt.py | 165 +++ .../better_thermostat/adapters/tado.py | 60 ++ .../better_thermostat/climate.py | 958 ++++++++++++++++++ .../better_thermostat/config_flow.py | 513 ++++++++++ custom_components/better_thermostat/const.py | 72 ++ .../better_thermostat/device_trigger.py | 152 +++ .../better_thermostat/diagnostics.py | 51 + .../better_thermostat/events/temperature.py | 46 + .../better_thermostat/events/trv.py | 340 +++++++ .../better_thermostat/events/window.py | 73 ++ .../better_thermostat/manifest.json | 21 + .../better_thermostat/services.yaml | 39 + .../better_thermostat/strings.json | 79 ++ .../better_thermostat/translations/da.json | 80 ++ .../better_thermostat/translations/de.json | 80 ++ .../better_thermostat/translations/en.json | 80 ++ .../better_thermostat/translations/fr.json | 80 ++ .../better_thermostat/translations/pl.json | 80 ++ .../better_thermostat/utils/bridge.py | 88 ++ .../better_thermostat/utils/controlling.py | 237 +++++ .../better_thermostat/utils/helpers.py | 499 +++++++++ .../better_thermostat/utils/weather.py | 185 ++++ 25 files changed, 4147 insertions(+) create mode 100644 custom_components/better_thermostat/__init__.py create mode 100644 custom_components/better_thermostat/adapters/deconz.py create mode 100644 custom_components/better_thermostat/adapters/generic.py create mode 100644 custom_components/better_thermostat/adapters/mqtt.py create mode 100644 custom_components/better_thermostat/adapters/tado.py create mode 100644 custom_components/better_thermostat/climate.py create mode 100644 custom_components/better_thermostat/config_flow.py create mode 100644 custom_components/better_thermostat/const.py create mode 100644 custom_components/better_thermostat/device_trigger.py create mode 100644 custom_components/better_thermostat/diagnostics.py create mode 100644 custom_components/better_thermostat/events/temperature.py create mode 100644 custom_components/better_thermostat/events/trv.py create mode 100644 custom_components/better_thermostat/events/window.py create mode 100644 custom_components/better_thermostat/manifest.json create mode 100644 custom_components/better_thermostat/services.yaml create mode 100644 custom_components/better_thermostat/strings.json create mode 100644 custom_components/better_thermostat/translations/da.json create mode 100644 custom_components/better_thermostat/translations/de.json create mode 100644 custom_components/better_thermostat/translations/en.json create mode 100644 custom_components/better_thermostat/translations/fr.json create mode 100644 custom_components/better_thermostat/translations/pl.json create mode 100644 custom_components/better_thermostat/utils/bridge.py create mode 100644 custom_components/better_thermostat/utils/controlling.py create mode 100644 custom_components/better_thermostat/utils/helpers.py create mode 100644 custom_components/better_thermostat/utils/weather.py diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py new file mode 100644 index 00000000..9a3bf07c --- /dev/null +++ b/custom_components/better_thermostat/__init__.py @@ -0,0 +1,56 @@ +"""The better_thermostat component.""" +import logging +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, Config +from homeassistant.config_entries import ConfigEntry + +from .const import CONF_FIX_CALIBRATION, CONF_HEATER + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "better_thermostat" +PLATFORMS = [Platform.CLIMATE] + + +async def async_setup(hass: HomeAssistant, config: Config): + """Set up this integration using YAML is not supported.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + 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) + + _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..1dda35a6 --- /dev/null +++ b/custom_components/better_thermostat/adapters/deconz.py @@ -0,0 +1,57 @@ +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 set_offset(self, entity_id, offset): + """Set new target offset.""" + await self.hass.services.async_call( + "deconz", + "configure", + {"entity_id": entity_id, "offset": offset}, + blocking=True, + limit=None, + 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..a8ac0b21 --- /dev/null +++ b/custom_components/better_thermostat/adapters/generic.py @@ -0,0 +1,56 @@ +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 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, + limit=None, + 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, + limit=None, + 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..3d582e90 --- /dev/null +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -0,0 +1,165 @@ +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, + limit=None, + 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 set_offset(self, entity_id, offset): + """Set new target offset.""" + max_calibration = float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).attributes.get("max", 127) + ) + ) + min_calibration = float( + str( + self.hass.states.get( + self.real_trvs[entity_id]["local_temperature_calibration_entity"] + ).attributes.get("min", -128) + ) + ) + + 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, + limit=None, + 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" + ): + 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, + limit=None, + 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..5bb30506 --- /dev/null +++ b/custom_components/better_thermostat/adapters/tado.py @@ -0,0 +1,60 @@ +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 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, + limit=None, + 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..07fc2e09 --- /dev/null +++ b/custom_components/better_thermostat/climate.py @@ -0,0 +1,958 @@ +"""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.weather import check_ambient_air_temperature, check_weather +from .utils.bridge import get_current_offset, init, load_adapter +from .utils.helpers import convert_to_float +from homeassistant.helpers import entity_platform +from homeassistant.core import callback, CoreState + +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 . import DOMAIN +from .const import ( + ATTR_STATE_CALL_FOR_HEAT, + ATTR_STATE_HUMIDIY, + ATTR_STATE_LAST_CHANGE, + ATTR_STATE_MAIN_MODE, + ATTR_STATE_WINDOW_OPEN, + ATTR_STATE_SAVED_TEMPERATURE, + 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, + 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, update_hvac_action +from .events.window import trigger_window_change, window_queue + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_devices): + """Setup sensor platform.""" + + async def async_service_handler(self, data): + _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]) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_SET_TEMP_TARGET_TEMPERATURE, + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, + async_service_handler, + [ + BetterThermostatEntityFeature.TARGET_TEMPERATURE, + BetterThermostatEntityFeature.TARGET_TEMPERATURE_RANGE, + ], + ) + platform.async_register_entity_service( + SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE, {}, 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) + + @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._async_unsub_state_changed = None + 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)) + + 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"]) + self.real_trvs[trv["trv"]] = { + "calibration": _calibration, + "integration": trv["integration"], + "adapter": _adapter, + "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, + "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. + """ + 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_weather(self) + if self._last_call_for_heat != self.call_for_heat: + self._last_call_for_heat = self.call_for_heat + self.async_write_ha_state() + if event is not None: + await self.control_queue_task.put(self) + + async def _trigger_time(self, event=None): + _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): + 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): + 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): + if self._async_unsub_state_changed is None: + return + + self.async_set_context(event.context) + + if (event.data.get("new_state")) is None: + return + + self.hass.async_create_task(trigger_trv_change(self, event)) + # await trigger_trv_change(self, event) + + async def _trigger_window_change(self, event): + 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.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 + for trv in self.real_trvs.keys(): + trv_state = self.hass.states.get(trv) + 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) + 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.cur_temp = convert_to_float( + str(sensor_state.state), self.name, "startup()" + ) + 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, + "startuo()", + ) + if self.window_id is not None: + 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 not self.bt_hvac_mode and old_state.state: + 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 + ) + + 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): + _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(): + await init(self, trv) + if self.real_trvs[trv]["calibration"] == 0: + self.real_trvs[trv]["last_calibration"] = await get_current_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"] = None + 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 self.async_update_ha_state() + update_hvac_action(self) + await asyncio.sleep(5) + # Add listener + if self.outdoor_sensor is not None: + self.async_on_remove( + async_track_time_change(self.hass, self._trigger_time, 5, 0, 0) + ) + + 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) + break + + @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, + } + + 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: + if self.bt_hvac_mode == HVACMode.HEAT: + return HVACAction.HEATING + else: + return 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..7038ce5f --- /dev/null +++ b/custom_components/better_thermostat/config_flow.py @@ -0,0 +1,513 @@ +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_CALIBRATIION_ROUND, + CONF_CALIBRATION, + CONF_CHILD_LOCK, + CONF_FIX_CALIBRATION, + CONF_HEAT_AUTO_SWAPPED, + CONF_HEATER, + CONF_HOMATICIP, + CONF_HUMIDITY, + CONF_MODEL, + CONF_OFF_TEMPERATURE, + CONF_OUTDOOR_SENSOR, + CONF_SENSOR, + CONF_SENSOR_WINDOW, + CONF_VALVE_MAINTENANCE, + CONF_WEATHER, + CONF_WINDOW_TIMEOUT, +) +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 . import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 2 + 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() + + _calibration = {"target_temp_based": "Target Temperature"} + _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): + _calibration["local_calibration_based"] = "Local Calibration" + _default_calibration = "local_calibration_based" + + fields[ + vol.Required( + CONF_CALIBRATION, + default=user_input.get(CONF_CALIBRATION, _default_calibration), + ) + ] = vol.In(_calibration) + + 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=user_input.get(CONF_HEAT_AUTO_SWAPPED, has_auto), + ) + ] = bool + + fields[ + vol.Optional( + CONF_FIX_CALIBRATION, + default=user_input.get(CONF_FIX_CALIBRATION, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_CALIBRATIION_ROUND, + default=user_input.get(CONF_CALIBRATIION_ROUND, True), + ) + ] = 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 "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, + default=user_input.get(CONF_WINDOW_TIMEOUT, 0), + ): int, + 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="", data=self.updated_config) + + user_input = user_input or {} + homematic = False + if _trv_config.get("integration").find("homematic") != -1: + homematic = True + + fields = OrderedDict() + + _calibration = {"target_temp_based": "Target Temperature"} + _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): + _calibration["local_calibration_based"] = "Local Calibration" + _default_calibration = "local_calibration_based" + + fields[ + vol.Required( + CONF_CALIBRATION, + default=_trv_config["advanced"].get( + CONF_CALIBRATION, _default_calibration + ), + ) + ] = vol.In(_calibration) + + 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 + + fields[ + vol.Optional( + CONF_FIX_CALIBRATION, + default=_trv_config["advanced"].get(CONF_FIX_CALIBRATION, False), + ) + ] = bool + + fields[ + vol.Optional( + CONF_CALIBRATIION_ROUND, + default=_trv_config["advanced"].get(CONF_CALIBRATIION_ROUND, True), + ) + ] = 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) + self.updated_config[CONF_WINDOW_TIMEOUT] = user_input.get( + CONF_WINDOW_TIMEOUT + ) + self.updated_config[CONF_OFF_TEMPERATURE] = user_input.get( + CONF_OFF_TEMPERATURE + ) + self.name = user_input.get(CONF_NAME, "-") + for trv in self.updated_config[CONF_HEATER]: + trv["adapter"] = load_adapter(self, trv["integration"], trv["trv"]) + 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) + ) + + fields[ + vol.Optional( + CONF_WINDOW_TIMEOUT, + default=self.config_entry.data.get(CONF_WINDOW_TIMEOUT, 30), + ) + ] = int + + 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..fc3714bf --- /dev/null +++ b/custom_components/better_thermostat/const.py @@ -0,0 +1,72 @@ +"""""" +import json +from enum import IntEnum +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_CALIBRATIION_ROUND = "calibration_round" +CONF_FIX_CALIBRATION = "fix_calibration" +CONF_HEAT_AUTO_SWAPPED = "heat_auto_swapped" +CONF_MODEL = "model" +CONF_HOMATICIP = "homaticip" +CONF_INTEGRATION = "integration" +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" + +SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE = "restore_saved_target_temperature" +SERVICE_SET_TEMP_TARGET_TEMPERATURE = "set_temp_target_temperature" + +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 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..3296bcc9 --- /dev/null +++ b/custom_components/better_thermostat/events/temperature.py @@ -0,0 +1,46 @@ +import logging +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" + ) + + if _incoming_temperature != self.cur_temp: + _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.async_write_ha_state() + await self.control_queue_task.put(self) + self.last_external_sensor_change = datetime.now() diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py new file mode 100644 index 00000000..962ae7e7 --- /dev/null +++ b/custom_components/better_thermostat/events/trv.py @@ -0,0 +1,340 @@ +from datetime import datetime +import logging +from typing import Union + +from homeassistant.components.climate.const import ( + HVACMode, + ATTR_HVAC_ACTION, + HVACAction, +) +from homeassistant.core import State, callback +from homeassistant.components.group.util import find_state_attributes +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): + """Processes TRV status updates + + Parameters + ---------- + self : + self instance of better_thermostat + event : + Event object from the eventbus. Contains the new and old state from the TRV. + + Returns + ------- + None + """ + if self.startup_running: + return + + entity_id = event.data.get("entity_id") + + child_lock = self.real_trvs[entity_id]["advanced"].get("child_lock") + + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + _org_trv_state = self.hass.states.get(entity_id).state + + 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 + + try: + new_state = convert_inbound_states(self, entity_id, new_state) + except TypeError: + _LOGGER.debug( + f"better_thermostat {self.name}: remapping TRV {entity_id} state failed, skipping" + ) + return + + _new_current_temp = convert_to_float( + str(new_state.attributes.get("current_temperature", None)), + self.name, + "TRV_current_temp", + ) + + if ( + _new_current_temp is not None + and self.real_trvs[entity_id]["current_temperature"] != _new_current_temp + ): + _old_temp = self.real_trvs[entity_id]["current_temperature"] + if self.real_trvs[entity_id]["calibration_received"] is False: + 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() + 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}" + ) + 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 is True: + self.async_write_ha_state() + return + + new_decoded_system_mode = str(new_state.state) + + if new_decoded_system_mode in (HVACMode.OFF, HVACMode.HEAT): + if self.real_trvs[entity_id]["hvac_mode"] != _org_trv_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} - converted {new_decoded_system_mode}" + ) + self.real_trvs[entity_id]["hvac_mode"] = _org_trv_state + 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 + ): + self.bt_hvac_mode = new_decoded_system_mode + + _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 self.bt_hvac_mode is not HVACMode.OFF: + 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 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}" + ) + if ( + child_lock is False + and self.real_trvs[entity_id]["target_temp_received"] is True + ): + self.bt_target_temp = _new_heating_setpoint + + if ( + self.bt_hvac_mode == HVACMode.OFF + and self.real_trvs[entity_id]["hvac_mode"] == HVACMode.OFF + ): + self.async_write_ha_state() + return + + self.async_write_ha_state() + update_hvac_action(self) + return await self.control_queue_task.put(self) + + +def update_hvac_action(self): + """Update hvac action.""" + # return the most common action if it is not off + states = [ + state + for entity_id in self.real_trvs + if (state := self.hass.states.get(entity_id)) is not None + ] + + # check if trv has pi_heating_demand + pi_heating_demands = list(find_state_attributes(states, "pi_heating_demand")) + if pi_heating_demands: + pi_heating_demand = max(pi_heating_demands) + if pi_heating_demand > 0: + self.attr_hvac_action = HVACAction.HEATING + self.async_write_ha_state() + return + + hvac_actions = list(find_state_attributes(states, ATTR_HVAC_ACTION)) + current_hvac_actions = [a for a in hvac_actions if a != HVACAction.OFF] + # return the most common action if it is not off + if current_hvac_actions: + self.attr_hvac_action = max( + set(current_hvac_actions), key=current_hvac_actions.count + ) + # return action off if all are off + elif all(a == HVACAction.OFF for a in hvac_actions): + self.attr_hvac_action = HVACAction.OFF + # else it's none + else: + self.attr_hvac_action = None + + self.async_write_ha_state() + + +def convert_inbound_states(self, entity_id, state: State) -> State: + """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") + + state.state = mode_remap(self, entity_id, str(state.state), True) + + if state.state not in (HVACMode.OFF, HVACMode.HEAT): + state.state = None + + return 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 = 5 + hvac_mode = None + + elif _has_system_mode is None: + 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 and heater should be off" + ) + _new_heating_setpoint = 5 + 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: + 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..e56b71e7 --- /dev/null +++ b/custom_components/better_thermostat/events/window.py @@ -0,0 +1,73 @@ +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"): + new_window_open = True + if new_state == "unknown": + _LOGGER.warning( + "better_thermostat %s: Window sensor state is unknown, assuming window is open", + self.name, + ) + 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: + 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() + await self.control_queue_task.put(self) + self.window_queue_task.task_done() diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json new file mode 100644 index 00000000..31178735 --- /dev/null +++ b/custom_components/better_thermostat/manifest.json @@ -0,0 +1,21 @@ +{ + "domain": "better_thermostat", + "name": "Better Thermostat", + "documentation": "https://github.com/KartoffelToby/better_thermostat", + "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", + "iot_class": "local_push", + "version": "1.0.0-beta45", + "config_flow": true, + "dependencies": [ + "climate", + "recorder" + ], + "after_dependencies": [ + "climate" + ], + "codeowners": [ + "@kartoffeltoby", + "@RubenKelevra" + ], + "requirements": [] +} \ No newline at end of file diff --git a/custom_components/better_thermostat/services.yaml b/custom_components/better_thermostat/services.yaml new file mode 100644 index 00000000..8e025205 --- /dev/null +++ b/custom_components/better_thermostat/services.yaml @@ -0,0 +1,39 @@ +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 + +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..a1293b63 --- /dev/null +++ b/custom_components/better_thermostat/strings.json @@ -0,0 +1,79 @@ +{ + "config": { + "step": { + "user": { + "description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration/new_device** ", + "data": { + "name": "Name", + "thermostat": "The real themostat", + "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/calibration** ", + "data": { + "calibration_round": "Should the calibration be rounded to the nearest", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + }, + "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 :(\nIt work anyway, but you have to create a automation to fit your specials based on the device events" + }, + "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": { + "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/calibration", + "weather": "Your weather entity to get the outdoor temperature", + "calibration_round": "Should the calibration be rounded to the nearest", + "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/calibration** ", + "data": { + "calibration_round": "Should the calibration be rounded to the nearest", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + } + } + } +} \ 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..4c5d53fd --- /dev/null +++ b/custom_components/better_thermostat/translations/da.json @@ -0,0 +1,80 @@ +{ + "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/new_device**", + "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/calibration** ", + "data": { + "calibration_round": "Skal kalibreringen afrundes til nærmeste", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + }, + "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": { + "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/calibration", + "weather": "Din vejrentitet for at få udendørstemperaturen", + "calibration_round": "Skal kalibreringen afrundes til nærmeste", + "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/calibration** ", + "data": { + "calibration_round": "Skal kalibreringen afrundes til nærmeste", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + } + } + } +} \ 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..a97caf98 --- /dev/null +++ b/custom_components/better_thermostat/translations/de.json @@ -0,0 +1,80 @@ +{ + "title": "Better Thermostat", + "config": { + "step": { + "user": { + "description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr infos: https://better-thermostat.org/configuration/new_device** ", + "data": { + "name": "Name", + "thermostat": "Das reale Thermostat", + "temperature_sensor": "Externer Temperatur Sensor", + "humidity_sensor": "Luftfeuchtigkeits Sensor", + "window_sensors": "Fenstersensor(s)", + "off_temperature": "Bei welcher außentemperatur soll das Thermostat abschalten?", + "window_off_delay": "Wie lange soll das Thermostat mit dem abschalten warten wenn ein Fenster offen ist?", + "outdoor_sensor": "Wenn ein außentemperatur sensor vorhanden ist, kann dieser anstelle der wetter entität genutz 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/calibration*** ", + "data": { + "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", + "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, muss es getauscht werden", + "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", + "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren", + "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", + "calibration": "Die art der Kalibrierung", + "fix_calibration": "Wenn dein TRV Probleme hat, die Zieltemperatur nicht zu erreichen oder zu überhitzen, kann diese Option hilfreich sein" + } + }, + "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 Kalibrations-modus:" + } + }, + "error": { + "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 trozdem funktionieren, aber du musst eine automation anlegen die den off modus abfängt." + }, + "abort": { + "single_instance_allowed": "Only a single Thermostat for each real is allowed.", + "no_devices_found": "Es konnten keine climate entitäten in Home Assistant gefunden werden, stelle sicher das dein reales Thermostat in Homeassistant vorhanden hast." + } + }, + "options": { + "step": { + "user": { + "description": "Aktuallisere die Better Thermostat einstellungen", + "data": { + "temperature_sensor": "Externer Temperatur Sensor", + "humidity_sensor": "Luftfeuchtigkeits Sensor", + "window_sensors": "Fenstersensor(s)", + "off_temperature": "Bei welcher außentemperatur soll das Thermostat abschalten?", + "window_off_delay": "Wie lange soll das Thermostat mit dem abschalten warten wenn ein Fenster offen ist?", + "outdoor_sensor": "Wenn ein außentemperatur sensor vorhanden ist, kann dieser anstelle der wetter entität genutz werden", + "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", + "calibration": "Die art der Kalibrierung https://better-thermostat.org/calibration", + "weather": "Die Wetter entität für die außentemperatur", + "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", + "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, kann es getauscht werden (für Google Home nutzer)", + "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", + "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren" + } + }, + "advanced": { + "description": "Aktuallisere die Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/calibration*** ", + "data": { + "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", + "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, muss es getauscht werden", + "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", + "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren", + "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", + "calibration": "Die art der Kalibrierung", + "fix_calibration": "Wenn dein TRV Probleme hat, die Zieltemperatur nicht zu erreichen oder zu überhitzen, kann diese Option hilfreich sein" + } + } + } + } +} \ 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..eae12029 --- /dev/null +++ b/custom_components/better_thermostat/translations/en.json @@ -0,0 +1,80 @@ +{ + "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/new_device** ", + "data": { + "name": "Name", + "thermostat": "The real themostat", + "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/calibration** ", + "data": { + "calibration_round": "Should the calibration be rounded to the nearest", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + }, + "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 :(\nIt work anyway, but you have to create a automation to fit your specials based on the device events" + }, + "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": { + "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/calibration", + "weather": "Your weather entity to get the outdoor temperature", + "calibration_round": "Should the calibration be rounded to the nearest", + "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/calibration** ", + "data": { + "calibration_round": "Should the calibration be rounded to the nearest", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + } + } + } +} \ 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..59688c5f --- /dev/null +++ b/custom_components/better_thermostat/translations/fr.json @@ -0,0 +1,80 @@ +{ + "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/new_device** ", + "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/calibration** ", + "data": { + "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + }, + "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": "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/calibration", + "weather": "Votre entité météo pour obtenir la température extérieure", + "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", + "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/calibration** ", + "data": { + "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", + "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", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + } + } + } +} \ 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..dd8edf13 --- /dev/null +++ b/custom_components/better_thermostat/translations/pl.json @@ -0,0 +1,80 @@ +{ + "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/new_device** ", + "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/calibration** ", + "data": { + "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", + "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ć", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + }, + "confirm": { + "title": "Potwierdź dodanie Better Thermostat", + "description": "Zamierzasz dodać `{name}` do Home Assistant.\nUżywając {trv} jako termostatu" + } + }, + "error": { + "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/calibration", + "weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną", + "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", + "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/calibration** ", + "data": { + "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", + "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ć", + "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + } + } + } + } +} \ 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..1b97054b --- /dev/null +++ b/custom_components/better_thermostat/utils/bridge.py @@ -0,0 +1,88 @@ +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.warning( + "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 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..2a625bec --- /dev/null +++ b/custom_components/better_thermostat/utils/controlling.py @@ -0,0 +1,237 @@ +import asyncio +import logging + +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(): + _temp = await control_trv(self, trv) + if _temp is False: + result = False + if result is False: + await self.control_queue_task.put(self) + 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 + _trv = self.hass.states.get(heater_entity_id) + _current_TRV_mode = _trv.state + _current_set_temperature = convert_to_float( + str(_trv.attributes.get("temperature", None)), self.name, "controlling()" + ) + + _hvac_mode_send = HVACMode.OFF + + _remapped_states = convert_outbound_states( + self, heater_entity_id, self.bt_hvac_mode + ) + if not isinstance(_remapped_states, dict): + await asyncio.sleep(10) + self.ignore_states = False + self.real_trvs[heater_entity_id]["ignore_trv_states"] = False + return False + _converted_hvac_mode = _remapped_states.get("system_mode", None) + _temperature = _remapped_states.get("temperature", None) + _calibration = _remapped_states.get("local_temperature_calibration", None) + + if self.call_for_heat is True: + _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 + + if ( + _calibration is not None + and self.bt_hvac_mode != HVACMode.OFF + and self.window_open is False + ): + 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, + ) + 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 + ) + + _cur_trv_temp = convert_to_float( + str(self.real_trvs[heater_entity_id]["current_temperature"]), + self.name, + "controlling()", + ) + + _calibration_delta = float( + str(format(float(abs(_cur_trv_temp - self.cur_temp)), ".1f")) + ) + + _shoud_calibrate = False + if _calibration_delta >= float(step_calibration): + _shoud_calibrate = True + + if ( + self.real_trvs[heater_entity_id]["calibration_received"] is True + and float(old) != float(_calibration) + and _shoud_calibrate is True + ): + _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 + + if _hvac_mode_send is not None: + if _hvac_mode_send != _current_TRV_mode: + _LOGGER.debug( + f"better_thermostat {self.name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_current_TRV_mode} to: {_hvac_mode_send}" + ) + self.real_trvs[heater_entity_id]["last_hvac_mode"] = _hvac_mode_send + await set_hvac_mode(self, heater_entity_id, _hvac_mode_send) + 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)) + + if _temperature is not None and self.window_open is False: + 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}" + ) + await set_temperature(self, heater_entity_id, _temperature) + self.real_trvs[heater_entity_id]["last_temperature"] = _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(checktarget_temperature(self, heater_entity_id)) + + await asyncio.sleep(3) + self.real_trvs[heater_entity_id]["ignore_trv_states"] = False + return True + + +async def check_system_mode(self, heater_entity_id=None): + """check system mode""" + _timeout = 0 + while ( + self.real_trvs[heater_entity_id]["hvac_mode"] + != self.real_trvs[heater_entity_id]["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) + self.real_trvs[heater_entity_id]["system_mode_received"] = True + return True + + +async def checktarget_temperature(self, heater_entity_id=None): + """Check if target temperature is reached.""" + _timeout = 0 + while True: + _current_set_temperature = convert_to_float( + str( + self.hass.states.get(heater_entity_id).attributes.get( + "temperature", None + ) + ), + self.name, + "check_target()", + ) + if ( + self.real_trvs[heater_entity_id]["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) + self.real_trvs[heater_entity_id]["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..03766e52 --- /dev/null +++ b/custom_components/better_thermostat/utils/helpers.py @@ -0,0 +1,499 @@ +"""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 ..const import CONF_HEAT_AUTO_SWAPPED + + +_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()" + + _current_trv_calibration = convert_to_float( + str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context + ) + _cur_trv_temp = convert_to_float( + str(self.real_trvs[entity_id]["current_temperature"]), 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 + + if self.real_trvs[entity_id]["advanced"].get("fix_calibration", False) is True: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.2 and _temp_diff < 1: + _cur_trv_temp = round_to_half_degree(_cur_trv_temp) + _cur_trv_temp += 0.5 + if _temp_diff >= 1.2: + _cur_trv_temp = round_to_half_degree(_cur_trv_temp) + _cur_trv_temp += 2.5 + if _temp_diff > -0.2 and _temp_diff < 0: + _cur_trv_temp = round_down_to_half_degree(_cur_trv_temp) + _cur_trv_temp -= 0.5 + if _temp_diff >= -1.2 and _temp_diff < 0: + _cur_trv_temp = round_down_to_half_degree(_cur_trv_temp) + _cur_trv_temp -= 2.5 + + _new_local_calibration = (self.cur_temp - _cur_trv_temp) + _current_trv_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 + """ + _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 + + if self.real_trvs[entity_id]["advanced"].get("fix_calibration", False) is True: + _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp)) + if _temp_diff > 0.3 and _calibrated_setpoint - _cur_trv_temp < 2.5: + _calibrated_setpoint += 2.5 + + # 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 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 value + elif value is None or value == "None": + return None + else: + try: + return float(str(format(float(value), ".1f"))) + 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 > 7: + 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) + 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_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) + 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/weather.py b/custom_components/better_thermostat/utils/weather.py new file mode 100644 index 00000000..5bb65971 --- /dev/null +++ b/custom_components/better_thermostat/utils/weather.py @@ -0,0 +1,185 @@ +import logging +from datetime import datetime, timedelta +import homeassistant.util.dt as dt_util +from homeassistant.components.recorder import get_instance, history + +# 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 + +_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 + + if self.weather_entity is not None: + self.call_for_heat = check_weather_prediction(self) + + elif self.outdoor_sensor is not None: + if None in (self.last_avg_outdoor_temp, self.off_temperature): + self.call_for_heat = False + return False + self.call_for_heat = self.last_avg_outdoor_temp < self.off_temperature + else: + self.call_for_heat = 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: + forcast = self.hass.states.get(self.weather_entity).attributes.get("forecast") + if len(forcast) > 0: + max_forcast_temp = int( + round( + ( + convert_to_float( + str(forcast[0]["temperature"]), + self.name, + "check_weather_prediction()", + ) + + convert_to_float( + str(forcast[1]["temperature"]), + self.name, + "check_weather_prediction()", + ) + ) + / 2 + ) + ) + return max_forcast_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 + + last_two_days_date_time = datetime.now() - timedelta(days=2) + start = dt_util.as_utc(last_two_days_date_time) + history_list = await get_instance(self.hass).async_add_executor_job( + history.state_changes_during_period, + self.hass, + start, + dt_util.as_utc(datetime.now()), + str(self.outdoor_sensor), + ) + historic_sensor_data = history_list.get(self.outdoor_sensor) + # create a list from valid data in historic_sensor_data + valid_historic_sensor_data = [] + invalid_sensor_data_count = 0 + if historic_sensor_data is not None: + _LOGGER.warning( + f"better_thermostat {self.name}: {self.outdoor_sensor} has no historic data." + ) + return convert_to_float( + self.hass.states.get(self.outdoor_sensor).state, + self.name, + "check_ambient_air_temperature()", + ) + for measurement in historic_sensor_data: + if isinstance( + measurement := convert_to_float( + str(measurement.state), self.name, "check_ambient_air_temperature()" + ), + float, + ): + valid_historic_sensor_data.append(measurement) + else: + invalid_sensor_data_count += 1 + + if len(valid_historic_sensor_data) == 0: + _LOGGER.warning( + f"better_thermostat {self.name}: no valid outdoor sensor data found." + ) + return None + + if invalid_sensor_data_count: + _LOGGER.debug( + f"better_thermostat {self.name}: ignored {invalid_sensor_data_count} invalid outdoor sensor data entries." + ) + + # remove the upper and lower 5% of the data + valid_historic_sensor_data.sort() + valid_historic_sensor_data = valid_historic_sensor_data[ + int(round(len(valid_historic_sensor_data) * 0.05)) : int( + round(len(valid_historic_sensor_data) * 0.95) + ) + ] + + if len(valid_historic_sensor_data) == 0: + _LOGGER.warning( + f"better_thermostat {self.name}: no valid outdoor sensor data found." + ) + return None + + _LOGGER.debug( + f"better_thermostat {self.name}: check_ambient_air_temperature is evaluating {len(valid_historic_sensor_data)} sensor values." + ) + + # calculate the average temperature + avg_temp = int( + round(sum(valid_historic_sensor_data) / len(valid_historic_sensor_data)) + ) + _LOGGER.debug( + f"better_thermostat {self.name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}" + ) + + self.last_avg_outdoor_temp = avg_temp From ead301cc702ff42586abcdead309ad82d431b6f0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 22 Nov 2022 00:20:54 +0000 Subject: [PATCH 004/158] Initial guest bedroom heating control --- packages/guest_bedroom.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/guest_bedroom.yaml diff --git a/packages/guest_bedroom.yaml b/packages/guest_bedroom.yaml new file mode 100644 index 00000000..eb4e11fa --- /dev/null +++ b/packages/guest_bedroom.yaml @@ -0,0 +1,25 @@ +--- +# 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) }}" + 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) }}" + action: + - service: homeassistant.turn_off + entity_id: input_boolean.call_for_guest_heat + From a886b097bef697751ba3e267c87d4a8499ff23b5 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 22 Nov 2022 22:02:08 +0000 Subject: [PATCH 005/158] Include heating on/off --- packages/guest_bedroom.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/guest_bedroom.yaml b/packages/guest_bedroom.yaml index eb4e11fa..12e984ff 100644 --- a/packages/guest_bedroom.yaml +++ b/packages/guest_bedroom.yaml @@ -9,6 +9,9 @@ automation: 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 @@ -19,6 +22,8 @@ automation: 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 From dd8dd3447755f3a2124079ffc0ad636970c7e152 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 22 Nov 2022 22:10:20 +0000 Subject: [PATCH 006/158] Remove guest bedroom from average --- sensors.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/sensors.yaml b/sensors.yaml index 1b7d7fae..af4b2a0d 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -85,7 +85,6 @@ entity_ids: - sensor.kitchen_temperature - sensor.craft_room_temperature - - sensor.guest_room_temperature - sensor.living_room_temperature - sensor.master_bedroom_temperature - sensor.nook_temperature From cff3d03eec2b5bafb7200a36b562e1b82004dd28 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 22 Nov 2022 22:13:25 +0000 Subject: [PATCH 007/158] Formatting --- packages/guest_bedroom.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/guest_bedroom.yaml b/packages/guest_bedroom.yaml index 12e984ff..f430a2d1 100644 --- a/packages/guest_bedroom.yaml +++ b/packages/guest_bedroom.yaml @@ -1,5 +1,5 @@ --- -# The input_boolean.call_for_guest_heat is provided at the upper level climate +# The input_boolean.call_for_guest_heat is provided at the upper level climate # package, in /packages/climate.yaml automation: @@ -27,4 +27,3 @@ automation: action: - service: homeassistant.turn_off entity_id: input_boolean.call_for_guest_heat - From 0432685c807b3a7871b19f1848f9315c5d3bd083 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 2 Dec 2022 00:04:50 +0000 Subject: [PATCH 008/158] Make yamllint happy --- packages/overflights.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/overflights.yaml b/packages/overflights.yaml index aebdb74b..5a246f2f 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -1,3 +1,4 @@ +--- sensor: - platform: opensky name: opensky_home From 0839ea6b2672a932aa2046fce3d8b8327184e1ab Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 2 Dec 2022 00:11:27 +0000 Subject: [PATCH 009/158] Fake secrets --- travis_secrets.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/travis_secrets.yaml b/travis_secrets.yaml index 3d661779..40cfdfc2 100644 --- a/travis_secrets.yaml +++ b/travis_secrets.yaml @@ -41,3 +41,6 @@ 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 From eb51aa1b85869ef760f37c1b11f739c668cea564 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 2 Dec 2022 00:11:58 +0000 Subject: [PATCH 010/158] Mastodon notifier --- notify.yaml | 57 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/notify.yaml b/notify.yaml index aa164192..3d0300eb 100644 --- a/notify.yaml +++ b/notify.yaml @@ -1,25 +1,32 @@ - - 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: 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: 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: 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 From 411af7f4d4863ad120f193792a1545db8322325a Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 2 Dec 2022 00:15:10 +0000 Subject: [PATCH 011/158] Add mastodon to action list --- packages/overflights.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/overflights.yaml b/packages/overflights.yaml index 5a246f2f..af893fef 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -47,3 +47,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.mastodon_overuplawmoor + data_template: + 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 }}' From d8df5c257533ad27cb0ac8bf587d05d6183241d5 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 7 Dec 2022 16:31:31 +0000 Subject: [PATCH 012/158] Sensor beginnings --- packages/overflights.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/overflights.yaml b/packages/overflights.yaml index af893fef..d865ce91 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -11,6 +11,12 @@ sensor: latitude: 55.765 longitude: -4.496 + - platform: mqtt + 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' trigger: From 646240fdc5c63e270cd4f1a7f249ade413335b7d Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 4 Jan 2023 17:40:37 +0000 Subject: [PATCH 013/158] Bump to 1.0.0-beta55 --- .../better_thermostat/__init__.py | 34 ++- .../better_thermostat/adapters/deconz.py | 10 + .../better_thermostat/adapters/generic.py | 10 + .../better_thermostat/adapters/mqtt.py | 27 ++- .../better_thermostat/adapters/tado.py | 10 + .../better_thermostat/climate.py | 175 ++++++++++++-- .../better_thermostat/config_flow.py | 216 +++++++++++++----- custom_components/better_thermostat/const.py | 25 +- .../better_thermostat/events/temperature.py | 18 +- .../better_thermostat/events/trv.py | 193 +++++++++------- .../better_thermostat/events/window.py | 19 +- .../better_thermostat/manifest.json | 4 +- .../SEA801-Zigbee_SEA802-Zigbee.py | 30 +++ .../better_thermostat/model_fixes/SPZB0001.py | 18 ++ .../better_thermostat/model_fixes/TS0601.py | 32 +++ .../model_fixes/TS0601_thermostat.py | 32 +++ .../model_fixes/TV02-Zigbee.py | 90 ++++++++ .../better_thermostat/model_fixes/default.py | 14 ++ .../better_thermostat/services.yaml | 9 + .../better_thermostat/strings.json | 26 ++- .../better_thermostat/translations/de.json | 92 ++++---- .../better_thermostat/translations/en.json | 26 ++- .../better_thermostat/utils/bridge.py | 10 + .../better_thermostat/utils/controlling.py | 158 +++++++------ .../better_thermostat/utils/helpers.py | 168 ++++++++++++-- .../better_thermostat/utils/model_quirks.py | 55 +++++ .../better_thermostat/utils/weather.py | 195 ++++++++++------ 27 files changed, 1312 insertions(+), 384 deletions(-) create mode 100644 custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py create mode 100644 custom_components/better_thermostat/model_fixes/SPZB0001.py create mode 100644 custom_components/better_thermostat/model_fixes/TS0601.py create mode 100644 custom_components/better_thermostat/model_fixes/TS0601_thermostat.py create mode 100644 custom_components/better_thermostat/model_fixes/TV02-Zigbee.py create mode 100644 custom_components/better_thermostat/model_fixes/default.py create mode 100644 custom_components/better_thermostat/utils/model_quirks.py diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py index 9a3bf07c..23f9bf88 100644 --- a/custom_components/better_thermostat/__init__.py +++ b/custom_components/better_thermostat/__init__.py @@ -4,7 +4,13 @@ from homeassistant.core import HomeAssistant, Config from homeassistant.config_entries import ConfigEntry -from .const import CONF_FIX_CALIBRATION, CONF_HEATER +from .const import ( + CONF_FIX_CALIBRATION, + CONF_CALIBRATION_MODE, + CONF_HEATER, + CONF_NO_SYSTEM_MODE_OFF, + CONF_WINDOW_TIMEOUT, +) _LOGGER = logging.getLogger(__name__) @@ -51,6 +57,32 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): 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 index 1dda35a6..6cc07f17 100644 --- a/custom_components/better_thermostat/adapters/deconz.py +++ b/custom_components/better_thermostat/adapters/deconz.py @@ -39,6 +39,16 @@ async def get_offset_steps(self, entity_id): 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( diff --git a/custom_components/better_thermostat/adapters/generic.py b/custom_components/better_thermostat/adapters/generic.py index a8ac0b21..ded8bd61 100644 --- a/custom_components/better_thermostat/adapters/generic.py +++ b/custom_components/better_thermostat/adapters/generic.py @@ -22,6 +22,16 @@ async def get_offset_steps(self, entity_id): 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( diff --git a/custom_components/better_thermostat/adapters/mqtt.py b/custom_components/better_thermostat/adapters/mqtt.py index 3d582e90..bbc20c6a 100644 --- a/custom_components/better_thermostat/adapters/mqtt.py +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -102,23 +102,37 @@ async def get_offset_steps(self, entity_id): ) -async def set_offset(self, entity_id, offset): - """Set new target offset.""" - max_calibration = float( +async def get_min_offset(self, entity_id): + """Get min offset.""" + # looks like z2m has a min max bug currently force to -10 + return -6.0 + return float( str( self.hass.states.get( self.real_trvs[entity_id]["local_temperature_calibration_entity"] - ).attributes.get("max", 127) + ).attributes.get("min", -10) ) ) - min_calibration = float( + + +async def get_max_offset(self, entity_id): + """Get max offset.""" + # looks like z2m has a min max bug currently force to 10 + return 6.0 + return float( str( self.hass.states.get( self.real_trvs[entity_id]["local_temperature_calibration_entity"] - ).attributes.get("min", -128) + ).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: @@ -142,6 +156,7 @@ async def set_offset(self, entity_id, offset): 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"] ) diff --git a/custom_components/better_thermostat/adapters/tado.py b/custom_components/better_thermostat/adapters/tado.py index 5bb30506..eb2505e4 100644 --- a/custom_components/better_thermostat/adapters/tado.py +++ b/custom_components/better_thermostat/adapters/tado.py @@ -38,6 +38,16 @@ async def get_offset_steps(self, entity_id): 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: diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index 07fc2e09..b941c0b1 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -8,7 +8,16 @@ from statistics import mean from .utils.weather import check_ambient_air_temperature, check_weather -from .utils.bridge import get_current_offset, init, load_adapter +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 from homeassistant.helpers import entity_platform from homeassistant.core import callback, CoreState @@ -45,6 +54,7 @@ ATTR_STATE_MAIN_MODE, ATTR_STATE_WINDOW_OPEN, ATTR_STATE_SAVED_TEMPERATURE, + ATTR_STATE_HEATING_POWER, CONF_HEATER, CONF_HUMIDITY, CONF_MODEL, @@ -58,13 +68,14 @@ 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, update_hvac_action +from .events.trv import trigger_trv_change from .events.window import trigger_window_change, window_queue _LOGGER = logging.getLogger(__name__) @@ -79,6 +90,8 @@ async def async_service_handler(self, data): 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( @@ -93,6 +106,9 @@ async def async_service_handler(self, data): 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( [ @@ -147,6 +163,10 @@ async def restore_temp_temperature(self): 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 { @@ -227,13 +247,22 @@ def __init__( 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.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. @@ -256,10 +285,12 @@ async def async_added_to_hass(self): 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, @@ -272,6 +303,8 @@ async def async_added_to_hass(self): "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, @@ -349,7 +382,6 @@ async def _trigger_trv_change(self, event): return self.hass.async_create_task(trigger_trv_change(self, event)) - # await trigger_trv_change(self, event) async def _trigger_window_change(self, event): self.async_set_context(event.context) @@ -467,7 +499,7 @@ async def startup(self): self.cur_humidity = convert_to_float( str(self.hass.states.get(self.humidity_entity_id).state), self.name, - "startuo()", + "startup()", ) if self.window_id is not None: window = self.hass.states.get(self.window_id) @@ -524,7 +556,7 @@ async def startup(self): self.bt_target_temp = convert_to_float( str(_oldtarget_temperature), self.name, "startup()" ) - if not self.bt_hvac_mode and old_state.state: + 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( @@ -547,6 +579,10 @@ async def startup(self): 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 @@ -561,11 +597,37 @@ async def startup(self): # if hvac mode could not be restored, turn heat off if self.bt_hvac_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): - _LOGGER.warning( - "better_thermostat %s: No previously hvac mode found on startup, turn heat off", - self.name, - ) - self.bt_hvac_mode = HVACMode.OFF + 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", @@ -598,6 +660,12 @@ async def startup(self): 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 @@ -634,7 +702,7 @@ async def startup(self): 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"] = None + 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, @@ -655,9 +723,9 @@ async def startup(self): self.startup_running = False self._available = True self.async_write_ha_state() - # await self.async_update_ha_state() - update_hvac_action(self) + # await asyncio.sleep(5) + # update_hvac_action(self) # Add listener if self.outdoor_sensor is not None: self.async_on_remove( @@ -695,8 +763,76 @@ async def startup(self): ) ) _LOGGER.info("better_thermostat %s: startup completed.", self.name) + self.async_write_ha_state() + await self.async_update_ha_state() 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. @@ -713,6 +849,7 @@ def extra_state_attributes(self): 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, } return dev_specific @@ -822,11 +959,15 @@ def hvac_mode(self): @property def hvac_action(self): """Return the current HVAC action""" - if self.attr_hvac_action is None: - if self.bt_hvac_mode == HVACMode.HEAT: - return HVACAction.HEATING + 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: - return HVACAction.IDLE + self.attr_hvac_action = HVACAction.IDLE return self.attr_hvac_action @property diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py index 7038ce5f..2526168a 100644 --- a/custom_components/better_thermostat/config_flow.py +++ b/custom_components/better_thermostat/config_flow.py @@ -1,6 +1,4 @@ import logging - - import voluptuous as vol from collections import OrderedDict @@ -9,15 +7,15 @@ from .utils.helpers import get_device_model, get_trv_intigration from .const import ( - CONF_CALIBRATIION_ROUND, + CONF_PROTECT_OVERHEATING, CONF_CALIBRATION, CONF_CHILD_LOCK, - CONF_FIX_CALIBRATION, 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, @@ -25,21 +23,67 @@ 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" + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) +) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - VERSION = 2 + VERSION = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): @@ -108,46 +152,56 @@ async def async_step_advanced(self, user_input=None, _trv_config=None): fields = OrderedDict() - _calibration = {"target_temp_based": "Target Temperature"} _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): - _calibration["local_calibration_based"] = "Local Calibration" _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, - default=user_input.get(CONF_CALIBRATION, _default_calibration), + CONF_CALIBRATION_MODE, + default=user_input.get( + CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION + ), ) - ] = vol.In(_calibration) - - has_auto = False - trv = self.hass.states.get(_trv_config.get("trv")) - if HVACMode.AUTO in trv.attributes.get("hvac_modes"): - has_auto = True + ] = CALIBRATION_MODE_SELECTOR fields[ vol.Optional( - CONF_HEAT_AUTO_SWAPPED, - default=user_input.get(CONF_HEAT_AUTO_SWAPPED, has_auto), + CONF_PROTECT_OVERHEATING, + default=user_input.get(CONF_PROTECT_OVERHEATING, False), ) ] = bool fields[ vol.Optional( - CONF_FIX_CALIBRATION, - default=user_input.get(CONF_FIX_CALIBRATION, False), + CONF_NO_SYSTEM_MODE_OFF, + default=user_input.get(CONF_NO_SYSTEM_MODE_OFF, False), ) ] = bool fields[ vol.Optional( - CONF_CALIBRATIION_ROUND, - default=user_input.get(CONF_CALIBRATIION_ROUND, True), + CONF_HEAT_AUTO_SWAPPED, + default=user_input.get(CONF_HEAT_AUTO_SWAPPED, False), ) ] = bool @@ -194,6 +248,19 @@ async def async_step_user(self, user_input=None): 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) @@ -205,7 +272,6 @@ async def async_step_user(self, user_input=None): "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]) @@ -254,10 +320,7 @@ async def async_step_user(self, user_input=None): vol.Optional(CONF_WEATHER): selector.EntitySelector( selector.EntitySelectorConfig(domain="weather", multiple=False) ), - vol.Optional( - CONF_WINDOW_TIMEOUT, - default=user_input.get(CONF_WINDOW_TIMEOUT, 0), - ): int, + vol.Optional(CONF_WINDOW_TIMEOUT): selector.DurationSelector(), vol.Optional( CONF_OFF_TEMPERATURE, default=user_input.get(CONF_OFF_TEMPERATURE, 20), @@ -306,8 +369,12 @@ async def async_step_advanced( 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="", data=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 @@ -316,48 +383,75 @@ async def async_step_advanced( fields = OrderedDict() - _calibration = {"target_temp_based": "Target Temperature"} _default_calibration = "target_temp_based" - _adapter = _trv_config.get("adapter", None) + 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): - _calibration["local_calibration_based"] = "Local Calibration" _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, + CONF_CALIBRATION_MODE, default=_trv_config["advanced"].get( - CONF_CALIBRATION, _default_calibration + CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION ), ) - ] = vol.In(_calibration) - - has_auto = False - trv = self.hass.states.get(_trv_config.get("trv")) - if HVACMode.AUTO in trv.attributes.get("hvac_modes"): - has_auto = True + ] = CALIBRATION_MODE_SELECTOR fields[ vol.Optional( - CONF_HEAT_AUTO_SWAPPED, - default=_trv_config["advanced"].get(CONF_HEAT_AUTO_SWAPPED, has_auto), + CONF_PROTECT_OVERHEATING, + default=_trv_config["advanced"].get(CONF_PROTECT_OVERHEATING, False), ) ] = bool fields[ vol.Optional( - CONF_FIX_CALIBRATION, - default=_trv_config["advanced"].get(CONF_FIX_CALIBRATION, False), + 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_CALIBRATIION_ROUND, - default=_trv_config["advanced"].get(CONF_CALIBRATIION_ROUND, True), + CONF_HEAT_AUTO_SWAPPED, + default=_trv_config["advanced"].get(CONF_HEAT_AUTO_SWAPPED, has_auto), ) ] = bool @@ -403,15 +497,25 @@ async def async_step_user(self, user_input=None): CONF_OUTDOOR_SENSOR, None ) self.updated_config[CONF_WEATHER] = user_input.get(CONF_WEATHER, None) - self.updated_config[CONF_WINDOW_TIMEOUT] = user_input.get( - CONF_WINDOW_TIMEOUT - ) + + 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 ) - self.name = user_input.get(CONF_NAME, "-") + for trv in self.updated_config[CONF_HEATER]: - trv["adapter"] = load_adapter(self, trv["integration"], trv["trv"]) + trv["adapter"] = None self.trv_bundle.append(trv) return await self.async_step_advanced( @@ -494,12 +598,20 @@ async def async_step_user(self, user_input=None): 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=self.config_entry.data.get(CONF_WINDOW_TIMEOUT, 30), + default=_timeout, + description={"suggested_value": _timeout}, ) - ] = int + ] = selector.DurationSelector() fields[ vol.Optional( diff --git a/custom_components/better_thermostat/const.py b/custom_components/better_thermostat/const.py index fc3714bf..928c19a5 100644 --- a/custom_components/better_thermostat/const.py +++ b/custom_components/better_thermostat/const.py @@ -1,6 +1,8 @@ """""" 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 @@ -38,12 +40,15 @@ CONF_PRECISION = "precision" CONF_CALIBRATION = "calibration" CONF_CHILD_LOCK = "child_lock" -CONF_CALIBRATIION_ROUND = "calibration_round" +CONF_PROTECT_OVERHEATING = "protect_overheating" +CONF_CALIBRATION_MODE = "calibration_mode" CONF_FIX_CALIBRATION = "fix_calibration" +CONF_HEATING_POWER_CALIBRATION = "heating_power_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" @@ -53,9 +58,12 @@ 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" 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), @@ -70,3 +78,18 @@ class BetterThermostatEntityFeature(IntEnum): 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" diff --git a/custom_components/better_thermostat/events/temperature.py b/custom_components/better_thermostat/events/temperature.py index 3296bcc9..323fcee9 100644 --- a/custom_components/better_thermostat/events/temperature.py +++ b/custom_components/better_thermostat/events/temperature.py @@ -1,4 +1,6 @@ import logging + +from custom_components.better_thermostat.const import CONF_HOMATICIP from ..utils.helpers import convert_to_float from datetime import datetime @@ -25,6 +27,7 @@ async def trigger_temperature_change(self, event): """ 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 @@ -33,7 +36,20 @@ async def trigger_temperature_change(self, event): str(new_state.state), self.name, "external_temperature" ) - if _incoming_temperature != self.cur_temp: + _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, diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index 962ae7e7..a828a095 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -1,6 +1,7 @@ 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, @@ -23,29 +24,16 @@ @callback async def trigger_trv_change(self, event): - """Processes TRV status updates - - Parameters - ---------- - self : - self instance of better_thermostat - event : - Event object from the eventbus. Contains the new and old state from the TRV. - - Returns - ------- - None - """ + """Trigger a change in the trv state.""" if self.startup_running: return - - entity_id = event.data.get("entity_id") - - child_lock = self.real_trvs[entity_id]["advanced"].get("child_lock") - + if self.control_queue_task is None: + return + update_hvac_action(self) + _main_change = False old_state = event.data.get("old_state") new_state = event.data.get("new_state") - _org_trv_state = self.hass.states.get(entity_id).state + entity_id = event.data.get("entity_id") if None in (new_state, old_state, new_state.attributes): _LOGGER.debug( @@ -59,67 +47,103 @@ async def trigger_trv_change(self, event): ) return - try: - new_state = convert_inbound_states(self, entity_id, new_state) - except TypeError: - _LOGGER.debug( - f"better_thermostat {self.name}: remapping TRV {entity_id} state failed, skipping" - ) - return + # if new_state == old_state: + # return + + _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(new_state.attributes.get("current_temperature", None)), + 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 + ) ): _old_temp = self.real_trvs[entity_id]["current_temperature"] - if self.real_trvs[entity_id]["calibration_received"] is False: - 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() + 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 is True: - self.async_write_ha_state() + if self.ignore_states: return - new_decoded_system_mode = str(new_state.state) + 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 new_decoded_system_mode in (HVACMode.OFF, HVACMode.HEAT): - if self.real_trvs[entity_id]["hvac_mode"] != _org_trv_state and not child_lock: + 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} - converted {new_decoded_system_mode}" + 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 + 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 + and self.real_trvs[entity_id]["last_hvac_mode"] != _org_trv_state.state ): - self.bt_hvac_mode = new_decoded_system_mode + 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 self.bt_hvac_mode is not HVACMode.OFF: + 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 @@ -135,6 +159,7 @@ async def trigger_trv_change(self, event): 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 @@ -145,22 +170,21 @@ async def trigger_trv_change(self, event): _LOGGER.debug( f"better_thermostat {self.name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}" ) - if ( - child_lock is False - and self.real_trvs[entity_id]["target_temp_received"] is True - ): - self.bt_target_temp = _new_heating_setpoint + self.bt_target_temp = _new_heating_setpoint + _main_change = True - if ( - self.bt_hvac_mode == HVACMode.OFF - and self.real_trvs[entity_id]["hvac_mode"] == HVACMode.OFF - ): - self.async_write_ha_state() - return + 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() - update_hvac_action(self) - return await self.control_queue_task.put(self) + return def update_hvac_action(self): @@ -176,29 +200,35 @@ def update_hvac_action(self): pi_heating_demands = list(find_state_attributes(states, "pi_heating_demand")) if pi_heating_demands: pi_heating_demand = max(pi_heating_demands) - if pi_heating_demand > 0: + if pi_heating_demand > 1: self.attr_hvac_action = HVACAction.HEATING self.async_write_ha_state() return + else: + self.attr_hvac_action = HVACAction.IDLE + self.async_write_ha_state() + return hvac_actions = list(find_state_attributes(states, ATTR_HVAC_ACTION)) - current_hvac_actions = [a for a in hvac_actions if a != HVACAction.OFF] - # return the most common action if it is not off - if current_hvac_actions: - self.attr_hvac_action = max( - set(current_hvac_actions), key=current_hvac_actions.count - ) + if not hvac_actions: + self.attr_hvac_action = None + self.async_write_ha_state() + return + # return action off if all are off - elif all(a == HVACAction.OFF for a in hvac_actions): + if all(a == HVACAction.OFF for a in hvac_actions): self.attr_hvac_action = HVACAction.OFF - # else it's none + # else check if is heating + elif self.bt_target_temp > self.cur_temp: + self.attr_hvac_action = HVACAction.HEATING else: - self.attr_hvac_action = None + self.attr_hvac_action = HVACAction.IDLE self.async_write_ha_state() + return -def convert_inbound_states(self, entity_id, state: State) -> State: +def convert_inbound_states(self, entity_id, state: State) -> str: """Convert hvac mode in a thermostat state from HA Parameters ---------- @@ -217,12 +247,11 @@ def convert_inbound_states(self, entity_id, state: State) -> State: if state.attributes is None or state.state is None: raise TypeError("convert_inbound_states() received None state, cannot convert") - state.state = mode_remap(self, entity_id, str(state.state), True) - - if state.state not in (HVACMode.OFF, HVACMode.HEAT): - state.state = None + remapped_state = mode_remap(self, entity_id, str(state.state), True) - return state + 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]: @@ -319,16 +348,21 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: 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 = 5 + _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] hvac_mode = None - - elif _has_system_mode is 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 and heater should be off" + 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 = 5 - hvac_mode = None + _new_heating_setpoint = self.real_trvs[entity_id]["min_temp"] + hvac_mode = None return { "temperature": _new_heating_setpoint, @@ -336,5 +370,6 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: "system_mode": hvac_mode, "local_temperature_calibration": _new_local_calibration, } - except Exception: + 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 index e56b71e7..5c2b9650 100644 --- a/custom_components/better_thermostat/events/window.py +++ b/custom_components/better_thermostat/events/window.py @@ -39,6 +39,10 @@ async def trigger_window_change(self, event) -> None: "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: @@ -60,7 +64,12 @@ 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: - await asyncio.sleep(self.window_delay) + 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: @@ -69,5 +78,13 @@ async def window_queue(self): 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 index 31178735..bb5274f5 100644 --- a/custom_components/better_thermostat/manifest.json +++ b/custom_components/better_thermostat/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://github.com/KartoffelToby/better_thermostat", "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", "iot_class": "local_push", - "version": "1.0.0-beta45", + "version": "1.0.0-beta55", "config_flow": true, "dependencies": [ "climate", @@ -18,4 +18,4 @@ "@RubenKelevra" ], "requirements": [] -} \ No newline at end of file +} 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..2d473f28 --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py @@ -0,0 +1,30 @@ +def fix_local_calibration(self, entity_id, offset): + # device SEA802 fix + if (self.cur_temp - self.bt_target_temp) < -0.2: + offset -= 2.5 + elif (self.cur_temp + 0.5) > self.bt_target_temp: + offset = round(offset + 0.5, 1) + 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..f6f2ab4e --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/SPZB0001.py @@ -0,0 +1,18 @@ +def fix_local_calibration(self, entity_id, offset): + if (self.cur_temp + 0.5) > self.bt_target_temp: + offset += 3 + return offset + + +def fix_target_temperature_calibration(self, entity_id, temperature): + if (self.cur_temp + 0.5) > self.bt_target_temp: + temperature -= 3 + 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..d7c3ff5a --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TS0601.py @@ -0,0 +1,32 @@ +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) + if (self.cur_temp + 0.5) > self.bt_target_temp: + offset += 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 + if (self.cur_temp + 0.5) > self.bt_target_temp: + temperature -= 2 + 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..d7c3ff5a --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py @@ -0,0 +1,32 @@ +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) + if (self.cur_temp + 0.5) > self.bt_target_temp: + offset += 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 + if (self.cur_temp + 0.5) > self.bt_target_temp: + temperature -= 2 + 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..5107bf0e --- /dev/null +++ b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py @@ -0,0 +1,90 @@ +# 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, + limit=None, + 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, + limit=None, + 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, + limit=None, + context=self._context, + ) + + await self.hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": temperature}, + blocking=True, + limit=None, + 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 index 8e025205..9fa995b2 100644 --- a/custom_components/better_thermostat/services.yaml +++ b/custom_components/better_thermostat/services.yaml @@ -16,6 +16,15 @@ restore_saved_target_temperature: 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: diff --git a/custom_components/better_thermostat/strings.json b/custom_components/better_thermostat/strings.json index a1293b63..7c9b8f2a 100644 --- a/custom_components/better_thermostat/strings.json +++ b/custom_components/better_thermostat/strings.json @@ -18,13 +18,19 @@ "advanced": { "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", "data": { - "calibration_round": "Should the calibration be rounded to the nearest", + "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + "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": { @@ -35,7 +41,7 @@ "error": { "failed": "something went wrong.", "no_name": "Please enter a name.", - "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" + "no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead." }, "abort": { "single_instance_allowed": "Only a single Thermostat for each real is allowed.", @@ -65,13 +71,19 @@ "advanced": { "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", "data": { - "calibration_round": "Should the calibration be rounded to the nearest", + "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + "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." } } } diff --git a/custom_components/better_thermostat/translations/de.json b/custom_components/better_thermostat/translations/de.json index a97caf98..56870483 100644 --- a/custom_components/better_thermostat/translations/de.json +++ b/custom_components/better_thermostat/translations/de.json @@ -3,78 +3,86 @@ "config": { "step": { "user": { - "description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr infos: https://better-thermostat.org/configuration/new_device** ", + "description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr Informationen: https://better-thermostat.org/configuration/new_device** ", "data": { "name": "Name", "thermostat": "Das reale Thermostat", - "temperature_sensor": "Externer Temperatur Sensor", - "humidity_sensor": "Luftfeuchtigkeits Sensor", - "window_sensors": "Fenstersensor(s)", - "off_temperature": "Bei welcher außentemperatur soll das Thermostat abschalten?", - "window_off_delay": "Wie lange soll das Thermostat mit dem abschalten warten wenn ein Fenster offen ist?", - "outdoor_sensor": "Wenn ein außentemperatur sensor vorhanden ist, kann dieser anstelle der wetter entität genutz werden", - "weather": "Die Wetter entität für die außentemperatur" + "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/calibration*** ", "data": { - "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", - "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, muss es getauscht werden", - "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", - "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren", - "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", - "calibration": "Die art der Kalibrierung", - "fix_calibration": "Wenn dein TRV Probleme hat, die Zieltemperatur nicht zu erreichen oder zu überhitzen, kann diese Option hilfreich sein" + "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 Kalibrations-modus:" + "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": { "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 trozdem funktionieren, aber du musst eine automation anlegen die den off modus abfängt." + "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": "Only a single Thermostat for each real is allowed.", - "no_devices_found": "Es konnten keine climate entitäten in Home Assistant gefunden werden, stelle sicher das dein reales Thermostat in Homeassistant vorhanden hast." + "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": "Aktuallisere die Better Thermostat einstellungen", + "description": "Aktualisiere die Better Thermostat Einstellungen", "data": { - "temperature_sensor": "Externer Temperatur Sensor", - "humidity_sensor": "Luftfeuchtigkeits Sensor", - "window_sensors": "Fenstersensor(s)", - "off_temperature": "Bei welcher außentemperatur soll das Thermostat abschalten?", - "window_off_delay": "Wie lange soll das Thermostat mit dem abschalten warten wenn ein Fenster offen ist?", - "outdoor_sensor": "Wenn ein außentemperatur sensor vorhanden ist, kann dieser anstelle der wetter entität genutz werden", - "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", - "calibration": "Die art der Kalibrierung https://better-thermostat.org/calibration", - "weather": "Die Wetter entität für die außentemperatur", - "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", - "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, kann es getauscht werden (für Google Home nutzer)", - "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", - "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren" + "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." } }, "advanced": { "description": "Aktuallisere die Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/calibration*** ", "data": { - "calibration_round": "Soll bei der Kalibrierung auf ganze stellen gerundet werden?", - "heat_auto_swapped": "Wenn das Thermostat den modus auto als heizen und heizen als boost benutzt, muss es getauscht werden", - "child_lock": "Ignoriere alle manuellen einstellungen am realen Thermostat (Kindersicherung)", - "homaticip": "Wenn du HomaticIP nuzt, solltest du diese Option aktivieren um die Funk-übertragung zu reduzieren", - "valve_maintenance": "Soll BT die wartung des Thermostats übernehmen?", - "calibration": "Die art der Kalibrierung", - "fix_calibration": "Wenn dein TRV Probleme hat, die Zieltemperatur nicht zu erreichen oder zu überhitzen, kann diese Option hilfreich sein" + "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)" } } } } -} \ No newline at end of file +} diff --git a/custom_components/better_thermostat/translations/en.json b/custom_components/better_thermostat/translations/en.json index eae12029..0de859de 100644 --- a/custom_components/better_thermostat/translations/en.json +++ b/custom_components/better_thermostat/translations/en.json @@ -6,7 +6,7 @@ "description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration/new_device** ", "data": { "name": "Name", - "thermostat": "The real themostat", + "thermostat": "The real thermostat", "temperature_sensor": "Temperature sensor", "humidity_sensor": "Humidity sensor", "window_sensors": "Window sensor", @@ -19,13 +19,19 @@ "advanced": { "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", "data": { - "calibration_round": "Should the calibration be rounded to the nearest", + "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + "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": { @@ -36,7 +42,7 @@ "error": { "failed": "something went wrong.", "no_name": "Please enter a name.", - "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" + "no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead." }, "abort": { "single_instance_allowed": "Only a single Thermostat for each real is allowed.", @@ -66,13 +72,19 @@ "advanced": { "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", "data": { - "calibration_round": "Should the calibration be rounded to the nearest", + "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" + "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." } } } diff --git a/custom_components/better_thermostat/utils/bridge.py b/custom_components/better_thermostat/utils/bridge.py index 1b97054b..5dc92d38 100644 --- a/custom_components/better_thermostat/utils/bridge.py +++ b/custom_components/better_thermostat/utils/bridge.py @@ -62,6 +62,16 @@ async def get_offset_steps(self, entity_id): 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( diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py index 2a625bec..92045171 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -1,6 +1,11 @@ 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, @@ -60,53 +65,50 @@ async def control_trv(self, heater_entity_id=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_TRV_mode = _trv.state _current_set_temperature = convert_to_float( str(_trv.attributes.get("temperature", None)), self.name, "controlling()" ) - _hvac_mode_send = HVACMode.OFF - _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 - _converted_hvac_mode = _remapped_states.get("system_mode", None) + _temperature = _remapped_states.get("temperature", None) _calibration = _remapped_states.get("local_temperature_calibration", None) - if self.call_for_heat is True: - _hvac_mode_send = _converted_hvac_mode + _new_hvac_mode = handle_window_open(self, _remapped_states) - 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 + # 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 - if ( - _calibration is not None - and self.bt_hvac_mode != HVACMode.OFF - and self.window_open is False - ): + # send new HVAC mode to TRV, if it changed + if _new_hvac_mode is not None and _new_hvac_mode != _trv.state: + _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: @@ -115,6 +117,7 @@ async def control_trv(self, heater_entity_id=None): 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 @@ -132,66 +135,71 @@ async def control_trv(self, heater_entity_id=None): "last_calibration", current_calibration ) - _cur_trv_temp = convert_to_float( - str(self.real_trvs[heater_entity_id]["current_temperature"]), - self.name, - "controlling()", - ) - - _calibration_delta = float( - str(format(float(abs(_cur_trv_temp - self.cur_temp)), ".1f")) - ) - - _shoud_calibrate = False - if _calibration_delta >= float(step_calibration): - _shoud_calibrate = True - - if ( - self.real_trvs[heater_entity_id]["calibration_received"] is True - and float(old) != float(_calibration) - and _shoud_calibrate is True - ): + 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 - if _hvac_mode_send is not None: - if _hvac_mode_send != _current_TRV_mode: - _LOGGER.debug( - f"better_thermostat {self.name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_current_TRV_mode} to: {_hvac_mode_send}" - ) - self.real_trvs[heater_entity_id]["last_hvac_mode"] = _hvac_mode_send - await set_hvac_mode(self, heater_entity_id, _hvac_mode_send) - 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)) - - if _temperature is not None and self.window_open is False: + # set new target temperature + if _temperature is not None and _new_hvac_mode != HVACMode.OFF: 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}" ) - await set_temperature(self, heater_entity_id, _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(checktarget_temperature(self, heater_entity_id)) + 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 - while ( - self.real_trvs[heater_entity_id]["hvac_mode"] - != self.real_trvs[heater_entity_id]["last_hvac_mode"] - ): + _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" @@ -201,13 +209,14 @@ async def check_system_mode(self, heater_entity_id=None): await asyncio.sleep(1) _timeout += 1 await asyncio.sleep(2) - self.real_trvs[heater_entity_id]["system_mode_received"] = True + _real_trv["system_mode_received"] = True return True -async def checktarget_temperature(self, heater_entity_id=None): +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( @@ -216,11 +225,15 @@ async def checktarget_temperature(self, heater_entity_id=None): ) ), self.name, - "check_target()", + "check_target_temperature()", + ) + + _LOGGER.debug( + f"better_thermostat {self.name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}" ) if ( - self.real_trvs[heater_entity_id]["last_temperature"] - == _current_set_temperature + _current_set_temperature is None + or _real_trv["last_temperature"] == _current_set_temperature ): _timeout = 0 break @@ -233,5 +246,6 @@ async def checktarget_temperature(self, heater_entity_id=None): await asyncio.sleep(1) _timeout += 1 await asyncio.sleep(2) - self.real_trvs[heater_entity_id]["target_temp_received"] = True + + _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 index 03766e52..4788e4af 100644 --- a/custom_components/better_thermostat/utils/helpers.py +++ b/custom_components/better_thermostat/utils/helpers.py @@ -8,8 +8,18 @@ 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 + +from ..const import ( + CONF_HEAT_AUTO_SWAPPED, + CONF_HEATING_POWER_CALIBRATION, + CONF_FIX_CALIBRATION, + CONF_PROTECT_OVERHEATING, +) _LOGGER = logging.getLogger(__name__) @@ -72,13 +82,36 @@ def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]: """ _context = "calculate_local_setpoint_delta()" - _current_trv_calibration = convert_to_float( - str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context - ) + 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}:" @@ -86,22 +119,63 @@ def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]: ) return None - if self.real_trvs[entity_id]["advanced"].get("fix_calibration", False) is True: + _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.2 and _temp_diff < 1: - _cur_trv_temp = round_to_half_degree(_cur_trv_temp) - _cur_trv_temp += 0.5 - if _temp_diff >= 1.2: - _cur_trv_temp = round_to_half_degree(_cur_trv_temp) - _cur_trv_temp += 2.5 - if _temp_diff > -0.2 and _temp_diff < 0: - _cur_trv_temp = round_down_to_half_degree(_cur_trv_temp) - _cur_trv_temp -= 0.5 - if _temp_diff >= -1.2 and _temp_diff < 0: - _cur_trv_temp = round_down_to_half_degree(_cur_trv_temp) - _cur_trv_temp -= 2.5 + 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 + 0.10) >= self.bt_target_temp: + _new_local_calibration += 1.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, + ) - _new_local_calibration = (self.cur_temp - _cur_trv_temp) + _current_trv_calibration return convert_to_float(str(_new_local_calibration), self.name, _context) @@ -121,25 +195,68 @@ def calculate_setpoint_override(self, entity_id) -> Union[float, None]: 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 - if self.real_trvs[entity_id]["advanced"].get("fix_calibration", False) is True: + _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.3 and _calibrated_setpoint - _cur_trv_temp < 2.5: + 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 + 0.10) >= self.bt_target_temp: + _calibrated_setpoint -= 1.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]: @@ -162,12 +279,12 @@ def convert_to_float( If error occurred and cannot convert the value. """ if isinstance(value, float): - return value + return round(value, 1) elif value is None or value == "None": return None else: try: - return float(str(format(float(value), ".1f"))) + 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}" @@ -217,8 +334,11 @@ def round_down_to_half_degree( return None split = str(float(str(value))).split(".", 1) decimale = int(split[1]) - if decimale > 7: - return float(str(split[0])) + 0.5 + 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])) 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/weather.py b/custom_components/better_thermostat/utils/weather.py index 5bb65971..f0c3924e 100644 --- a/custom_components/better_thermostat/utils/weather.py +++ b/custom_components/better_thermostat/utils/weather.py @@ -1,7 +1,9 @@ +from collections import deque import logging -from datetime import datetime, timedelta +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 @@ -9,6 +11,8 @@ # from homeassistant.components.recorder.history import state_changes_during_period from .helpers import convert_to_float +from statistics import median + _LOGGER = logging.getLogger(__name__) @@ -27,16 +31,28 @@ def check_weather(self) -> 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: - self.call_for_heat = check_weather_prediction(self) + _call_for_heat_weather = check_weather_prediction(self) - elif self.outdoor_sensor is not None: + if self.outdoor_sensor is not None: if None in (self.last_avg_outdoor_temp, self.off_temperature): - self.call_for_heat = False - return False - self.call_for_heat = self.last_avg_outdoor_temp < self.off_temperature - else: + # 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_weather or _call_for_heat_outdoor + + if self.weather_entity is None and self.outdoor_sensor is None: self.call_for_heat = True if old_call_for_heat != self.call_for_heat: @@ -66,18 +82,27 @@ def check_weather_prediction(self) -> bool: return None try: - forcast = self.hass.states.get(self.weather_entity).attributes.get("forecast") - if len(forcast) > 0: - max_forcast_temp = int( + 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(forcast[0]["temperature"]), + str(forecast[0]["temperature"]), self.name, "check_weather_prediction()", ) + convert_to_float( - str(forcast[1]["temperature"]), + str(forecast[1]["temperature"]), self.name, "check_weather_prediction()", ) @@ -85,7 +110,10 @@ def check_weather_prediction(self) -> bool: / 2 ) ) - return max_forcast_temp < self.off_temperature + return ( + cur_outside_temp < self.off_temperature + or max_forecast_temp < self.off_temperature + ) else: raise TypeError except TypeError: @@ -112,74 +140,97 @@ async def check_ambient_air_temperature(self): ) return None - last_two_days_date_time = datetime.now() - timedelta(days=2) - start = dt_util.as_utc(last_two_days_date_time) - history_list = await get_instance(self.hass).async_add_executor_job( - history.state_changes_during_period, - self.hass, - start, - dt_util.as_utc(datetime.now()), - str(self.outdoor_sensor), + self.last_avg_outdoor_temp = convert_to_float( + self.hass.states.get(self.outdoor_sensor).state, + self.name, + "check_ambient_air_temperature()", ) - historic_sensor_data = history_list.get(self.outdoor_sensor) - # create a list from valid data in historic_sensor_data - valid_historic_sensor_data = [] - invalid_sensor_data_count = 0 - if historic_sensor_data is not None: - _LOGGER.warning( - f"better_thermostat {self.name}: {self.outdoor_sensor} has no historic data." - ) - return convert_to_float( - self.hass.states.get(self.outdoor_sensor).state, - self.name, - "check_ambient_air_temperature()", - ) - for measurement in historic_sensor_data: - if isinstance( - measurement := convert_to_float( - str(measurement.state), self.name, "check_ambient_air_temperature()" - ), - float, - ): - valid_historic_sensor_data.append(measurement) - else: - invalid_sensor_data_count += 1 - - if len(valid_historic_sensor_data) == 0: - _LOGGER.warning( - f"better_thermostat {self.name}: no valid outdoor sensor data found." - ) - return None - - if invalid_sensor_data_count: + 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( - f"better_thermostat {self.name}: ignored {invalid_sensor_data_count} invalid outdoor sensor data entries." + "Initializing values for %s from the database", self.outdoor_sensor ) - - # remove the upper and lower 5% of the data - valid_historic_sensor_data.sort() - valid_historic_sensor_data = valid_historic_sensor_data[ - int(round(len(valid_historic_sensor_data) * 0.05)) : int( - round(len(valid_historic_sensor_data) * 0.95) + 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, ) - ] - if len(valid_historic_sensor_data) == 0: - _LOGGER.warning( - f"better_thermostat {self.name}: no valid outdoor sensor data found." - ) - return None + 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()), + ) - _LOGGER.debug( - f"better_thermostat {self.name}: check_ambient_air_temperature is evaluating {len(valid_historic_sensor_data)} sensor values." - ) + avg_temp = _temp_history.min + + _LOGGER.debug("Initializing from database completed") + else: + avg_temp = self.last_avg_outdoor_temp - # calculate the average temperature - avg_temp = int( - round(sum(valid_historic_sensor_data) / len(valid_historic_sensor_data)) - ) _LOGGER.debug( f"better_thermostat {self.name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}" ) 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 From 057a86cdee8c22130bb58134af207089397c7672 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 11 Jan 2023 12:12:12 +0000 Subject: [PATCH 014/158] Bring in Better Thermostat UI --- www/better-thermostat-ui-card.js | 483 +++++++++++++++++++++++++++++++ 1 file changed, 483 insertions(+) create mode 100644 www/better-thermostat-ui-card.js 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}; From d01243b245971521ba8f219e0753465d5c52a719 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 2 Feb 2023 13:44:04 +0000 Subject: [PATCH 015/158] Fix MQTT sensor --- packages/overflights.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/overflights.yaml b/packages/overflights.yaml index d865ce91..37f07219 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -11,11 +11,12 @@ sensor: latitude: 55.765 longitude: -4.496 - - platform: mqtt - 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') }}" +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' From c79286f6aa8ba029d90871a5ed0606cbd49e9159 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 2 Feb 2023 13:49:16 +0000 Subject: [PATCH 016/158] Viewpoint Mastodon platform --- notify.yaml | 6 ++++++ travis_secrets.yaml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/notify.yaml b/notify.yaml index 3d0300eb..bcd8726d 100644 --- a/notify.yaml +++ b/notify.yaml @@ -17,6 +17,12 @@ 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 diff --git a/travis_secrets.yaml b/travis_secrets.yaml index 40cfdfc2..54ea0924 100644 --- a/travis_secrets.yaml +++ b/travis_secrets.yaml @@ -44,3 +44,6 @@ 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 From 2ce5a8dc0105c746f1c07ad776e0f386d23e584c Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 2 Feb 2023 13:53:54 +0000 Subject: [PATCH 017/158] Convert to Mastodon --- automation/mark_person_as_arrived.yaml | 2 +- scripts/tweet_engine.yaml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) 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/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 From 4081f412901d902570c29d5317458c06306bbfce Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 2 Feb 2023 13:55:07 +0000 Subject: [PATCH 018/158] Bin Twitter --- notify.yaml | 12 ------------ packages/overflights.yaml | 4 ---- travis_secrets.yaml | 4 ---- 3 files changed, 20 deletions(-) diff --git a/notify.yaml b/notify.yaml index bcd8726d..472ab49a 100644 --- a/notify.yaml +++ b/notify.yaml @@ -1,16 +1,4 @@ --- -- 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: mastodon name: mastodon_overuplawmoor base_url: https://botsin.space/ diff --git a/packages/overflights.yaml b/packages/overflights.yaml index 37f07219..324cfa73 100644 --- a/packages/overflights.yaml +++ b/packages/overflights.yaml @@ -49,11 +49,7 @@ 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 - data_template: - 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.mastodon_overuplawmoor data_template: message: 'Flight {{ trigger.event.data.callsign }} is passing near Uplawmoor at {{ trigger.event.data.altitude }} meters. diff --git a/travis_secrets.yaml b/travis_secrets.yaml index 54ea0924..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 From 944e15000c6dd53f61ceb9d03abe2feae23fc5c1 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 4 Feb 2023 16:32:02 +0000 Subject: [PATCH 019/158] Use new format --- esphome/ble_proxy_1.yaml | 9 +++++---- esphome/ili9341_1.yaml | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/ble_proxy_1.yaml b/esphome/ble_proxy_1.yaml index 05a831ec..da80624c 100644 --- a/esphome/ble_proxy_1.yaml +++ b/esphome/ble_proxy_1.yaml @@ -10,10 +10,11 @@ substitutions: esphome: name: ${device_name} comment: ${device_description} - platform: ESP32 + +esp32: board: nodemcu-32s -captive_portal: +# captive_portal: logger: @@ -24,10 +25,10 @@ time: id: esptime timezone: Europe/London +bluetooth_proxy: + esp32_ble_tracker: scan_parameters: interval: 1100ms window: 1100ms active: true - -bluetooth_proxy: diff --git a/esphome/ili9341_1.yaml b/esphome/ili9341_1.yaml index 6a1f1f2c..3dc4560b 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: From 4ebbdc641bb24acfa99a2643e650658f2a36299f Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 7 Feb 2023 09:17:25 +0000 Subject: [PATCH 020/158] Fix sensor reference --- packages/pihole.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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", From 647492035c54d92368bee3d4c00ccbba54e1250f Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 7 Feb 2023 16:34:57 +0000 Subject: [PATCH 021/158] =?UTF-8?q?Remove=20unavailable=20=EF=BF=AB=20on?= =?UTF-8?q?=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/boot_room.yaml | 2 ++ packages/ensuite.yaml | 1 + packages/garage.yaml | 1 + packages/kitchen.yaml | 1 + packages/living_room_lights.yaml | 1 + 5 files changed, 6 insertions(+) diff --git a/packages/boot_room.yaml b/packages/boot_room.yaml index 296af72f..d00d013a 100644 --- a/packages/boot_room.yaml +++ b/packages/boot_room.yaml @@ -27,6 +27,7 @@ automation: - platform: state entity_id: binary_sensor.boot_room_motion to: 'on' + from: 'off' # Sensor attached to wrong door at the moment... # - platform: state # entity_id: binary_sensor.boot_room_door @@ -73,6 +74,7 @@ automation: - platform: state entity_id: binary_sensor.boot_room_motion to: 'on' + from: 'off' # Sensor attached to wrong door at the moment... # - platform: state # entity_id: binary_sensor.boot_room_door diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index 027bcee4..9b7ec87b 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -32,6 +32,7 @@ automation: - platform: state entity_id: binary_sensor.ensuite_motion to: 'on' + from: 'off' action: - service: light.turn_on data_template: diff --git a/packages/garage.yaml b/packages/garage.yaml index c2190f96..371fb6c3 100644 --- a/packages/garage.yaml +++ b/packages/garage.yaml @@ -6,6 +6,7 @@ automation: - platform: state entity_id: binary_sensor.garage_motion to: 'on' + from: 'off' action: - service: light.turn_on entity_id: light.garage_lights diff --git a/packages/kitchen.yaml b/packages/kitchen.yaml index 40e0755c..d5b30a5f 100644 --- a/packages/kitchen.yaml +++ b/packages/kitchen.yaml @@ -14,6 +14,7 @@ automation: platform: state entity_id: binary_sensor.kitchen_motion to: 'on' + from: 'off' condition: - condition: numeric_state entity_id: sensor.average_external_light_level diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index 58a6c18a..0b4d3a80 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -107,6 +107,7 @@ automation: - platform: state entity_id: binary_sensor.living_room_motion to: 'on' + from: 'off' condition: condition: and conditions: From a17257ff17628e4d35e90e7a51e60ca977ff2af2 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 7 Feb 2023 16:35:22 +0000 Subject: [PATCH 022/158] Light level triggers --- packages/boot_room.yaml | 15 +++++++++++++-- packages/front_hall.yaml | 13 +++++++++++-- packages/hallway.yaml | 29 ++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/boot_room.yaml b/packages/boot_room.yaml index d00d013a..87426dbf 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 @@ -38,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" @@ -57,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: diff --git a/packages/front_hall.yaml b/packages/front_hall.yaml index 2d3645fa..2d4c788d 100644 --- a/packages/front_hall.yaml +++ b/packages/front_hall.yaml @@ -1,6 +1,15 @@ --- +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 trigger: - platform: state entity_id: @@ -11,7 +20,7 @@ automation: 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" diff --git a/packages/hallway.yaml b/packages/hallway.yaml index 855a86ec..1aaae99a 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 @@ -34,10 +43,11 @@ automation: - binary_sensor.hall_door_motion - binary_sensor.hall_rooms_motion 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" @@ -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 From 5f070f9d7b5581787ba5b1f770cfc7a9e8898bcb Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 7 Feb 2023 17:00:11 +0000 Subject: [PATCH 023/158] Better Thermostat 1.0.1 --- .../better_thermostat/adapters/mqtt.py | 4 --- .../better_thermostat/events/temperature.py | 2 +- .../better_thermostat/events/trv.py | 5 ++- .../better_thermostat/manifest.json | 5 ++- .../model_fixes/BHT-002-GCLZB.py | 31 +++++++++++++++++++ .../SEA801-Zigbee_SEA802-Zigbee.py | 3 +- .../better_thermostat/model_fixes/SPZB0001.py | 4 --- .../better_thermostat/model_fixes/TS0601.py | 6 ++-- .../model_fixes/TS0601_thermostat.py | 6 ++-- .../better_thermostat/strings.json | 8 ++--- .../better_thermostat/translations/da.json | 8 ++--- .../better_thermostat/translations/de.json | 6 ++-- .../better_thermostat/translations/en.json | 8 ++--- .../better_thermostat/translations/fr.json | 8 ++--- .../better_thermostat/translations/pl.json | 8 ++--- .../better_thermostat/utils/bridge.py | 2 +- 16 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 custom_components/better_thermostat/model_fixes/BHT-002-GCLZB.py diff --git a/custom_components/better_thermostat/adapters/mqtt.py b/custom_components/better_thermostat/adapters/mqtt.py index bbc20c6a..a648a0be 100644 --- a/custom_components/better_thermostat/adapters/mqtt.py +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -104,8 +104,6 @@ async def get_offset_steps(self, entity_id): async def get_min_offset(self, entity_id): """Get min offset.""" - # looks like z2m has a min max bug currently force to -10 - return -6.0 return float( str( self.hass.states.get( @@ -117,8 +115,6 @@ async def get_min_offset(self, entity_id): async def get_max_offset(self, entity_id): """Get max offset.""" - # looks like z2m has a min max bug currently force to 10 - return 6.0 return float( str( self.hass.states.get( diff --git a/custom_components/better_thermostat/events/temperature.py b/custom_components/better_thermostat/events/temperature.py index 323fcee9..762846be 100644 --- a/custom_components/better_thermostat/events/temperature.py +++ b/custom_components/better_thermostat/events/temperature.py @@ -57,6 +57,6 @@ async def trigger_temperature_change(self, event): _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) - self.last_external_sensor_change = datetime.now() diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index a828a095..91f7dc2f 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -72,7 +72,10 @@ async def trigger_trv_change(self, event): and ( (datetime.now() - self.last_internal_sensor_change).total_seconds() > _time_diff - or self.real_trvs[entity_id]["calibration_received"] is False + 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"] diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json index bb5274f5..dd0fd3bf 100644 --- a/custom_components/better_thermostat/manifest.json +++ b/custom_components/better_thermostat/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://github.com/KartoffelToby/better_thermostat", "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", "iot_class": "local_push", - "version": "1.0.0-beta55", + "version": "1.0.1", "config_flow": true, "dependencies": [ "climate", @@ -14,8 +14,7 @@ "climate" ], "codeowners": [ - "@kartoffeltoby", - "@RubenKelevra" + "@kartoffeltoby" ], "requirements": [] } 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 index 2d473f28..6deb9782 100644 --- a/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py +++ b/custom_components/better_thermostat/model_fixes/SEA801-Zigbee_SEA802-Zigbee.py @@ -2,8 +2,7 @@ def fix_local_calibration(self, entity_id, offset): # device SEA802 fix if (self.cur_temp - self.bt_target_temp) < -0.2: offset -= 2.5 - elif (self.cur_temp + 0.5) > self.bt_target_temp: - offset = round(offset + 0.5, 1) + return offset diff --git a/custom_components/better_thermostat/model_fixes/SPZB0001.py b/custom_components/better_thermostat/model_fixes/SPZB0001.py index f6f2ab4e..f3f27188 100644 --- a/custom_components/better_thermostat/model_fixes/SPZB0001.py +++ b/custom_components/better_thermostat/model_fixes/SPZB0001.py @@ -1,12 +1,8 @@ def fix_local_calibration(self, entity_id, offset): - if (self.cur_temp + 0.5) > self.bt_target_temp: - offset += 3 return offset def fix_target_temperature_calibration(self, entity_id, temperature): - if (self.cur_temp + 0.5) > self.bt_target_temp: - temperature -= 3 return temperature diff --git a/custom_components/better_thermostat/model_fixes/TS0601.py b/custom_components/better_thermostat/model_fixes/TS0601.py index d7c3ff5a..8dc284a8 100644 --- a/custom_components/better_thermostat/model_fixes/TS0601.py +++ b/custom_components/better_thermostat/model_fixes/TS0601.py @@ -3,8 +3,7 @@ def fix_local_calibration(self, entity_id, offset): offset -= 2.5 elif (self.cur_temp + 0.10) >= self.bt_target_temp: offset = round(offset + 0.5, 1) - if (self.cur_temp + 0.5) > self.bt_target_temp: - offset += 1 + return offset @@ -19,8 +18,7 @@ def fix_target_temperature_calibration(self, entity_id, temperature): and temperature - _cur_trv_temp < 1.5 ): temperature += 1.5 - if (self.cur_temp + 0.5) > self.bt_target_temp: - temperature -= 2 + return temperature diff --git a/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py index d7c3ff5a..8dc284a8 100644 --- a/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py +++ b/custom_components/better_thermostat/model_fixes/TS0601_thermostat.py @@ -3,8 +3,7 @@ def fix_local_calibration(self, entity_id, offset): offset -= 2.5 elif (self.cur_temp + 0.10) >= self.bt_target_temp: offset = round(offset + 0.5, 1) - if (self.cur_temp + 0.5) > self.bt_target_temp: - offset += 1 + return offset @@ -19,8 +18,7 @@ def fix_target_temperature_calibration(self, entity_id, temperature): and temperature - _cur_trv_temp < 1.5 ): temperature += 1.5 - if (self.cur_temp + 0.5) > self.bt_target_temp: - temperature -= 2 + return temperature diff --git a/custom_components/better_thermostat/strings.json b/custom_components/better_thermostat/strings.json index 7c9b8f2a..a9270339 100644 --- a/custom_components/better_thermostat/strings.json +++ b/custom_components/better_thermostat/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration/new_device** ", + "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 themostat", @@ -16,7 +16,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "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", @@ -60,7 +60,7 @@ "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/calibration", + "calibration": "The sort of calibration https://better-thermostat.org/configuration/#second-step", "weather": "Your weather entity to get the outdoor temperature", "calibration_round": "Should the calibration be rounded to the nearest", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", @@ -69,7 +69,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "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", diff --git a/custom_components/better_thermostat/translations/da.json b/custom_components/better_thermostat/translations/da.json index 4c5d53fd..4cda3150 100644 --- a/custom_components/better_thermostat/translations/da.json +++ b/custom_components/better_thermostat/translations/da.json @@ -3,7 +3,7 @@ "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/new_device**", + "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", @@ -17,7 +17,7 @@ } }, "advanced": { - "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/calibration** ", + "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "Skal kalibreringen afrundes til nærmeste", "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", @@ -55,7 +55,7 @@ "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/calibration", + "calibration": "Den slags kalibrering https://better-thermostat.org/configuration/#second-step", "weather": "Din vejrentitet for at få udendørstemperaturen", "calibration_round": "Skal kalibreringen afrundes til nærmeste", "heat_auto_swapped": "Hvis auto betyder varme til din TRV, og du ønsker at bytte den", @@ -64,7 +64,7 @@ } }, "advanced": { - "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/calibration** ", + "description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "Skal kalibreringen afrundes til nærmeste", "heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det", diff --git a/custom_components/better_thermostat/translations/de.json b/custom_components/better_thermostat/translations/de.json index 56870483..80c26a61 100644 --- a/custom_components/better_thermostat/translations/de.json +++ b/custom_components/better_thermostat/translations/de.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr Informationen: https://better-thermostat.org/configuration/new_device** ", + "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", @@ -17,7 +17,7 @@ } }, "advanced": { - "description": "Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/calibration*** ", + "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.", @@ -66,7 +66,7 @@ } }, "advanced": { - "description": "Aktuallisere die Einstellungen für {trv}\n\n***Infos über die Kalibrierungstypen: https://better-thermostat.org/calibration*** ", + "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.", diff --git a/custom_components/better_thermostat/translations/en.json b/custom_components/better_thermostat/translations/en.json index 0de859de..a3362a36 100644 --- a/custom_components/better_thermostat/translations/en.json +++ b/custom_components/better_thermostat/translations/en.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration/new_device** ", + "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", @@ -17,7 +17,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "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", @@ -61,7 +61,7 @@ "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/calibration", + "calibration": "The sort of calibration https://better-thermostat.org/configuration/#second-step", "weather": "Your weather entity to get the outdoor temperature", "calibration_round": "Should the calibration be rounded to the nearest", "heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it", @@ -70,7 +70,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "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", diff --git a/custom_components/better_thermostat/translations/fr.json b/custom_components/better_thermostat/translations/fr.json index 59688c5f..36f6656c 100644 --- a/custom_components/better_thermostat/translations/fr.json +++ b/custom_components/better_thermostat/translations/fr.json @@ -3,7 +3,7 @@ "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/new_device** ", + "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", @@ -17,7 +17,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser (pour les utilisateurs de Google Home)", @@ -55,7 +55,7 @@ "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/calibration", + "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", "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser", @@ -64,7 +64,7 @@ } }, "advanced": { - "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/calibration** ", + "description": "Advanced configuration\n\n**Info about calibration types: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", "heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser", diff --git a/custom_components/better_thermostat/translations/pl.json b/custom_components/better_thermostat/translations/pl.json index dd8edf13..03c27cd1 100644 --- a/custom_components/better_thermostat/translations/pl.json +++ b/custom_components/better_thermostat/translations/pl.json @@ -3,7 +3,7 @@ "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/new_device** ", + "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", @@ -17,7 +17,7 @@ } }, "advanced": { - "description": "Zaawansowana konfiguracja\n\n**Informacja o typach kalibracji: https://better-thermostat.org/calibration** ", + "description": "Zaawansowana konfiguracja\n\n**Informacja o typach kalibracji: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", @@ -55,7 +55,7 @@ "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/calibration", + "calibration": "Rodzaje kalibracji https://better-thermostat.org/configuration/#second-step", "weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną", "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", @@ -64,7 +64,7 @@ } }, "advanced": { - "description": "Zaawansowana konfiguracja**Informacja o typach kalibracji: https://better-thermostat.org/calibration** ", + "description": "Zaawansowana konfiguracja**Informacja o typach kalibracji: https://better-thermostat.org/configuration/#second-step** ", "data": { "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", "heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić", diff --git a/custom_components/better_thermostat/utils/bridge.py b/custom_components/better_thermostat/utils/bridge.py index 5dc92d38..6b48a927 100644 --- a/custom_components/better_thermostat/utils/bridge.py +++ b/custom_components/better_thermostat/utils/bridge.py @@ -28,7 +28,7 @@ def load_adapter(self, integration, entity_id, get_name=False): "custom_components.better_thermostat.adapters.generic", package="better_thermostat", ) - _LOGGER.warning( + _LOGGER.info( "better_thermostat %s: intigration: %s isn't native supported, feel free to open an issue, fallback adapter %s", self.name, integration, From fac289052c6579a6e08e526a6bb6b5c40d8d6ff0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 8 Feb 2023 09:10:10 +0000 Subject: [PATCH 024/158] Above, not below --- packages/living_room_lights.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index 0b4d3a80..d5c6252e 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -95,7 +95,7 @@ automation: 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: From ff12fc0358a96739d63c76da440a394a216998de Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 12 Feb 2023 22:42:31 +0000 Subject: [PATCH 025/158] away not Away --- packages/climate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/climate.yaml b/packages/climate.yaml index 2e0e4fa3..18a7f7e7 100644 --- a/packages/climate.yaml +++ b/packages/climate.yaml @@ -199,7 +199,7 @@ automation: - service: climate.set_preset_mode data: entity_id: climate.hot_water - preset_mode: "Away" + preset_mode: "away" - service: logbook.log data_template: name: EVENT From 6f246cb8f8b7ab06c624ba268012e0d13478a9c9 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 12 Feb 2023 23:05:33 +0000 Subject: [PATCH 026/158] Trim crap --- packages/climate.yaml | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/packages/climate.yaml b/packages/climate.yaml index 18a7f7e7..4b80534c 100644 --- a/packages/climate.yaml +++ b/packages/climate.yaml @@ -98,11 +98,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 +119,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 +133,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 +158,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 +180,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 @@ -218,14 +198,14 @@ automation: entity_id: binary_sensor.home_occupied state: "on" action: + - service: climate.set_preset_mode + data: + entity_id: all + preset_mode: "none" - service: climate.set_temperature 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 @@ -244,14 +224,14 @@ automation: entity_id: binary_sensor.home_occupied state: "on" action: + - service: climate.set_preset_mode + data: + entity_id: all + preset_mode: "none" - service: climate.set_temperature 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 From 78b45d36205b8f5ecc477d47c0f9f097aaaca892 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 12 Feb 2023 23:18:56 +0000 Subject: [PATCH 027/158] Remove some set_presets --- packages/climate.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/climate.yaml b/packages/climate.yaml index 4b80534c..2414a258 100644 --- a/packages/climate.yaml +++ b/packages/climate.yaml @@ -198,10 +198,6 @@ automation: entity_id: binary_sensor.home_occupied state: "on" action: - - service: climate.set_preset_mode - data: - entity_id: all - preset_mode: "none" - service: climate.set_temperature data: entity_id: climate.house @@ -224,10 +220,6 @@ automation: entity_id: binary_sensor.home_occupied state: "on" action: - - service: climate.set_preset_mode - data: - entity_id: all - preset_mode: "none" - service: climate.set_temperature data: entity_id: climate.house From 214af5fcf0c460bcfbbc3b073155ddc31a35182b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 20 Feb 2023 10:25:05 +0000 Subject: [PATCH 028/158] Octopus energy v5.4.1 --- custom_components/octopus_energy/__init__.py | 175 +++ .../octopus_energy/api_client.py | 545 +++++++ .../octopus_energy/binary_sensor.py | 383 +++++ .../octopus_energy/config_flow.py | 276 ++++ custom_components/octopus_energy/const.py | 36 + .../octopus_energy/diagnostics.py | 42 + .../octopus_energy/manifest.json | 16 + custom_components/octopus_energy/sensor.py | 1317 +++++++++++++++++ .../octopus_energy/sensor_utils.py | 241 +++ .../octopus_energy/services.yaml | 32 + .../octopus_energy/target_sensor_utils.py | 184 +++ .../octopus_energy/translations/en.json | 68 + custom_components/octopus_energy/utils.py | 121 ++ esphome/common/esp32_camera_common.yaml | 6 + 14 files changed, 3442 insertions(+) create mode 100644 custom_components/octopus_energy/__init__.py create mode 100644 custom_components/octopus_energy/api_client.py create mode 100644 custom_components/octopus_energy/binary_sensor.py create mode 100644 custom_components/octopus_energy/config_flow.py create mode 100644 custom_components/octopus_energy/const.py create mode 100644 custom_components/octopus_energy/diagnostics.py create mode 100644 custom_components/octopus_energy/manifest.json create mode 100644 custom_components/octopus_energy/sensor.py create mode 100644 custom_components/octopus_energy/sensor_utils.py create mode 100644 custom_components/octopus_energy/services.yaml create mode 100644 custom_components/octopus_energy/target_sensor_utils.py create mode 100644 custom_components/octopus_energy/translations/en.json create mode 100644 custom_components/octopus_energy/utils.py diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py new file mode 100644 index 00000000..768ff034 --- /dev/null +++ b/custom_components/octopus_energy/__init__.py @@ -0,0 +1,175 @@ +import logging +from datetime import timedelta +import asyncio + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + + CONFIG_TARGET_NAME, + + DATA_CLIENT, + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_RATES, + DATA_ACCOUNT_ID, + DATA_ACCOUNT, + DATA_SAVING_SESSIONS, + DATA_SAVING_SESSIONS_COORDINATOR +) + +from .api_client import OctopusEnergyApiClient + +from .utils import ( + get_active_tariff_code +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry): + """This is called from the config flow.""" + hass.data.setdefault(DOMAIN, {}) + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_dependencies(hass, entry.data) + + # 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") + ) + elif CONFIG_TARGET_NAME in entry.data: + 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_get_current_electricity_agreement_tariff_codes(client, config): + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + tariff_codes = {} + current = now() + if 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 != 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 + +async def async_setup_dependencies(hass, config): + """Setup the coordinator and api client which will be shared by various entities""" + + if DATA_CLIENT not in hass.data[DOMAIN]: + client = OctopusEnergyApiClient(config[CONFIG_MAIN_API_KEY]) + hass.data[DOMAIN][DATA_CLIENT] = client + hass.data[DOMAIN][DATA_ACCOUNT_ID] = config[CONFIG_MAIN_ACCOUNT_ID] + + setup_rates_coordinator(hass, client, config) + + setup_saving_sessions_coordinators(hass, client) + + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + +def setup_rates_coordinator(hass, client, config): + async def async_update_electricity_rates_data(): + """Fetch data from API endpoint.""" + # Only get data every half hour or if we don't have any data + current = now() + if (DATA_RATES not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][DATA_RATES]) == 0): + + tariff_codes = await async_get_current_electricity_agreement_tariff_codes(client, config) + _LOGGER.debug(f'tariff_codes: {tariff_codes}') + + period_from = as_utc(current.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 = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + if new_rates != None: + rates[key] = new_rates + elif (DATA_RATES in hass.data[DOMAIN] and key in hass.data[DOMAIN][DATA_RATES]): + _LOGGER.debug(f"Failed to retrieve new rates for {tariff_code}, so using cached rates") + rates[key] = hass.data[DOMAIN][DATA_RATES][key] + + hass.data[DOMAIN][DATA_RATES] = rates + + return hass.data[DOMAIN][DATA_RATES] + + hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="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), + ) + +def setup_saving_sessions_coordinators(hass, client: OctopusEnergyApiClient): + 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() + if DATA_SAVING_SESSIONS not in hass.data[DOMAIN] or current.minute % 30 == 0: + savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_SAVING_SESSIONS] = savings + + 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), + ) + +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.""" + if CONFIG_MAIN_API_KEY in entry.data: + target_domain = "sensor" + elif CONFIG_TARGET_NAME in entry.data: + target_domain = "binary_sensor" + + unload_ok = all( + await asyncio.gather( + *[hass.config_entries.async_forward_entry_unload(entry, target_domain)] + ) + ) + + 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..1f88c96d --- /dev/null +++ b/custom_components/octopus_energy/api_client.py @@ -0,0 +1,545 @@ +import logging +import json +import aiohttp +from datetime import (timedelta) +from homeassistant.util.dt import (as_utc, now, as_local, parse_datetime) + +from .utils import ( + get_tariff_parts, + get_valid_from, + rates_to_thirty_minute_increments +) + +_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) {{ + serialNumber + smartExportElectricityMeter {{ + deviceId + }} + smartImportElectricityMeter {{ + deviceId + }} + }} + agreements {{ + validFrom + validTo + tariff {{ + ...on StandardTariff {{ + tariffCode + }} + ...on DayNightTariff {{ + tariffCode + }} + ...on ThreeRateTariff {{ + tariffCode + }} + ...on HalfHourlyTariff {{ + tariffCode + }} + ...on PrepayTariff {{ + tariffCode + }} + }} + }} + }} + }} + gasAgreements(active: true) {{ + meterPoint {{ + mprn + meters(includeInactive: false) {{ + serialNumber + consumptionUnits + }} + agreements {{ + validFrom + validTo + tariff {{ + tariffCode + }} + }} + }} + }} + }} +}}''' + +saving_session_query = '''query {{ + savingSessions {{ + account(accountNumber: "{account_id}") {{ + hasJoinedCampaign + joinedEvents {{ + eventId + startAt + endAt + }} + }} + }} + octoPoints {{ + account(accountNumber: "{account_id}") {{ + currentPointsInWallet + }} + }} +}}''' + +class OctopusEnergyApiClient: + + def __init__(self, api_key): + if (api_key == 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() + + async def async_refresh_token(self): + """Get the user's refresh token""" + if (self._graphql_expiration != 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 != None and "data" in token_response_body): + self._graphql_token = token_response_body["data"]["obtainKrakenToken"]["token"] + self._graphql_expiration = now() + timedelta(hours=1) + else: + _LOGGER.error("Failed to retrieve auth token") + raise Exception('Failed to refresh 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(account_response_body) + + if (account_response_body != None and "data" in account_response_body): + 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"] != None, + "is_smart_meter": m["smartImportElectricityMeter"] != None or m["smartExportElectricityMeter"] != None, + }, mp["meterPoint"]["meters"])), + "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, + }, mp["meterPoint"]["agreements"])) + }, account_response_body["data"]["account"]["electricityAgreements"])), + "gas_meter_points": list(map(lambda mp: { + "mprn": mp["meterPoint"]["mprn"], + "meters": list(map(lambda m: { + "serial_number": m["serialNumber"], + "consumption_units": m["consumptionUnits"], + }, mp["meterPoint"]["meters"])), + "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, + }, mp["meterPoint"]["agreements"])) + }, account_response_body["data"]["account"]["gasAgreements"])), + } + 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 != 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_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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract standard rates: {url}') + raise + + 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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + # 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) + for rate in day_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == False: + results.append(rate) + except: + _LOGGER.error(f'Failed to extract day rates: {url}') + raise + + 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: + try: + data = await self.__async_read_response(response, url) + if data == 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) + for rate in night_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == True: + results.append(rate) + except: + _LOGGER.error(f'Failed to extract night rates: {url}') + raise + + # 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) + _LOGGER.debug(results) + + 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) + product_code = tariff_parts["product_code"] + + if (await self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + elif (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 != 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) + product_code = tariff_parts["product_code"] + + if (await self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + + 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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract standard gas rates: {url}') + raise + + 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 != 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_products(self, is_variable): + """Get all products""" + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products?is_variable={is_variable}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data): + return data["results"] + + return [] + + 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) + product_code = tariff_parts["product_code"] + + if await self.__async_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: + try: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data and len(data["results"]) > 0): + result = { + "value_exc_vat": float(data["results"][0]["value_exc_vat"]), + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + except: + _LOGGER.error(f'Failed to extract electricity standing charges: {url}') + raise + + 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) + product_code = tariff_parts["product_code"] + + if await self.__async_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: + try: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data and len(data["results"]) > 0): + result = { + "value_exc_vat": float(data["results"][0]["value_exc_vat"]), + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + except: + _LOGGER.error(f'Failed to extract gas standing charges: {url}') + raise + + return result + + async def __async_is_tracker_tariff(self, tariff_code): + tariff_parts = get_tariff_parts(tariff_code) + product_code = tariff_parts["product_code"] + + if product_code in self._product_tracker_cache: + return self._product_tracker_cache[product_code] + + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://api.octopus.energy/v1/products/{product_code}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response(response, url) + if data == None: + return False + + is_tracker = "is_tracker" in data and data["is_tracker"] + self._product_tracker_cache[product_code] = is_tracker + return is_tracker + + async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to): + """Get the tracker rates""" + + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' + async with client.get(url, auth=auth) as response: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + items = [] + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + vat = float(period["breakdown"]["standing_charge"]["VAT"]) + + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + vat = float(period["breakdown"]["unit_charge"]["VAT"]) + items.append( + { + "valid_from": valid_from.strftime("%Y-%m-%dT%H:%M:%SZ"), + "valid_to": valid_to.strftime("%Y-%m-%dT%H:%M:%SZ"), + "value_exc_vat": float(period["unit_rate"]) - vat, + "value_inc_vat": float(period["unit_rate"]), + } + ) + + results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract tracker gas rates: {url}') + raise + + return results + + async def __async_get_tracker_standing_charge__(self, tariff_code, period_from, period_to): + """Get the tracker standing charge""" + + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' + async with client.get(url, auth=auth) as response: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + vat = float(period["breakdown"]["standing_charge"]["VAT"]) + return { + "value_exc_vat": float(period["standing_charge"]) - vat, + "value_inc_vat": float(period["standing_charge"]) + } + except: + _LOGGER.error(f'Failed to extract tracker gas rates: {url}') + raise + + return None + + 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: + _LOGGER.error(f'Request failed ({url}): {response.status}; {text}') + return None + + try: + return json.loads(text) + except: + raise Exception(f'Failed to extract response json: {url}; {text}') diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py new file mode 100644 index 00000000..d81b4a41 --- /dev/null +++ b/custom_components/octopus_energy/binary_sensor.py @@ -0,0 +1,383 @@ +from datetime import timedelta +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 homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers import config_validation as cv, entity_platform, service +from .const import ( + CONFIG_TARGET_OFFSET, + DOMAIN, + + CONFIG_MAIN_API_KEY, + 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, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_ACCOUNT +) + +from .target_sensor_utils import ( + calculate_continuous_times, + calculate_intermittent_times, + is_target_rate_active +) + +from .sensor_utils import ( + is_saving_sessions_event_active, + get_next_saving_sessions_event +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +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_season_sensors(hass, entry, 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_season_sensors(hass, entry, async_add_entities): + _LOGGER.debug('Setting up Season Saving entity') + 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(saving_session_coordinator)], 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(coordinator, config, is_export)] + async_add_entities(entities, True) + +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, + offset, + 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, + offset, + 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 = is_target_rate_active(current_date, self._target_rates, offset) + + if offset != None and active_result["next_time"] != None: + self._attributes["next_time"] = apply_offset(active_result["next_time"], offset) + else: + self._attributes["next_time"] = active_result["next_time"] + + self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"] + self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"] + + 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() + +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() + self._state = is_saving_sessions_event_active(current_date, self._events) + 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/config_flow.py b/custom_components/octopus_energy/config_flow.py new file mode 100644 index 00000000..0d7605fd --- /dev/null +++ b/custom_components/octopus_energy/config_flow.py @@ -0,0 +1,276 @@ +import re +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 .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + + 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, + + DATA_SCHEMA_ACCOUNT, + DATA_CLIENT, + DATA_ACCOUNT_ID, + + REGEX_TIME, + REGEX_ENTITY_NAME, + REGEX_HOURS, + REGEX_OFFSET_PARTS, +) + +from .api_client import OctopusEnergyApiClient + +from .utils import get_active_tariff_code + +_LOGGER = logging.getLogger(__name__) + +def validate_target_rate_sensor(data): + errors = {} + + matches = re.search(REGEX_ENTITY_NAME, data[CONFIG_TARGET_NAME]) + if matches == None: + errors[CONFIG_TARGET_NAME] = "invalid_target_name" + + # For some reason float type isn't working properly - reporting user input malformed + matches = re.search(REGEX_HOURS, data[CONFIG_TARGET_HOURS]) + if matches == None: + errors[CONFIG_TARGET_HOURS] = "invalid_target_hours" + else: + data[CONFIG_TARGET_HOURS] = float(data[CONFIG_TARGET_HOURS]) + 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 == 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 == 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 == None: + errors[CONFIG_TARGET_OFFSET] = "invalid_offset" + + return errors + +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 = {} + + client = OctopusEnergyApiClient(user_input[CONFIG_MAIN_API_KEY]) + account_info = await client.async_get_account(user_input[CONFIG_MAIN_ACCOUNT_ID]) + if (account_info == 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="Octopus Energy", + 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]) + + meters = [] + now = utcnow() + if 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 != None: + meters.append(point["mpan"]) + + 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, + }) + + async def async_step_target_rate(self, user_input): + """Setup a target based on the provided user input""" + + errors = validate_target_rate_sensor(user_input) + + 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]) + + meters = [] + now = utcnow() + if 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 != None: + meters.append(point["mpan"]) + + 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] + + return self.async_show_form( + step_id="target_rate", + data_schema=vol.Schema({ + vol.Required(CONFIG_TARGET_HOURS, default=f'{config[CONFIG_TARGET_HOURS]}'): str, + 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, + }), + 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) + + 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, + }) + ) + 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) + 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) + + errors = validate_target_rate_sensor(config) + + 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..a0f69ac0 --- /dev/null +++ b/custom_components/octopus_energy/const.py @@ -0,0 +1,36 @@ +import voluptuous as vol + +DOMAIN = "octopus_energy" + +CONFIG_MAIN_API_KEY = "Api key" +CONFIG_MAIN_ACCOUNT_ID = "Account Id" + +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" + +DATA_CONFIG = "CONFIG" +DATA_ELECTRICITY_RATES_COORDINATOR = "ELECTRICITY_RATES_COORDINATOR" +DATA_CLIENT = "CLIENT" +DATA_RATES = "RATES" +DATA_GAS_TARIFF_CODE = "GAS_TARIFF_CODE" +DATA_ACCOUNT_ID = "ACCOUNT_ID" +DATA_ACCOUNT = "ACCOUNT" +DATA_SAVING_SESSIONS = "SAVING_SESSIONS" +DATA_SAVING_SESSIONS_COORDINATOR = "SAVING_SESSIONS_COORDINATOR" + +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_]+$" +REGEX_TARIFF_PARTS = "^([A-Z])-([0-9A-Z]+)-([A-Z0-9-]+)-([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, +}) diff --git a/custom_components/octopus_energy/diagnostics.py b/custom_components/octopus_energy/diagnostics.py new file mode 100644 index 00000000..eaa2f23b --- /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 = len(account_info["electricity_meter_points"]) + if 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" }) + + points_length = len(account_info["gas_meter_points"]) + if 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" }) + + _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/manifest.json b/custom_components/octopus_energy/manifest.json new file mode 100644 index 00000000..f0b47316 --- /dev/null +++ b/custom_components/octopus_energy/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "octopus_energy", + "name": "Octopus Energy", + "config_flow": true, + "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/", + "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@bottlecapdave" + ], + "version": "5.4.1", + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py new file mode 100644 index 00000000..dfdd3272 --- /dev/null +++ b/custom_components/octopus_energy/sensor.py @@ -0,0 +1,1317 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, now, as_utc, parse_datetime) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + VOLUME_CUBIC_METERS +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .sensor_utils import ( + async_get_consumption_data, + calculate_electricity_consumption, + async_calculate_electricity_cost, + calculate_gas_consumption, + async_calculate_gas_cost +) + +from .utils import (get_active_tariff_code) +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_CLIENT, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +def create_reading_coordinator(hass, client, is_electricity, identifier, serial_number): + """Create reading coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + + previous_consumption_key = f'{identifier}_{serial_number}_previous_consumption' + previous_data = None + if previous_consumption_key in hass.data[DOMAIN]: + previous_data = hass.data[DOMAIN][previous_consumption_key] + + 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)) + + data = await async_get_consumption_data( + client, + previous_data, + utcnow(), + period_from, + period_to, + identifier, + serial_number, + is_electricity + ) + + if data != None and len(data) > 0: + hass.data[DOMAIN][previous_consumption_key] = data + return data + + return [] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="rates", + 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}_consumption_coordinator'] = coordinator + + return coordinator + +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, entry, async_add_entities): + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + client = hass.data[DOMAIN][DATA_CLIENT] + + rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] + + await rate_coordinator.async_config_entry_first_refresh() + + saving_session_coordinator = hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] + + await saving_session_coordinator.async_config_entry_first_refresh() + + entities = [OctopusEnergySavingSessionPoints(saving_session_coordinator)] + + 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 != None: + for meter in point["meters"]: + _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeElectricityReading(coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(coordinator, client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityCurrentRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityPreviousRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityNextRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityCurrentStandingCharge(client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + 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 != None: + for meter in point["meters"]: + _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyPreviousAccumulativeGasReadingKwh(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, gas_tariff_code, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyGasCurrentRate(client, gas_tariff_code, point["mprn"], meter["serial_number"])) + entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, point["mprn"], meter["serial_number"])) + 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) + +class OctopusEnergyElectricitySensor(SensorEntity, RestoreEntity): + def __init__(self, mpan, serial_number, is_export, is_smart_meter): + """Init sensor""" + self._mpan = mpan + self._serial_number = serial_number + self._is_export = is_export + self._is_smart_meter = is_smart_meter + + 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": "Electricity Meter", + } + +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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"] + }, rate)) + self._attributes = { + "rate": current_rate, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "rates": ratesAttributes + } + + self._state = current_rate["value_inc_vat"] / 100 + 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_previous_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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 = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the next rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_next_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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 = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityCurrentStandingCharge(OctopusEnergyElectricitySensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_current_standing_charge'; + + @property + def name(self): + """Name of the sensor.""" + return f'Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeElectricityReading(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost.""" + + def __init__(self, coordinator, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): + def __init__(self, mprn, serial_number): + """Init sensor""" + self._mprn = mprn + self._serial_number = 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", + } + +class OctopusEnergyGasCurrentRate(OctopusEnergyGasSensor): + """Sensor for displaying the current rate.""" + + def __init__(self, client, tariff_code, mprn, serial_number): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + 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_rate'; + + @property + def name(self): + """Name of the sensor.""" + return f'Octopus Energy 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""" + 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 OctopusEnergyGasCurrentRate') + + 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"))) + + rates = await self._client.async_get_gas_rates(self._tariff_code, period_from, period_to) + + current_rate = None + if rates != None: + for period in rates: + if utc_now >= period["valid_from"] and utc_now <= period["valid_to"]: + current_rate = period + break + + if current_rate != None: + self._latest_date = period_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"] = period_from + current_rate["valid_to"] = period_to + self._attributes = current_rate + else: + self._state = None + self._attributes = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyGasCurrentStandingCharge(OctopusEnergyGasSensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client, tariff_code, mprn, serial_number): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + 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'Octopus Energy 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasReading(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading.""" + + def __init__(self, coordinator, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._native_consumption_units = native_consumption_units + 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}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy 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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasReadingKwh(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading in kwh.""" + + def __init__(self, coordinator, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._native_consumption_units = native_consumption_units + 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}_previous_accumulative_consumption_kwh" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption (kWh)" + + @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: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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas cost.""" + + def __init__(self, coordinator, client, tariff_code, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._client = client + self._tariff_code = tariff_code + self._native_consumption_units = native_consumption_units + + 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}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy 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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') \ No newline at end of file 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/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/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/translations/en.json b/custom_components/octopus_energy/translations/en.json new file mode 100644 index 00000000..f0538d05 --- /dev/null +++ b/custom_components/octopus_energy/translations/en.json @@ -0,0 +1,68 @@ +{ + "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)" + } + }, + "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.", + "data": { + "entity_id": "The name of your target", + "Hours": "The hours you require.", + "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" + } + } + }, + "error": { + "account_not_found": "Account information was not found", + "invalid_target_hours": "Target hours must be in half hour increments.", + "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" + }, + "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" + } + }, + "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.", + "data": { + "Hours": "The hours you require.", + "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" + } + } + }, + "error": { + "invalid_target_hours": "Target hours must be in half hour increments.", + "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" + }, + "abort": { + "not_supported": "Configuration for target rates is not supported at the moment." + } + } +} \ 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/esphome/common/esp32_camera_common.yaml b/esphome/common/esp32_camera_common.yaml index ac7bbff7..b43b8444 100644 --- a/esphome/common/esp32_camera_common.yaml +++ b/esphome/common/esp32_camera_common.yaml @@ -49,6 +49,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 From d987c687d3db3f2200d1a71cb8e4daf5830dcb36 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 20 Feb 2023 10:25:05 +0000 Subject: [PATCH 029/158] Octopus energy v5.4.1 --- custom_components/octopus_energy/__init__.py | 175 +++ .../octopus_energy/api_client.py | 545 +++++++ .../octopus_energy/binary_sensor.py | 383 +++++ .../octopus_energy/config_flow.py | 276 ++++ custom_components/octopus_energy/const.py | 36 + .../octopus_energy/diagnostics.py | 42 + .../octopus_energy/manifest.json | 16 + custom_components/octopus_energy/sensor.py | 1317 +++++++++++++++++ .../octopus_energy/sensor_utils.py | 241 +++ .../octopus_energy/services.yaml | 32 + .../octopus_energy/target_sensor_utils.py | 184 +++ .../octopus_energy/translations/en.json | 68 + custom_components/octopus_energy/utils.py | 121 ++ esphome/common/esp32_camera_common.yaml | 6 + 14 files changed, 3442 insertions(+) create mode 100644 custom_components/octopus_energy/__init__.py create mode 100644 custom_components/octopus_energy/api_client.py create mode 100644 custom_components/octopus_energy/binary_sensor.py create mode 100644 custom_components/octopus_energy/config_flow.py create mode 100644 custom_components/octopus_energy/const.py create mode 100644 custom_components/octopus_energy/diagnostics.py create mode 100644 custom_components/octopus_energy/manifest.json create mode 100644 custom_components/octopus_energy/sensor.py create mode 100644 custom_components/octopus_energy/sensor_utils.py create mode 100644 custom_components/octopus_energy/services.yaml create mode 100644 custom_components/octopus_energy/target_sensor_utils.py create mode 100644 custom_components/octopus_energy/translations/en.json create mode 100644 custom_components/octopus_energy/utils.py diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py new file mode 100644 index 00000000..768ff034 --- /dev/null +++ b/custom_components/octopus_energy/__init__.py @@ -0,0 +1,175 @@ +import logging +from datetime import timedelta +import asyncio + +from homeassistant.util.dt import (now, as_utc) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + + CONFIG_TARGET_NAME, + + DATA_CLIENT, + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_RATES, + DATA_ACCOUNT_ID, + DATA_ACCOUNT, + DATA_SAVING_SESSIONS, + DATA_SAVING_SESSIONS_COORDINATOR +) + +from .api_client import OctopusEnergyApiClient + +from .utils import ( + get_active_tariff_code +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry): + """This is called from the config flow.""" + hass.data.setdefault(DOMAIN, {}) + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_dependencies(hass, entry.data) + + # 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") + ) + elif CONFIG_TARGET_NAME in entry.data: + 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_get_current_electricity_agreement_tariff_codes(client, config): + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + tariff_codes = {} + current = now() + if 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 != 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 + +async def async_setup_dependencies(hass, config): + """Setup the coordinator and api client which will be shared by various entities""" + + if DATA_CLIENT not in hass.data[DOMAIN]: + client = OctopusEnergyApiClient(config[CONFIG_MAIN_API_KEY]) + hass.data[DOMAIN][DATA_CLIENT] = client + hass.data[DOMAIN][DATA_ACCOUNT_ID] = config[CONFIG_MAIN_ACCOUNT_ID] + + setup_rates_coordinator(hass, client, config) + + setup_saving_sessions_coordinators(hass, client) + + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + +def setup_rates_coordinator(hass, client, config): + async def async_update_electricity_rates_data(): + """Fetch data from API endpoint.""" + # Only get data every half hour or if we don't have any data + current = now() + if (DATA_RATES not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][DATA_RATES]) == 0): + + tariff_codes = await async_get_current_electricity_agreement_tariff_codes(client, config) + _LOGGER.debug(f'tariff_codes: {tariff_codes}') + + period_from = as_utc(current.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 = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + if new_rates != None: + rates[key] = new_rates + elif (DATA_RATES in hass.data[DOMAIN] and key in hass.data[DOMAIN][DATA_RATES]): + _LOGGER.debug(f"Failed to retrieve new rates for {tariff_code}, so using cached rates") + rates[key] = hass.data[DOMAIN][DATA_RATES][key] + + hass.data[DOMAIN][DATA_RATES] = rates + + return hass.data[DOMAIN][DATA_RATES] + + hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="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), + ) + +def setup_saving_sessions_coordinators(hass, client: OctopusEnergyApiClient): + 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() + if DATA_SAVING_SESSIONS not in hass.data[DOMAIN] or current.minute % 30 == 0: + savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_SAVING_SESSIONS] = savings + + 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), + ) + +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.""" + if CONFIG_MAIN_API_KEY in entry.data: + target_domain = "sensor" + elif CONFIG_TARGET_NAME in entry.data: + target_domain = "binary_sensor" + + unload_ok = all( + await asyncio.gather( + *[hass.config_entries.async_forward_entry_unload(entry, target_domain)] + ) + ) + + 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..1f88c96d --- /dev/null +++ b/custom_components/octopus_energy/api_client.py @@ -0,0 +1,545 @@ +import logging +import json +import aiohttp +from datetime import (timedelta) +from homeassistant.util.dt import (as_utc, now, as_local, parse_datetime) + +from .utils import ( + get_tariff_parts, + get_valid_from, + rates_to_thirty_minute_increments +) + +_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) {{ + serialNumber + smartExportElectricityMeter {{ + deviceId + }} + smartImportElectricityMeter {{ + deviceId + }} + }} + agreements {{ + validFrom + validTo + tariff {{ + ...on StandardTariff {{ + tariffCode + }} + ...on DayNightTariff {{ + tariffCode + }} + ...on ThreeRateTariff {{ + tariffCode + }} + ...on HalfHourlyTariff {{ + tariffCode + }} + ...on PrepayTariff {{ + tariffCode + }} + }} + }} + }} + }} + gasAgreements(active: true) {{ + meterPoint {{ + mprn + meters(includeInactive: false) {{ + serialNumber + consumptionUnits + }} + agreements {{ + validFrom + validTo + tariff {{ + tariffCode + }} + }} + }} + }} + }} +}}''' + +saving_session_query = '''query {{ + savingSessions {{ + account(accountNumber: "{account_id}") {{ + hasJoinedCampaign + joinedEvents {{ + eventId + startAt + endAt + }} + }} + }} + octoPoints {{ + account(accountNumber: "{account_id}") {{ + currentPointsInWallet + }} + }} +}}''' + +class OctopusEnergyApiClient: + + def __init__(self, api_key): + if (api_key == 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() + + async def async_refresh_token(self): + """Get the user's refresh token""" + if (self._graphql_expiration != 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 != None and "data" in token_response_body): + self._graphql_token = token_response_body["data"]["obtainKrakenToken"]["token"] + self._graphql_expiration = now() + timedelta(hours=1) + else: + _LOGGER.error("Failed to retrieve auth token") + raise Exception('Failed to refresh 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(account_response_body) + + if (account_response_body != None and "data" in account_response_body): + 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"] != None, + "is_smart_meter": m["smartImportElectricityMeter"] != None or m["smartExportElectricityMeter"] != None, + }, mp["meterPoint"]["meters"])), + "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, + }, mp["meterPoint"]["agreements"])) + }, account_response_body["data"]["account"]["electricityAgreements"])), + "gas_meter_points": list(map(lambda mp: { + "mprn": mp["meterPoint"]["mprn"], + "meters": list(map(lambda m: { + "serial_number": m["serialNumber"], + "consumption_units": m["consumptionUnits"], + }, mp["meterPoint"]["meters"])), + "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, + }, mp["meterPoint"]["agreements"])) + }, account_response_body["data"]["account"]["gasAgreements"])), + } + 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 != 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_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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract standard rates: {url}') + raise + + 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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + # 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) + for rate in day_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == False: + results.append(rate) + except: + _LOGGER.error(f'Failed to extract day rates: {url}') + raise + + 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: + try: + data = await self.__async_read_response(response, url) + if data == 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) + for rate in night_rates: + if (self.__is_night_rate(rate, is_smart_meter)) == True: + results.append(rate) + except: + _LOGGER.error(f'Failed to extract night rates: {url}') + raise + + # 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) + _LOGGER.debug(results) + + 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) + product_code = tariff_parts["product_code"] + + if (await self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + elif (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 != 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) + product_code = tariff_parts["product_code"] + + if (await self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + + 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: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract standard gas rates: {url}') + raise + + 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 != 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_products(self, is_variable): + """Get all products""" + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'{self._base_url}/v1/products?is_variable={is_variable}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data): + return data["results"] + + return [] + + 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) + product_code = tariff_parts["product_code"] + + if await self.__async_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: + try: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data and len(data["results"]) > 0): + result = { + "value_exc_vat": float(data["results"][0]["value_exc_vat"]), + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + except: + _LOGGER.error(f'Failed to extract electricity standing charges: {url}') + raise + + 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) + product_code = tariff_parts["product_code"] + + if await self.__async_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: + try: + data = await self.__async_read_response(response, url) + if (data != None and "results" in data and len(data["results"]) > 0): + result = { + "value_exc_vat": float(data["results"][0]["value_exc_vat"]), + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } + except: + _LOGGER.error(f'Failed to extract gas standing charges: {url}') + raise + + return result + + async def __async_is_tracker_tariff(self, tariff_code): + tariff_parts = get_tariff_parts(tariff_code) + product_code = tariff_parts["product_code"] + + if product_code in self._product_tracker_cache: + return self._product_tracker_cache[product_code] + + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://api.octopus.energy/v1/products/{product_code}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response(response, url) + if data == None: + return False + + is_tracker = "is_tracker" in data and data["is_tracker"] + self._product_tracker_cache[product_code] = is_tracker + return is_tracker + + async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to): + """Get the tracker rates""" + + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' + async with client.get(url, auth=auth) as response: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + items = [] + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + vat = float(period["breakdown"]["standing_charge"]["VAT"]) + + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + vat = float(period["breakdown"]["unit_charge"]["VAT"]) + items.append( + { + "valid_from": valid_from.strftime("%Y-%m-%dT%H:%M:%SZ"), + "valid_to": valid_to.strftime("%Y-%m-%dT%H:%M:%SZ"), + "value_exc_vat": float(period["unit_rate"]) - vat, + "value_inc_vat": float(period["unit_rate"]), + } + ) + + results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code) + except: + _LOGGER.error(f'Failed to extract tracker gas rates: {url}') + raise + + return results + + async def __async_get_tracker_standing_charge__(self, tariff_code, period_from, period_to): + """Get the tracker standing charge""" + + results = [] + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' + async with client.get(url, auth=auth) as response: + try: + data = await self.__async_read_response(response, url) + if data == None: + return None + + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + vat = float(period["breakdown"]["standing_charge"]["VAT"]) + return { + "value_exc_vat": float(period["standing_charge"]) - vat, + "value_inc_vat": float(period["standing_charge"]) + } + except: + _LOGGER.error(f'Failed to extract tracker gas rates: {url}') + raise + + return None + + 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: + _LOGGER.error(f'Request failed ({url}): {response.status}; {text}') + return None + + try: + return json.loads(text) + except: + raise Exception(f'Failed to extract response json: {url}; {text}') diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py new file mode 100644 index 00000000..d81b4a41 --- /dev/null +++ b/custom_components/octopus_energy/binary_sensor.py @@ -0,0 +1,383 @@ +from datetime import timedelta +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 homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers import config_validation as cv, entity_platform, service +from .const import ( + CONFIG_TARGET_OFFSET, + DOMAIN, + + CONFIG_MAIN_API_KEY, + 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, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_ACCOUNT +) + +from .target_sensor_utils import ( + calculate_continuous_times, + calculate_intermittent_times, + is_target_rate_active +) + +from .sensor_utils import ( + is_saving_sessions_event_active, + get_next_saving_sessions_event +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +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_season_sensors(hass, entry, 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_season_sensors(hass, entry, async_add_entities): + _LOGGER.debug('Setting up Season Saving entity') + 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(saving_session_coordinator)], 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(coordinator, config, is_export)] + async_add_entities(entities, True) + +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, + offset, + 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, + offset, + 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 = is_target_rate_active(current_date, self._target_rates, offset) + + if offset != None and active_result["next_time"] != None: + self._attributes["next_time"] = apply_offset(active_result["next_time"], offset) + else: + self._attributes["next_time"] = active_result["next_time"] + + self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"] + self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"] + + 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() + +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() + self._state = is_saving_sessions_event_active(current_date, self._events) + 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/config_flow.py b/custom_components/octopus_energy/config_flow.py new file mode 100644 index 00000000..0d7605fd --- /dev/null +++ b/custom_components/octopus_energy/config_flow.py @@ -0,0 +1,276 @@ +import re +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 .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + CONFIG_MAIN_ACCOUNT_ID, + + 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, + + DATA_SCHEMA_ACCOUNT, + DATA_CLIENT, + DATA_ACCOUNT_ID, + + REGEX_TIME, + REGEX_ENTITY_NAME, + REGEX_HOURS, + REGEX_OFFSET_PARTS, +) + +from .api_client import OctopusEnergyApiClient + +from .utils import get_active_tariff_code + +_LOGGER = logging.getLogger(__name__) + +def validate_target_rate_sensor(data): + errors = {} + + matches = re.search(REGEX_ENTITY_NAME, data[CONFIG_TARGET_NAME]) + if matches == None: + errors[CONFIG_TARGET_NAME] = "invalid_target_name" + + # For some reason float type isn't working properly - reporting user input malformed + matches = re.search(REGEX_HOURS, data[CONFIG_TARGET_HOURS]) + if matches == None: + errors[CONFIG_TARGET_HOURS] = "invalid_target_hours" + else: + data[CONFIG_TARGET_HOURS] = float(data[CONFIG_TARGET_HOURS]) + 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 == 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 == 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 == None: + errors[CONFIG_TARGET_OFFSET] = "invalid_offset" + + return errors + +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 = {} + + client = OctopusEnergyApiClient(user_input[CONFIG_MAIN_API_KEY]) + account_info = await client.async_get_account(user_input[CONFIG_MAIN_ACCOUNT_ID]) + if (account_info == 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="Octopus Energy", + 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]) + + meters = [] + now = utcnow() + if 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 != None: + meters.append(point["mpan"]) + + 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, + }) + + async def async_step_target_rate(self, user_input): + """Setup a target based on the provided user input""" + + errors = validate_target_rate_sensor(user_input) + + 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]) + + meters = [] + now = utcnow() + if 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 != None: + meters.append(point["mpan"]) + + 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] + + return self.async_show_form( + step_id="target_rate", + data_schema=vol.Schema({ + vol.Required(CONFIG_TARGET_HOURS, default=f'{config[CONFIG_TARGET_HOURS]}'): str, + 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, + }), + 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) + + 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, + }) + ) + 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) + 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) + + errors = validate_target_rate_sensor(config) + + 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..a0f69ac0 --- /dev/null +++ b/custom_components/octopus_energy/const.py @@ -0,0 +1,36 @@ +import voluptuous as vol + +DOMAIN = "octopus_energy" + +CONFIG_MAIN_API_KEY = "Api key" +CONFIG_MAIN_ACCOUNT_ID = "Account Id" + +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" + +DATA_CONFIG = "CONFIG" +DATA_ELECTRICITY_RATES_COORDINATOR = "ELECTRICITY_RATES_COORDINATOR" +DATA_CLIENT = "CLIENT" +DATA_RATES = "RATES" +DATA_GAS_TARIFF_CODE = "GAS_TARIFF_CODE" +DATA_ACCOUNT_ID = "ACCOUNT_ID" +DATA_ACCOUNT = "ACCOUNT" +DATA_SAVING_SESSIONS = "SAVING_SESSIONS" +DATA_SAVING_SESSIONS_COORDINATOR = "SAVING_SESSIONS_COORDINATOR" + +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_]+$" +REGEX_TARIFF_PARTS = "^([A-Z])-([0-9A-Z]+)-([A-Z0-9-]+)-([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, +}) diff --git a/custom_components/octopus_energy/diagnostics.py b/custom_components/octopus_energy/diagnostics.py new file mode 100644 index 00000000..eaa2f23b --- /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 = len(account_info["electricity_meter_points"]) + if 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" }) + + points_length = len(account_info["gas_meter_points"]) + if 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" }) + + _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/manifest.json b/custom_components/octopus_energy/manifest.json new file mode 100644 index 00000000..f0b47316 --- /dev/null +++ b/custom_components/octopus_energy/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "octopus_energy", + "name": "Octopus Energy", + "config_flow": true, + "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/", + "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@bottlecapdave" + ], + "version": "5.4.1", + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py new file mode 100644 index 00000000..dfdd3272 --- /dev/null +++ b/custom_components/octopus_energy/sensor.py @@ -0,0 +1,1317 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, now, as_utc, parse_datetime) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass +) +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + VOLUME_CUBIC_METERS +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .sensor_utils import ( + async_get_consumption_data, + calculate_electricity_consumption, + async_calculate_electricity_cost, + calculate_gas_consumption, + async_calculate_gas_cost +) + +from .utils import (get_active_tariff_code) +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + + DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_SAVING_SESSIONS_COORDINATOR, + DATA_CLIENT, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +def create_reading_coordinator(hass, client, is_electricity, identifier, serial_number): + """Create reading coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + + previous_consumption_key = f'{identifier}_{serial_number}_previous_consumption' + previous_data = None + if previous_consumption_key in hass.data[DOMAIN]: + previous_data = hass.data[DOMAIN][previous_consumption_key] + + 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)) + + data = await async_get_consumption_data( + client, + previous_data, + utcnow(), + period_from, + period_to, + identifier, + serial_number, + is_electricity + ) + + if data != None and len(data) > 0: + hass.data[DOMAIN][previous_consumption_key] = data + return data + + return [] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="rates", + 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}_consumption_coordinator'] = coordinator + + return coordinator + +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, entry, async_add_entities): + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + client = hass.data[DOMAIN][DATA_CLIENT] + + rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] + + await rate_coordinator.async_config_entry_first_refresh() + + saving_session_coordinator = hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] + + await saving_session_coordinator.async_config_entry_first_refresh() + + entities = [OctopusEnergySavingSessionPoints(saving_session_coordinator)] + + 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 != None: + for meter in point["meters"]: + _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeElectricityReading(coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(coordinator, client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityCurrentRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityPreviousRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityNextRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityCurrentStandingCharge(client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + 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 != None: + for meter in point["meters"]: + _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyPreviousAccumulativeGasReadingKwh(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, gas_tariff_code, point["mprn"], meter["serial_number"], meter["consumption_units"])) + entities.append(OctopusEnergyGasCurrentRate(client, gas_tariff_code, point["mprn"], meter["serial_number"])) + entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, point["mprn"], meter["serial_number"])) + 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) + +class OctopusEnergyElectricitySensor(SensorEntity, RestoreEntity): + def __init__(self, mpan, serial_number, is_export, is_smart_meter): + """Init sensor""" + self._mpan = mpan + self._serial_number = serial_number + self._is_export = is_export + self._is_smart_meter = is_smart_meter + + 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": "Electricity Meter", + } + +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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"] + }, rate)) + self._attributes = { + "rate": current_rate, + "is_export": self._is_export, + "is_smart_meter": self._is_smart_meter, + "rates": ratesAttributes + } + + self._state = current_rate["value_inc_vat"] / 100 + 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_previous_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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 = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the next rate.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + # Pass coordinator to base class + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + self._state = None + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_next_rate" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: + _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 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 = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyElectricityCurrentStandingCharge(OctopusEnergyElectricitySensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_current_standing_charge'; + + @property + def name(self): + """Name of the sensor.""" + return f'Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeElectricityReading(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity reading.""" + + def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the previous days accumulative electricity cost.""" + + def __init__(self, coordinator, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) + + 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}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): + def __init__(self, mprn, serial_number): + """Init sensor""" + self._mprn = mprn + self._serial_number = 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", + } + +class OctopusEnergyGasCurrentRate(OctopusEnergyGasSensor): + """Sensor for displaying the current rate.""" + + def __init__(self, client, tariff_code, mprn, serial_number): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + 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_rate'; + + @property + def name(self): + """Name of the sensor.""" + return f'Octopus Energy 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""" + 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 OctopusEnergyGasCurrentRate') + + 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"))) + + rates = await self._client.async_get_gas_rates(self._tariff_code, period_from, period_to) + + current_rate = None + if rates != None: + for period in rates: + if utc_now >= period["valid_from"] and utc_now <= period["valid_to"]: + current_rate = period + break + + if current_rate != None: + self._latest_date = period_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"] = period_from + current_rate["valid_to"] = period_to + self._attributes = current_rate + else: + self._state = None + self._attributes = {} + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyGasCurrentStandingCharge(OctopusEnergyGasSensor): + """Sensor for displaying the current standing charge.""" + + def __init__(self, client, tariff_code, mprn, serial_number): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + 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'Octopus Energy 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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasReading(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading.""" + + def __init__(self, coordinator, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._native_consumption_units = native_consumption_units + 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}_previous_accumulative_consumption" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy 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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasReadingKwh(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas reading in kwh.""" + + def __init__(self, coordinator, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._native_consumption_units = native_consumption_units + 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}_previous_accumulative_consumption_kwh" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption (kWh)" + + @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: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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): + """Sensor for displaying the previous days accumulative gas cost.""" + + def __init__(self, coordinator, client, tariff_code, mprn, serial_number, native_consumption_units): + """Init sensor.""" + super().__init__(coordinator) + OctopusEnergyGasSensor.__init__(self, mprn, serial_number) + + self._client = client + self._tariff_code = tariff_code + self._native_consumption_units = native_consumption_units + + 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}_previous_accumulative_cost" + + @property + def name(self): + """Name of the sensor.""" + return f"Octopus Energy 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 + ) + + 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"] + } + + 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 + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') + +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: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + if (self._state is None): + self._state = 0 + + _LOGGER.debug(f'Restored state: {self._state}') \ No newline at end of file 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/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/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/translations/en.json b/custom_components/octopus_energy/translations/en.json new file mode 100644 index 00000000..f0538d05 --- /dev/null +++ b/custom_components/octopus_energy/translations/en.json @@ -0,0 +1,68 @@ +{ + "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)" + } + }, + "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.", + "data": { + "entity_id": "The name of your target", + "Hours": "The hours you require.", + "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" + } + } + }, + "error": { + "account_not_found": "Account information was not found", + "invalid_target_hours": "Target hours must be in half hour increments.", + "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" + }, + "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" + } + }, + "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.", + "data": { + "Hours": "The hours you require.", + "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" + } + } + }, + "error": { + "invalid_target_hours": "Target hours must be in half hour increments.", + "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" + }, + "abort": { + "not_supported": "Configuration for target rates is not supported at the moment." + } + } +} \ 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/esphome/common/esp32_camera_common.yaml b/esphome/common/esp32_camera_common.yaml index ac7bbff7..b43b8444 100644 --- a/esphome/common/esp32_camera_common.yaml +++ b/esphome/common/esp32_camera_common.yaml @@ -49,6 +49,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 From 0cfbc52616f6e7ac82a41a84182541f043e7a6eb Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 22 Feb 2023 22:55:21 +0000 Subject: [PATCH 030/158] Use additional sensor --- packages/device_alerts.yaml | 3 ++- packages/ensuite.yaml | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/device_alerts.yaml b/packages/device_alerts.yaml index de9378fd..e280a52d 100644 --- a/packages/device_alerts.yaml +++ b/packages/device_alerts.yaml @@ -18,7 +18,8 @@ automation: - 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 diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index 9b7ec87b..27626980 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -1,4 +1,18 @@ --- +binary_sensor: + - platform: template + sensors: + bathroom_window: + friendly_name: Bathroom 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 From 4ea9fe910d4eded31b2b896c1182c36ca30b69a0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 22 Feb 2023 22:55:46 +0000 Subject: [PATCH 031/158] Improve yamllint --- packages/device_alerts.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/device_alerts.yaml b/packages/device_alerts.yaml index e280a52d..d9dc41d4 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: From f8fe8ffd7af90e20112e5013f2fad3cbd694ad4d Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 22 Feb 2023 23:29:09 +0000 Subject: [PATCH 032/158] Fix stupid mistake --- packages/ensuite.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index 27626980..7f15a2a3 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -2,8 +2,8 @@ binary_sensor: - platform: template sensors: - bathroom_window: - friendly_name: Bathroom Window + ensuite_window: + friendly_name: Ensuite Window device_class: window icon_template: >- {% if is_state('binary_sensor.ensuite_window','on') %} From 7040c782d5a03855566d6eafc3781c23ecbc89ec Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 23 Feb 2023 14:36:13 +0000 Subject: [PATCH 033/158] Stream to Frigate --- esphome/common/esp32_camera_common.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From f230f22bb9f780c2b6c8fab191aa52384d0aac58 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 6 Mar 2023 21:13:00 +0100 Subject: [PATCH 034/158] Bring in white channels --- esphome/common/h801_common.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/common/h801_common.yaml b/esphome/common/h801_common.yaml index 265bdece..7b0d3494 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" + - platform: rgbww + name: ${friendly_name} red: pwm_r green: pwm_g blue: pwm_b + cold_white: pwm_w1 + warm_white: pwm_w2 id: thelight effects: - random: From a570789136cb7c1f7f03d938d43d70c79077913c Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 6 Mar 2023 21:17:43 +0100 Subject: [PATCH 035/158] 3 control devices --- esphome/tin_hut_shelf_lights_bench.yaml | 7 +++++++ esphome/tin_hut_shelf_lights_left.yaml | 7 +++++++ esphome/tin_hut_shelf_lights_right.yaml | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 esphome/tin_hut_shelf_lights_bench.yaml create mode 100644 esphome/tin_hut_shelf_lights_left.yaml create mode 100644 esphome/tin_hut_shelf_lights_right.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 From 7f1437189458052bc89289b45ad2e081e65a013e Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 6 Mar 2023 21:25:32 +0100 Subject: [PATCH 036/158] Fix warning --- esphome/common/common.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e758656bd4b8fbaea0e5bd335eac7dc45a54e142 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 8 Mar 2023 19:27:28 +0000 Subject: [PATCH 037/158] Combine craft room into package --- automation/craftroom_touch_toggle.yaml | 17 ------------- packages/craft_room.yaml | 34 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 17 deletions(-) delete mode 100644 automation/craftroom_touch_toggle.yaml create mode 100644 packages/craft_room.yaml 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/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" From 4d48ffa45575023f28542c06473e33f58b80cdcb Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 11 Mar 2023 20:38:14 +0000 Subject: [PATCH 038/158] Frigate integration 3.0.1 --- custom_components/frigate/__init__.py | 20 ++----------- custom_components/frigate/camera.py | 2 +- custom_components/frigate/manifest.json | 2 +- custom_components/frigate/media_source.py | 6 ++-- custom_components/frigate/update.py | 2 +- custom_components/frigate/views.py | 35 +++++++++++++++++++++-- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/custom_components/frigate/__init__.py b/custom_components/frigate/__init__.py index 21546fc1..030702f8 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,14 +164,7 @@ 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 @@ -286,7 +272,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/camera.py b/custom_components/frigate/camera.py index 10aea4e7..f8991250 100644 --- a/custom_components/frigate/camera.py +++ b/custom_components/frigate/camera.py @@ -209,7 +209,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() diff --git a/custom_components/frigate/manifest.json b/custom_components/frigate/manifest.json index 00b38066..36a84e99 100644 --- a/custom_components/frigate/manifest.json +++ b/custom_components/frigate/manifest.json @@ -2,7 +2,7 @@ "domain": "frigate", "documentation": "https://github.com/blakeblackshear/frigate", "name": "Frigate", - "version": "3.0.0", + "version": "3.0.1", "issue_tracker": "https://github.com/blakeblackshear/frigate-hass-integration/issues", "dependencies": [ "http", diff --git a/custom_components/frigate/media_source.py b/custom_components/frigate/media_source.py index 2151ec9b..365fed57 100644 --- a/custom_components/frigate/media_source.py +++ b/custom_components/frigate/media_source.py @@ -496,9 +496,9 @@ def get_integration_proxy_path(self) -> str: 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} + for attribute in self.__attrs_attrs__: + if getattr(self, attribute.name) is None: # type: ignore[attr-defined] + return {attribute.name: data} # type: ignore[attr-defined] raise ValueError("No empty attribute available") @property 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..60db1471 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,18 @@ 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(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 +224,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 +461,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, ) From d9d029d4aa626cf9e92cf4de8f1e2f7ed6c0ca2b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 15 Mar 2023 22:38:36 +0000 Subject: [PATCH 039/158] Improve linting --- esphome/bandwidthometer.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/bandwidthometer.yaml b/esphome/bandwidthometer.yaml index bdedcceb..f42a54d8 100644 --- a/esphome/bandwidthometer.yaml +++ b/esphome/bandwidthometer.yaml @@ -1,3 +1,4 @@ +--- substitutions: device_name: bandwidthometer device_description: Bandwidthometer Display @@ -82,7 +83,7 @@ light: interval: - interval: 500ms then: - - if: + - if: condition: not: wifi.connected: From b5f2733c9692936cadbe44e5708c13ba78594ae2 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 15 Mar 2023 22:43:04 +0000 Subject: [PATCH 040/158] Convert to neopixelbus --- esphome/bandwidthometer.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/bandwidthometer.yaml b/esphome/bandwidthometer.yaml index f42a54d8..7e99a3ba 100644 --- a/esphome/bandwidthometer.yaml +++ b/esphome/bandwidthometer.yaml @@ -13,8 +13,6 @@ esphome: esp8266: board: d1_mini - framework: - version: 2.7.4 captive_portal: @@ -62,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: From 632e4572df0b39ca09283243ff2580eb5f10dd7c Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 16 Mar 2023 08:36:26 +0000 Subject: [PATCH 041/158] Fix interval --- esphome/bandwidthometer.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/bandwidthometer.yaml b/esphome/bandwidthometer.yaml index 7e99a3ba..7282d689 100644 --- a/esphome/bandwidthometer.yaml +++ b/esphome/bandwidthometer.yaml @@ -81,7 +81,7 @@ light: interval: - interval: 500ms then: - - if: + - if: condition: not: wifi.connected: From 458dafe9d3b18bf4e3b1da379da201405b42b7b3 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 16 Mar 2023 13:16:48 +0000 Subject: [PATCH 042/158] Fix deprecation issues here --- esphome/ili9341_1.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/ili9341_1.yaml b/esphome/ili9341_1.yaml index 6a1f1f2c..46043e5d 100644 --- a/esphome/ili9341_1.yaml +++ b/esphome/ili9341_1.yaml @@ -102,8 +102,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 From de21061fe84825405501693423f48e68aa7c18c6 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 22 Mar 2023 10:47:52 +0000 Subject: [PATCH 043/158] Tidy up BLE tracker --- esphome/ble_proxy_1.yaml | 34 ----------- esphome/common/esp32-ble-tracker.yaml | 83 +++++++++++++++++++++++++++ esphome/esp32-ble-tracker-1.yaml | 7 +++ 3 files changed, 90 insertions(+), 34 deletions(-) delete mode 100644 esphome/ble_proxy_1.yaml create mode 100644 esphome/common/esp32-ble-tracker.yaml create mode 100644 esphome/esp32-ble-tracker-1.yaml diff --git a/esphome/ble_proxy_1.yaml b/esphome/ble_proxy_1.yaml deleted file mode 100644 index da80624c..00000000 --- a/esphome/ble_proxy_1.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -packages: - common: !include common/common.yaml - -substitutions: - device_name: bluetooth_proxy_1 - device_description: Bluetooth Proxy 1 - friendly_name: Bluetooth Proxy 1 - -esphome: - name: ${device_name} - comment: ${device_description} - -esp32: - board: nodemcu-32s - -# captive_portal: - -logger: - -web_server: - -time: - - platform: homeassistant - id: esptime - timezone: Europe/London - -bluetooth_proxy: - -esp32_ble_tracker: - scan_parameters: - interval: 1100ms - window: 1100ms - active: true 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/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 From 2fd8b190d58547de7206c265abff1dbca7f0befc Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 3 Apr 2023 21:29:48 +0100 Subject: [PATCH 044/158] Slider entity row --- www/slider-entity-row.js | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 www/slider-entity-row.js diff --git a/www/slider-entity-row.js b/www/slider-entity-row.js new file mode 100644 index 00000000..e30198cc --- /dev/null +++ b/www/slider-entity-row.js @@ -0,0 +1,82 @@ +function t(t,e,s,i){var r,n=arguments.length,a=n<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,s):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,s,i);else for(var o=t.length-1;o>=0;o--)(r=t[o])&&(a=(n<3?r(a):n>3?r(e,s,a):r(e,s))||a);return n>3&&a&&Object.defineProperty(e,s,a),a}const e=window,s=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),r=new WeakMap;let n=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==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(s&&void 0===t){const s=void 0!==e&&1===e.length;s&&(t=r.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&r.set(e,t))}return t}toString(){return this.cssText}};const a=(t,...e)=>{const s=1===t.length?t[0]:e.reduce(((e,s,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.")})(s)+t[i+1]),t[0]);return new n(s,t,i)},o=s?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,i))(e)})(t):t;var l;const h=window,u=h.trustedTypes,c=u?u.emptyScript:"",d=h.reactiveElementPolyfillSupport,_={toAttribute(t,e){switch(e){case Boolean:t=t?c:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let s=t;switch(e){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t)}catch(t){s=null}}return s}},p=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:_,reflect:!1,hasChanged:p};let b=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,s)=>{const i=this._$Ep(s,e);void 0!==i&&(this._$Ev.set(i,s),t.push(i))})),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 s="symbol"==typeof t?Symbol():"__"+t,i=this.getPropertyDescriptor(t,s,e);void 0!==i&&Object.defineProperty(this.prototype,t,i)}}static getPropertyDescriptor(t,e,s){return{get(){return this[e]},set(i){const r=this[t];this[e]=i,this.requestUpdate(t,r,s)},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(),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 s of e)this.createProperty(s,t[s])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const t of s)e.unshift(o(t))}else void 0!==t&&e.push(o(t));return e}static _$Ep(t,e){const s=e.attribute;return!1===s?void 0:"string"==typeof s?s:"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,s;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.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)=>{s?t.adoptedStyleSheets=i.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):i.forEach((s=>{const i=document.createElement("style"),r=e.litNonce;void 0!==r&&i.setAttribute("nonce",r),i.textContent=s.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,s){this._$AK(t,s)}_$EO(t,e,s=g){var i;const r=this.constructor._$Ep(t,s);if(void 0!==r&&!0===s.reflect){const n=(void 0!==(null===(i=s.converter)||void 0===i?void 0:i.toAttribute)?s.converter:_).toAttribute(e,s.type);this._$El=t,null==n?this.removeAttribute(r):this.setAttribute(r,n),this._$El=null}}_$AK(t,e){var s;const i=this.constructor,r=i._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=i.getPropertyOptions(r),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(s=t.converter)||void 0===s?void 0:s.fromAttribute)?t.converter:_;this._$El=r,this[r]=n.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,s){let i=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||p)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===s.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,s))):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 s=this._$AL;try{e=this.shouldUpdate(s),e?(this.willUpdate(s),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(s)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(s)}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){}};var v;b.finalized=!0,b.elementProperties=new Map,b.elementStyles=[],b.shadowRootOptions={mode:"open"},null==d||d({ReactiveElement:b}),(null!==(l=h.reactiveElementVersions)&&void 0!==l?l:h.reactiveElementVersions=[]).push("1.5.0");const f=window,m=f.trustedTypes,$=m?m.createPolicy("lit-html",{createHTML:t=>t}):void 0,y=`lit$${(Math.random()+"").slice(9)}$`,O="?"+y,w=`<${O}>`,j=document,A=(t="")=>j.createComment(t),S=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,x=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,C=/-->/g,T=/>/g,k=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),P=/'/g,M=/"/g,U=/^(?:script|style|textarea|title)$/i,H=(t=>(e,...s)=>({_$litType$:t,strings:e,values:s}))(1),R=Symbol.for("lit-noChange"),N=Symbol.for("lit-nothing"),z=new WeakMap,L=j.createTreeWalker(j,129,null,!1),I=(t,e)=>{const s=t.length-1,i=[];let r,n=2===e?"":"",a=x;for(let e=0;e"===l[0]?(a=null!=r?r:x,h=-1):void 0===l[1]?h=-2:(h=a.lastIndex-l[2].length,o=l[1],a=void 0===l[3]?k:'"'===l[3]?M:P):a===M||a===P?a=k:a===C||a===T?a=x:(a=k,r=void 0);const c=a===k&&t[e+1].startsWith("/>")?" ":"";n+=a===x?s+w:h>=0?(i.push(o),s.slice(0,h)+"$lit$"+s.slice(h)+y+c):s+y+(-2===h?(i.push(void 0),e):c)}const o=n+(t[s]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==$?$.createHTML(o):o,i]};class D{constructor({strings:t,_$litType$:e},s){let i;this.parts=[];let r=0,n=0;const a=t.length-1,o=this.parts,[l,h]=I(t,e);if(this.el=D.createElement(l,s),L.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(i=L.nextNode())&&o.length0){i.textContent=m?m.emptyScript:"";for(let s=0;sE(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!==N&&S(this._$AH)?this._$AA.nextSibling.data=t:this.T(j.createTextNode(t)),this._$AH=t}$(t){var e;const{values:s,_$litType$:i}=t,r="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=D.createElement(i.h,this.options)),i);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.p(s);else{const t=new W(r,this),e=t.v(this.options);t.p(s),this.T(e),this._$AH=t}}_$AC(t){let e=z.get(t.strings);return void 0===e&&z.set(t.strings,e=new D(t)),e}k(t){E(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let s,i=0;for(const r of t)i===e.length?e.push(s=new V(this.O(A()),this.O(A()),this,this.options)):s=e[i],s._$AI(r),i++;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=N}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,s,i){const r=this.strings;let n=!1;if(void 0===r)t=B(this,t,e,0),n=!S(t)||t!==this._$AH&&t!==R,n&&(this._$AH=t);else{const i=t;let a,o;for(t=r[0],a=0;a{var i,r;const n=null!==(i=null==s?void 0:s.renderBefore)&&void 0!==i?i:e;let a=n._$litPart$;if(void 0===a){const t=null!==(r=null==s?void 0:s.renderBefore)&&void 0!==r?r:null;n._$litPart$=a=new V(e.insertBefore(A(),t),t,void 0,null!=s?s:{})}return a._$AI(t),a})(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 R}}tt.finalized=!0,tt._$litElement$=!0,null===(Q=globalThis.litElementHydrateSupport)||void 0===Q||Q.call(globalThis,{LitElement:tt});const et=globalThis.litElementPolyfillSupport;null==et||et({LitElement:tt}),(null!==(X=globalThis.litElementVersions)&&void 0!==X?X:globalThis.litElementVersions=[]).push("3.2.2");const st=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(s){s.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(s){s.createProperty(e.key,t)}};function it(t){return(e,s)=>void 0!==s?((t,e,s)=>{e.constructor.createProperty(s,t)})(t,e,s):st(t,e)}var rt;null===(rt=window.HTMLSlotElement)||void 0===rt||rt.prototype.assignedElements;class nt{constructor(t){this._config=t}set hass(t){this._hass=t,this.stateObj=t.states[this._config.entity]}get value(){return this._value?Math.round(this._value/this.step)*this.step:0}set value(t){t!==this.value&&(this._value=t)}get string(){return`${this.value}`}get hidden(){return!1}get hasSlider(){return!0}get hasToggle(){return!0}get background(){}renderToggle(t){return this.hasToggle?H` + + `:void 0}get isOff(){return!this.stateObj||"unavailable"===this.stateObj.state||("off"===this.stateObj.state||0===this.value)}get min(){var t,e;return null!==(e=null!==(t=this._config.min)&&void 0!==t?t:this._min)&&void 0!==e?e:0}get max(){var t,e;return null!==(e=null!==(t=this._config.max)&&void 0!==t?t:this._max)&&void 0!==e?e:100}get step(){var t,e;return null!==(e=null!==(t=this._config.step)&&void 0!==t?t:this._step)&&void 0!==e?e:5}get dir(){var t;return null!==(t=this._config.dir)&&void 0!==t?t:void 0}}const at={red:0,green:1,blue:2,white:3,cold_white:3,warm_white:4},ot={hue:0,saturation:1};class lt extends nt{get attribute(){return this._config.attribute||"brightness_pct"}get _rgbww(){const t=this.stateObj.attributes;switch(t.color_mode){case"rgb":case"hs":case"xy":return[...t.rgb_color,0,0];case"rgbw":return[...t.rgbw_color,0];case"rgbww":return[...t.rgbww_color];default:return[0,0,0,0,0]}}get _value(){if(!this.stateObj||"on"!==this.stateObj.state)return 0;const t=this.stateObj.attributes;switch(this.attribute){case"color_temp":return Math.round(t.color_temp_kelvin);case"color_temp_mired":return Math.round(t.color_temp);case"white_value":return Math.round(t.white_value);case"brightness":return Math.round(t.brightness);case"brightness_pct":return Math.round(100*t.brightness/255);case"red":case"green":case"blue":return t.rgb_color?Math.round(this._rgbww[at[this.attribute]]):0;case"white":return t.rgbw_color?Math.round(100*this._rgbww[at[this.attribute]]/255):0;case"cold_white":case"warm_white":return t.rgbww_color?Math.round(100*this._rgbww[at[this.attribute]]/255):0;case"hue":case"saturation":return t.hs_color?Math.round(t.hs_color[ot[this.attribute]]):0;case"effect":return t.effect_list?t.effect_list.indexOf(t.effect):0;default:return 0}}get _step(){switch(this.attribute){case"effect":return 1;default:return 5}}get _min(){switch(this.attribute){case"color_temp":return this.stateObj?this.stateObj.attributes.min_color_temp_kelvin:0;case"color_temp_mired":return this.stateObj?this.stateObj.attributes.min_mireds:0;default:return 0}}get _max(){switch(this.attribute){case"color_temp":return this.stateObj?this.stateObj.attributes.max_color_temp_kelvin:0;case"color_temp_mired":return this.stateObj?this.stateObj.attributes.max_mireds:0;case"red":case"green":case"blue":case"white_value":case"brightness":return 255;case"hue":return 360;case"effect":return this.stateObj&&this.stateObj.attributes.effect_list?this.stateObj.attributes.effect_list.length-1:0;default:return 100}}get isOff(){return"on"!==this.stateObj.state}set _value(t){if(!this.stateObj)return;const e=this.stateObj.attributes.color_mode;let s,i=this.attribute,r=!0;switch(i){case"brightness":case"brightness_pct":(t="brightness"===i?Math.round(t):Math.round(t/100*255))||(r=!1),i="brightness";break;case"red":case"green":case"blue":if(s=this._rgbww,s[at[i]]=t,"rgbww"===e){i="rgbww_color",t=s;break}if("rgbw"===e){i="rgbw_color",t=s.slice(0,4);break}i="rgb_color",t=s.slice(0,3);break;case"white":s=this._rgbww,s[at[i]]=Math.round(t/100*255),t=s.slice(0,4),i="rgbw_color";break;case"cold_white":case"warm_white":s=this._rgbww,s[at[i]]=Math.round(t/100*255),t=s,i="rgbww_color";break;case"hue":case"saturation":s=this.stateObj.attributes.hs_color||[0,0],s[ot[i]]=t,t=s,i="hs_color";break;case"effect":t=this.stateObj.attributes.effect_list[t],i="effect";break;case"color_temp":i="kelvin";break;case"color_temp_mired":i="color_temp"}r?this._hass.callService("light","turn_on",{entity_id:this.stateObj.entity_id,[i]:t}):this._hass.callService("light","turn_off",{entity_id:this.stateObj.entity_id})}get string(){if(this.stateObj&&"off"===this.stateObj.state)return this._hass.localize("component.light.state._.off");switch(this.attribute){case"color_temp_mired":case"brightness":return`${this.value}`;case"color_temp":return`${this.value} K`;case"brightness_pct":case"saturation":return`${this.value} %`;case"hue":return`${this.value} °`;case"effect":return this.stateObj?this.stateObj.attributes.effect:"";default:return this.value}}get hasSlider(){var t,e,s,i,r,n,a;const o=this.stateObj.attributes,l=2&o.supported_features||(null===(t=o.supported_color_modes)||void 0===t?void 0:t.some((t=>["color_temp"].includes(t)))),h=16&o.supported_features||(null===(e=o.supported_color_modes)||void 0===e?void 0:e.some((t=>["rgb","rgbw","rgbww"].includes(t)))),u=null===(s=o.supported_color_modes)||void 0===s?void 0:s.some((t=>["rgbw"].includes(t))),c=null===(i=o.supported_color_modes)||void 0===i?void 0:i.some((t=>["rgbww"].includes(t))),d=16&o.supported_features||(null===(r=o.supported_color_modes)||void 0===r?void 0:r.some((t=>["hs"].includes(t)))),_=null===(n=o.supported_color_modes)||void 0===n?void 0:n.some((t=>["xy"].includes(t))),p=1&o.supported_features||l||h||d||_||(null===(a=o.supported_color_modes)||void 0===a?void 0:a.some((t=>["brightness"].includes(t)))),g=h||d;if(!this.stateObj)return!1;switch(this.attribute){case"brightness":case"brightness_pct":return!(!("brightness"in this.stateObj.attributes)&&!p);case"color_temp":case"color_temp_mired":return!(!("color_temp"in this.stateObj.attributes)&&!l);case"white_value":return!!(128&o.supported_features||"white_value"in this.stateObj.attributes);case"white":return!!u;case"cold_white":case"warm_white":return!!c;case"red":case"green":case"blue":return!(!("rgb_color"in this.stateObj.attributes)&&!g);case"hue":case"saturation":return!(!("hs_color"in this.stateObj.attributes)&&!g);case"effect":return"effect"in this.stateObj.attributes||"effect_list"in this.stateObj.attributes;default:return!1}}get background(){return"hue"===this.attribute?"linear-gradient(to right,red,yellow,green,cyan,blue,magenta,red)":"color_temp_mired"===this.attribute?"linear-gradient(to right,rgb(166,209,255),rgb(255,255,255),rgb(255,160,0))":"color_temp"===this.attribute?"linear-gradient(to left,rgb(166,209,255),rgb(255,255,255),rgb(255,160,0))":"red"===this.attribute?"linear-gradient(to right,rgb(0,0,0),rgb(255,0,0))":"green"===this.attribute?"linear-gradient(to right,rgb(0,0,0),rgb(0,255,0))":"blue"===this.attribute?"linear-gradient(to right,rgb(0,0,0),rgb(0,0,255))":"brightness"===this.attribute?"linear-gradient(to right,rgb(0,0,0),rgb(255,255,255))":void 0}}class ht extends nt{get _value(){return this.stateObj.attributes.is_volume_muted?0:Math.round(100*this.stateObj.attributes.volume_level)}set _value(t){t/=100,this._hass.callService("media_player","volume_set",{entity_id:this.stateObj.entity_id,volume_level:t}),t&&this.stateObj.attributes.is_volume_muted&&this._hass.callService("media_player","volume_mute",{entity_id:this.stateObj.entity_id,is_volume_muted:!1})}get isOff(){return"off"===this.stateObj.state}get string(){return this.stateObj.attributes.is_volume_muted?"-":this.stateObj.attributes.volume_level?`${this.value} %`:this._hass.localize("component.media_player.state._.off")}get hasToggle(){return!0}_handleMute(){this._hass.callService("media_player","volume_mute",{entity_id:this.stateObj.entity_id,is_volume_muted:!this.stateObj.attributes.is_volume_muted})}renderToggle(t){const e=t.states[this.stateObj.entity_id],s=document.createElement("ha-icon");s.style.display="flex",s.icon=e.attributes.is_volume_muted?"mdi:volume-off":"mdi:volume-high";const i=document.createElement("ha-icon-button");return i.appendChild(s),i.addEventListener("click",(()=>this._handleMute())),this.hasToggle?i:void 0}}class ut extends nt{get _value(){return this.stateObj.attributes.temperature}set _value(t){this._hass.callService("climate","set_temperature",{entity_id:this.stateObj.entity_id,temperature:t})}get string(){return`${this.value} ${this._hass.config.unit_system.temperature}`}get isOff(){return"off"===this.stateObj.state}get _min(){return this.stateObj.attributes.min_temp}get _max(){return this.stateObj.attributes.max_temp}get _step(){return 1}}class ct extends nt{get attribute(){return this._config.attribute||"position"}get _value(){switch(this.attribute){case"position":return"closed"===this.stateObj.state?0:this.stateObj.attributes.current_position;case"tilt":return this.stateObj.attributes.current_tilt_position;default:return 0}}set _value(t){switch(this.attribute){case"position":this._hass.callService("cover","set_cover_position",{entity_id:this.stateObj.entity_id,position:t});break;case"tilt":this._hass.callService("cover","set_cover_tilt_position",{entity_id:this.stateObj.entity_id,tilt_position:t})}}get string(){if(!this.hasSlider)return"";switch(this.attribute){case"position":return"closed"===this.stateObj.state?this._hass.localize("component.cover.state._.closed"):100===this.value?this._hass.localize("component.cover.state._.open"):`${this.value} %`;case"tilt":return`${this.value} %`}}get hasToggle(){return!1}get hasSlider(){switch(this.attribute){case"position":if("current_position"in this.stateObj.attributes)return!0;if("supported_features"in this.stateObj.attributes&&4&this.stateObj.attributes.supported_features)return!0;case"tilt":if("current_tilt_position"in this.stateObj.attributes)return!0;if("supported_features"in this.stateObj.attributes&&128&this.stateObj.attributes.supported_features)return!0;default:return!1}}get _step(){return 10}}class dt extends nt{get _value(){return"off"!==this.stateObj.state?this.stateObj.attributes.percentage:0}set _value(t){t>0?this._hass.callService("fan","set_percentage",{entity_id:this.stateObj.entity_id,percentage:t}):this._hass.callService("fan","turn_off",{entity_id:this.stateObj.entity_id})}get string(){return"off"===this.stateObj.state?this._hass.localize("component.fan.state._.off"):`${this.stateObj.attributes.percentage} %`}get hasSlider(){return"percentage"in this.stateObj.attributes}get _step(){return this.stateObj.attributes.percentage_step}}class _t extends nt{get _value(){return this.stateObj.state}set _value(t){this._hass.callService("input_number","set_value",{entity_id:this.stateObj.entity_id,value:t})}get string(){return`${parseFloat(this.stateObj.state)} ${this.stateObj.attributes.unit_of_measurement||""}`.trim()}get isOff(){return!1}get hasToggle(){return!1}get hasSlider(){return"slider"===this.stateObj.attributes.mode}get _min(){return this.stateObj.attributes.min}get _max(){return this.stateObj.attributes.max}get _step(){return this.stateObj.attributes.step}}class pt extends nt{get _value(){return this.stateObj.attributes.options.indexOf(this.stateObj.state)}set _value(t){t in this.stateObj.attributes.options&&this._hass.callService("input_select","select_option",{entity_id:this.stateObj.entity_id,option:this.stateObj.attributes.options[t]})}get string(){return this.stateObj.state}get isOff(){return!1}get hasToggle(){return!1}get hasSlider(){return this.stateObj.attributes.options&&this.stateObj.attributes.options.length>0}get _max(){return this.stateObj.attributes.options.length-1}get _step(){return 1}}class gt extends nt{get _value(){return this.stateObj.state}set _value(t){this._hass.callService("number","set_value",{entity_id:this.stateObj.entity_id,value:t})}get string(){return`${parseFloat(this.stateObj.state)} ${this.stateObj.attributes.unit_of_measurement||""}`.trim()}get isOff(){return!1}get hasToggle(){return!1}get hasSlider(){return!0}get _min(){return this.stateObj.attributes.min}get _max(){return this.stateObj.attributes.max}get _step(){return this.stateObj.attributes.step}}class bt extends nt{get _value(){return this.stateObj.attributes.temperature}set _value(t){this._hass.callService("water_heater","set_temperature",{entity_id:this.stateObj.entity_id,temperature:t})}get string(){return`${this.value} ${this._hass.config.unit_system.temperature}`}get isOff(){return"off"===this.stateObj.state}get _min(){return this.stateObj.attributes.min_temp}get _max(){return this.stateObj.attributes.max_temp}get _step(){return 1}}class vt extends nt{get _value(){return this.stateObj.attributes.humidity}set _value(t){this._hass.callService("humidifier","set_humidity",{entity_id:this.stateObj.entity_id,humidity:t})}get string(){return`${this.value} %`}get isOff(){return"off"===this.stateObj.state}get _min(){return this.stateObj.attributes.min_humidity}get _max(){return this.stateObj.attributes.max_humidity}get _step(){return 1}}var ft="17.3.0";class mt extends tt{setConfig(t){if(this._config=t,!t.entity)throw new Error("No entity specified.");const e=t.entity.split(".")[0],s=function(t){return{light:lt,media_player:ht,climate:ut,water_heater:bt,cover:ct,fan:dt,input_number:_t,input_select:pt,number:gt,humidifier:vt}[t]}(e);if(!s)throw new Error(`Unsupported entity type: ${e}`);this.ctrl=new s(t)}async resized(){await this.updateComplete,this.shadowRoot&&this.parentElement&&(this.hide_state=this._config.full_row?this.parentElement.clientWidth<=180:this.parentElement.clientWidth<=335)}async firstUpdated(){await this.resized()}render(){var t;const e=this.ctrl;if(e.hass=this.hass,!e.stateObj)return H` + + ${this.hass.localize("ui.panel.lovelace.warning.entity_not_found","entity",this._config.entity)} + + `;const s=(null!==(t=e.dir)&&void 0!==t?t:this.hass.translationMetadata.translations[this.hass.language||"en"].isRTL)?"rtl":"ltr",i="unavailable"!==e.stateObj.state&&e.hasSlider&&!(e.isOff&&this._config.hide_when_off),r=this._config.toggle&&e.hasToggle,n=!r&&(!1===this._config.hide_state||!this._config.hide_state&&!this.hide_state&&(!e.isOff||!this._config.hide_when_off)),a=H` +
t.stopPropagation()}> + ${i?H` + ${this._config.colorize&&e.background?H` + + `:""} + e.value=this.shadowRoot.querySelector("ha-slider").value} + class=${this._config.full_row||this._config.grow?"full":""} + ignore-bar-touch + > + `:""} + ${r?e.renderToggle(this.hass):""} + ${n?H` + ${"unavailable"===e.stateObj.state?this.hass.localize("state.default.unavailable"):e.string} + `:""} +
+ `;if(this._config.full_row){if(this._config.hide_when_off&&e.isOff)return H``;if(!0===this._config.show_icon){const t=this._config;return H` +
+ + ${a} +
+ `}return a}return H` + + ${a} + + `}static get styles(){return a` + .wrapper { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 7; + height: 40px; + } + .state { + min-width: 45px; + text-align: end; + } + ha-entity-toggle { + min-width: auto; + margin-left: 8px; + } + ha-slider { + width: 100%; + min-width: 100px; + --paper-slider-secondary-color: transparent; + } + ha-slider:not(.full) { + max-width: 200px; + } + `}}t([it()],mt.prototype,"hass",void 0),t([it()],mt.prototype,"hide_state",void 0),customElements.get("slider-entity-row")||(customElements.define("slider-entity-row",mt),console.info(`%cSLIDER-ENTITY-ROW ${ft} IS INSTALLED`,"color: green; font-weight: bold","")); From cb0cd2430abb212e3da25cdbc9a5a0418872fd5f Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 15:57:35 +0100 Subject: [PATCH 045/158] Fix aurora alert --- automation/aurora_alert.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/automation/aurora_alert.yaml b/automation/aurora_alert.yaml index d123e899..b5226dc8 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: From 947127cdf17dd8bc694c364c772c9dd799b363f7 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:11:27 +0100 Subject: [PATCH 046/158] Simple Thermostat 2.5.0 --- www/simple-thermostat.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/www/simple-thermostat.js b/www/simple-thermostat.js index c0d30aa0..31de78e7 100644 --- a/www/simple-thermostat.js +++ b/www/simple-thermostat.js @@ -1,4 +1,4 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SimpleThermostat=e()}(this,(function(){"use strict";const t="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,e=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},i=`{{lit-${String(Math.random()).slice(2)}}}`,n=`\x3c!--${i}--\x3e`,s=new RegExp(`${i}|${n}`),r="$lit$";class o{constructor(t,e){this.parts=[],this.element=e;const n=[],o=[],c=document.createTreeWalker(e.content,133,null,!1);let h=0,u=-1,p=0;const{strings:f,values:{length:g}}=t;for(;p0;){const e=f[p],i=d.exec(e)[2],n=i.toLowerCase()+r,o=t.getAttribute(n);t.removeAttribute(n);const a=o.split(s);this.parts.push({type:"attribute",index:u,name:i,strings:a}),p+=a.length-1}}"TEMPLATE"===t.tagName&&(o.push(t),c.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(i)>=0){const i=t.parentNode,o=e.split(s),c=o.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},c=t=>-1!==t.index,l=()=>document.createComment(""),d=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function h(t,e){const{element:{content:i},parts:n}=t,s=document.createTreeWalker(i,133,null,!1);let r=p(n),o=n[r],a=-1,c=0;const l=[];let d=null;for(;s.nextNode();){a++;const t=s.currentNode;for(t.previousSibling===d&&(d=null),e.has(t)&&(l.push(t),null===d&&(d=t)),null!==d&&c++;void 0!==o&&o.index===a;)o.index=null!==d?-1:o.index-c,r=p(n,r),o=n[r]}l.forEach((t=>t.parentNode.removeChild(t)))}const u=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,133,null,!1);for(;i.nextNode();)e++;return e},p=(t,e=-1)=>{for(let i=e+1;i"function"==typeof t&&f.has(t),_={},m={};class v{constructor(t,e,i){this.__parts=[],this.template=t,this.processor=e,this.options=i}update(t){let e=0;for(const i of this.__parts)void 0!==i&&i.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const e=t?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],n=this.template.parts,s=document.createTreeWalker(e,133,null,!1);let r,o=0,a=0,l=s.nextNode();for(;o-1||s)&&-1===t.indexOf("--\x3e",a+1);const c=d.exec(t);e+=null===c?t+(s?y:n):t.substr(0,c.index)+c[1]+c[2]+r+c[3]+i}return e+=this.strings[t],e}getTemplateElement(){const t=document.createElement("template");return t.innerHTML=this.getHTML(),t}}const w=t=>null===t||!("object"==typeof t||"function"==typeof t),S=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class x{constructor(t,e,i){this.dirty=!0,this.element=t,this.name=e,this.strings=i,this.parts=[];for(let t=0;t{try{const t={get capture(){return z=!0,!1}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class k{constructor(t,e,i){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=i,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;g(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=_,t(this)}if(this.__pendingValue===_)return;const t=this.__pendingValue,e=this.value,i=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),n=null!=t&&(null==e||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=A(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=_}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const A=t=>t&&(z?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function E(t){let e=T.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},T.set(t.type,e));let n=e.stringsArray.get(t.strings);if(void 0!==n)return n;const s=t.strings.join(i);return n=e.keyString.get(s),void 0===n&&(n=new o(t,t.getTemplateElement()),e.keyString.set(s,n)),e.stringsArray.set(t.strings,n),n}const T=new Map,N=new WeakMap;const V=new class{handleAttributeExpressions(t,e,i,n){const s=e[0];if("."===s){return new C(t,e.slice(1),i).parts}if("@"===s)return[new k(t,e.slice(1),n.eventContext)];if("?"===s)return[new P(t,e.slice(1),i)];return new x(t,e,i).parts}handleTextExpression(t){return new j(t)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.2.1");const U=(t,...e)=>new b(t,e,"html",V),I=(t,e)=>`${t}--${e}`;let M=!0;void 0===window.ShadyCSS?M=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),M=!1);const R=t=>e=>{const n=I(e.type,t);let s=T.get(n);void 0===s&&(s={stringsArray:new WeakMap,keyString:new Map},T.set(n,s));let r=s.stringsArray.get(e.strings);if(void 0!==r)return r;const a=e.strings.join(i);if(r=s.keyString.get(a),void 0===r){const i=e.getTemplateElement();M&&window.ShadyCSS.prepareTemplateDom(i,t),r=new o(e,i),s.keyString.set(a,r)}return s.stringsArray.set(e.strings,r),r},F=["html","svg"],B=new Set,L=(t,e,i)=>{B.add(t);const n=i?i.element:document.createElement("template"),s=e.querySelectorAll("style"),{length:r}=s;if(0===r)return void window.ShadyCSS.prepareTemplateStyles(n,t);const o=document.createElement("style");for(let t=0;t{F.forEach((e=>{const i=T.get(I(e,t));void 0!==i&&i.keyString.forEach((t=>{const{element:{content:e}}=t,i=new Set;Array.from(e.querySelectorAll("style")).forEach((t=>{i.add(t)})),h(t,i)}))}))})(t);const a=n.content;i?function(t,e,i=null){const{element:{content:n},parts:s}=t;if(null==i)return void n.appendChild(e);const r=document.createTreeWalker(n,133,null,!1);let o=p(s),a=0,c=-1;for(;r.nextNode();)for(c++,r.currentNode===i&&(a=u(e),i.parentNode.insertBefore(e,i));-1!==o&&s[o].index===c;){if(a>0){for(;-1!==o;)s[o].index+=a,o=p(s,o);return}o=p(s,o)}}(i,o,a.firstChild):a.insertBefore(o,a.firstChild),window.ShadyCSS.prepareTemplateStyles(n,t);const c=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==c)e.insertBefore(c.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(o,a.firstChild);const t=new Set;t.add(o),h(i,t)}};window.JSCompiler_renameProperty=(t,e)=>t;const q={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},D=(t,e)=>e!==t&&(e==e||t==t),H={attribute:!0,type:String,converter:q,reflect:!1,hasChanged:D},W="finalized";class J extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach(((e,i)=>{const n=this._attributeNameForProperty(i,e);void 0!==n&&(this._attributeToPropertyMap.set(n,i),t.push(n))})),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach(((t,e)=>this._classProperties.set(e,t)))}}static createProperty(t,e=H){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const s=this[t];this[e]=n,this.requestUpdateInternal(t,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||H}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty(W)||t.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const i of e)this.createProperty(i,t[i])}}static _attributeNameForProperty(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,i=D){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,n=e.converter||q,s="function"==typeof n?n:n.fromAttribute;return s?s(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,n=e.converter;return(n&&n.toAttribute||q.toAttribute)(t,i)}initialize(){this._updateState=0,this._updatePromise=new Promise((t=>this._enableUpdatingResolver=t)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((t,e)=>this[e]=t)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,i){e!==i&&this._attributeToProperty(t,i)}_propertyToAttribute(t,e,i=H){const n=this.constructor,s=n._attributeNameForProperty(t,i);if(void 0!==s){const t=n._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=8|this._updateState,null==t?this.removeAttribute(s):this.setAttribute(s,t),this._updateState=-9&this._updateState}}_attributeToProperty(t,e){if(8&this._updateState)return;const i=this.constructor,n=i._attributeToPropertyMap.get(t);if(void 0!==n){const t=i.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=i._propertyValueFromAttribute(e,t),this._updateState=-17&this._updateState}}requestUpdateInternal(t,e,i){let n=!0;if(void 0!==t){const s=this.constructor;i=i||s.getPropertyOptions(t),s._valueHasChanged(this[t],e,i.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==i.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,i))):n=!1}!this._hasRequestedUpdate&&n&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this.requestUpdateInternal(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(t){}const t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{t=this.shouldUpdate(e),t?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((t,e)=>this._propertyToAttribute(e,this[e],t))),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}J.finalized=!0;const G=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,K=Symbol();class Y{constructor(t,e){if(e!==K)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(G?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const Q={};class X extends J{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(Array.isArray(t)){const e=(t,i)=>t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),n=[];i.forEach((t=>n.unshift(t))),this._styles=n}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!G){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new Y(String(e),K)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?G?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==Q&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return Q}}X.finalized=!0,X.render=(t,i,n)=>{if(!n||"object"!=typeof n||!n.scopeName)throw new Error("The `scopeName` option is required.");const s=n.scopeName,r=N.has(i),o=M&&11===i.nodeType&&!!i.host,a=o&&!B.has(s),c=a?document.createDocumentFragment():i;if(((t,i,n)=>{let s=N.get(i);void 0===s&&(e(i,i.firstChild),N.set(i,s=new j(Object.assign({templateFactory:E},n))),s.appendInto(i)),s.setValue(t),s.commit()})(t,c,Object.assign({templateFactory:R(s)},n)),a){const t=N.get(c);N.delete(c);const n=t.value instanceof v?t.value.template:void 0;L(s,c,n),e(i,i.firstChild),i.appendChild(c),N.set(i,t)}!r&&o&&window.ShadyCSS.styleElement(i.host)};const Z=(t,e,i,n)=>{if("length"===i||"prototype"===i)return;if("arguments"===i||"caller"===i)return;const s=Object.getOwnPropertyDescriptor(t,i),r=Object.getOwnPropertyDescriptor(e,i);!tt(s,r)&&n||Object.defineProperty(t,i,r)},tt=function(t,e){return void 0===t||t.configurable||t.writable===e.writable&&t.enumerable===e.enumerable&&t.configurable===e.configurable&&(t.writable||t.value===e.value)},et=(t,e)=>`/* Wrapped ${t}*/\n${e}`,it=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),nt=Object.getOwnPropertyDescriptor(Function.prototype.toString,"name");var st=(t,e,{ignoreNonConfigurable:i=!1}={})=>{const{name:n}=t;for(const n of Reflect.ownKeys(e))Z(t,e,n,i);return((t,e)=>{const i=Object.getPrototypeOf(e);i!==Object.getPrototypeOf(t)&&Object.setPrototypeOf(t,i)})(t,e),((t,e,i)=>{const n=""===i?"":`with ${i.trim()}() `,s=et.bind(null,n,e.toString());Object.defineProperty(s,"name",nt),Object.defineProperty(t,"toString",{...it,value:s})})(t,e,n),t},rt=(t,e={})=>{if("function"!=typeof t)throw new TypeError(`Expected the first argument to be a function, got \`${typeof t}\``);const{wait:i=0,before:n=!1,after:s=!0}=e;if(!n&&!s)throw new Error("Both `before` and `after` are false, function wouldn't be called.");let r,o;const a=function(...e){const a=this,c=n&&!r;return clearTimeout(r),r=setTimeout((()=>{r=void 0,s&&(o=t.apply(a,e))}),i),c&&(o=t.apply(a,e)),o};return st(a,t),a.cancel=()=>{r&&(clearTimeout(r),r=void 0)},a},ot="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var at=function(t){var e={exports:{}};return t(e,e.exports),e.exports}((function(t,e){var i="__lodash_hash_undefined__",n=9007199254740991,s="[object Arguments]",r="[object Array]",o="[object Boolean]",a="[object Date]",c="[object Error]",l="[object Function]",d="[object Map]",h="[object Number]",u="[object Object]",p="[object Promise]",f="[object RegExp]",g="[object Set]",_="[object String]",m="[object Symbol]",v="[object WeakMap]",y="[object ArrayBuffer]",b="[object DataView]",w=/^\[object .+?Constructor\]$/,S=/^(?:0|[1-9]\d*)$/,x={};x["[object Float32Array]"]=x["[object Float64Array]"]=x["[object Int8Array]"]=x["[object Int16Array]"]=x["[object Int32Array]"]=x["[object Uint8Array]"]=x["[object Uint8ClampedArray]"]=x["[object Uint16Array]"]=x["[object Uint32Array]"]=!0,x[s]=x[r]=x[y]=x[o]=x[b]=x[a]=x[c]=x[l]=x[d]=x[h]=x[u]=x[f]=x[g]=x[_]=x[v]=!1;var $="object"==typeof ot&&ot&&ot.Object===Object&&ot,j="object"==typeof self&&self&&self.Object===Object&&self,P=$||j||Function("return this")(),C=e&&!e.nodeType&&e,O=C&&t&&!t.nodeType&&t,z=O&&O.exports===C,k=z&&$.process,A=function(){try{return k&&k.binding&&k.binding("util")}catch(t){}}(),E=A&&A.isTypedArray;function T(t,e){for(var i=-1,n=null==t?0:t.length;++ia))return!1;var l=r.get(t);if(l&&r.get(e))return l==e;var d=-1,h=!0,u=2&i?new bt:void 0;for(r.set(t,e),r.set(e,t);++d-1},vt.prototype.set=function(t,e){var i=this.__data__,n=xt(i,t);return n<0?(++this.size,i.push([t,e])):i[n][1]=e,this},yt.prototype.clear=function(){this.size=0,this.__data__={hash:new mt,map:new(st||vt),string:new mt}},yt.prototype.delete=function(t){var e=At(this,t).delete(t);return this.size-=e?1:0,e},yt.prototype.get=function(t){return At(this,t).get(t)},yt.prototype.has=function(t){return At(this,t).has(t)},yt.prototype.set=function(t,e){var i=At(this,t),n=i.size;return i.set(t,e),this.size+=i.size==n?0:1,this},bt.prototype.add=bt.prototype.push=function(t){return this.__data__.set(t,i),this},bt.prototype.has=function(t){return this.__data__.has(t)},wt.prototype.clear=function(){this.__data__=new vt,this.size=0},wt.prototype.delete=function(t){var e=this.__data__,i=e.delete(t);return this.size=e.size,i},wt.prototype.get=function(t){return this.__data__.get(t)},wt.prototype.has=function(t){return this.__data__.has(t)},wt.prototype.set=function(t,e){var i=this.__data__;if(i instanceof vt){var n=i.__data__;if(!st||n.length<199)return n.push([t,e]),this.size=++i.size,this;i=this.__data__=new yt(n)}return i.set(t,e),this.size=i.size,this};var Tt=tt?function(t){return null==t?[]:(t=Object(t),function(t,e){for(var i=-1,n=null==t?0:t.length,s=0,r=[];++i-1&&t%1==0&&t-1&&t%1==0&&t<=n}function qt(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function Dt(t){return null!=t&&"object"==typeof t}var Ht=E?function(t){return function(e){return t(e)}}(E):function(t){return Dt(t)&&Lt(t.length)&&!!x[$t(t)]};function Wt(t){return null!=(e=t)&&Lt(e.length)&&!Bt(e)?St(t):Ot(t);var e}t.exports=function(t,e){return Pt(t,e)}}));var ct=((t,...e)=>{const i=e.reduce(((e,i,n)=>e+(t=>{if(t instanceof Y)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[n+1]),t[0]);return new Y(i,K)})`:host { +!function(){const e={DEBUG:!1};try{if(process)return process.env=Object.assign({},process.env),void Object.assign(process.env,e)}catch(e){}globalThis.process={env:e}}();var e="simple-thermostat";const t="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,i=(e,t,i=null)=>{for(;t!==i;){const i=t.nextSibling;e.removeChild(t),t=i}},n=`{{lit-${String(Math.random()).slice(2)}}}`,s=`\x3c!--${n}--\x3e`,o=new RegExp(`${n}|${s}`);class a{constructor(e,t){this.parts=[],this.element=t;const i=[],s=[],a=document.createTreeWalker(t.content,133,null,!1);let l=0,u=-1,h=0;const{strings:p,values:{length:f}}=e;for(;h0;){const t=p[h],i=d.exec(t)[2],n=i.toLowerCase()+"$lit$",s=e.getAttribute(n);e.removeAttribute(n);const a=s.split(o);this.parts.push({type:"attribute",index:u,name:i,strings:a}),h+=a.length-1}}"TEMPLATE"===e.tagName&&(s.push(e),a.currentNode=e.content)}else if(3===e.nodeType){const t=e.data;if(t.indexOf(n)>=0){const n=e.parentNode,s=t.split(o),a=s.length-1;for(let t=0;t{const i=e.length-t.length;return i>=0&&e.slice(i)===t},l=e=>-1!==e.index,c=()=>document.createComment(""),d=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;function u(e,t){const{element:{content:i},parts:n}=e,s=document.createTreeWalker(i,133,null,!1);let o=p(n),a=n[o],r=-1,l=0;const c=[];let d=null;for(;s.nextNode();){r++;const e=s.currentNode;for(e.previousSibling===d&&(d=null),t.has(e)&&(c.push(e),null===d&&(d=e)),null!==d&&l++;void 0!==a&&a.index===r;)a.index=null!==d?-1:a.index-l,o=p(n,o),a=n[o]}c.forEach((e=>e.parentNode.removeChild(e)))}const h=e=>{let t=11===e.nodeType?0:1;const i=document.createTreeWalker(e,133,null,!1);for(;i.nextNode();)t++;return t},p=(e,t=-1)=>{for(let i=t+1;i"function"==typeof e&&f.has(e),v={},m={};class y{constructor(e,t,i){this.__parts=[],this.template=e,this.processor=t,this.options=i}update(e){let t=0;for(const i of this.__parts)void 0!==i&&i.setValue(e[t]),t++;for(const e of this.__parts)void 0!==e&&e.commit()}_clone(){const e=t?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),i=[],n=this.template.parts,s=document.createTreeWalker(e,133,null,!1);let o,a=0,r=0,c=s.nextNode();for(;ae}),_=` ${n} `;class w{constructor(e,t,i,n){this.strings=e,this.values=t,this.type=i,this.processor=n}getHTML(){const e=this.strings.length-1;let t="",i=!1;for(let o=0;o-1||i)&&-1===e.indexOf("--\x3e",a+1);const r=d.exec(e);t+=null===r?e+(i?_:s):e.substr(0,r.index)+r[1]+r[2]+"$lit$"+r[3]+n}return t+=this.strings[e],t}getTemplateElement(){const e=document.createElement("template");let t=this.getHTML();return void 0!==b&&(t=b.createHTML(t)),e.innerHTML=t,e}}const S=e=>null===e||!("object"==typeof e||"function"==typeof e),x=e=>Array.isArray(e)||!(!e||!e[Symbol.iterator]);class ${constructor(e,t,i){this.dirty=!0,this.element=e,this.name=t,this.strings=i,this.parts=[];for(let e=0;e{try{const e={get capture(){return j=!0,!1}};window.addEventListener("test",e,e),window.removeEventListener("test",e,e)}catch(e){}})();class N{constructor(e,t,i){this.value=void 0,this.__pendingValue=void 0,this.element=e,this.eventName=t,this.eventContext=i,this.__boundHandleEvent=e=>this.handleEvent(e)}setValue(e){this.__pendingValue=e}commit(){for(;g(this.__pendingValue);){const e=this.__pendingValue;this.__pendingValue=v,e(this)}if(this.__pendingValue===v)return;const e=this.__pendingValue,t=this.value,i=null==e||null!=t&&(e.capture!==t.capture||e.once!==t.once||e.passive!==t.passive),n=null!=e&&(null==t||i);i&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=T(e),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=e,this.__pendingValue=v}handleEvent(e){"function"==typeof this.value?this.value.call(this.eventContext||this.element,e):this.value.handleEvent(e)}}const T=e=>e&&(j?{capture:e.capture,passive:e.passive,once:e.once}:e.capture);function z(e){let t=A.get(e.type);void 0===t&&(t={stringsArray:new WeakMap,keyString:new Map},A.set(e.type,t));let i=t.stringsArray.get(e.strings);if(void 0!==i)return i;const s=e.strings.join(n);return i=t.keyString.get(s),void 0===i&&(i=new a(e,e.getTemplateElement()),t.keyString.set(s,i)),t.stringsArray.set(e.strings,i),i}const A=new Map,V=new WeakMap;const R=new class{handleAttributeExpressions(e,t,i,n){const s=t[0];if("."===s){return new C(e,t.slice(1),i).parts}if("@"===s)return[new N(e,t.slice(1),n.eventContext)];if("?"===s)return[new k(e,t.slice(1),i)];return new $(e,t,i).parts}handleTextExpression(e){return new P(e)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");const I=(e,...t)=>new w(e,t,"html",R),F=(e,t)=>`${e}--${t}`;let U=!0;void 0===window.ShadyCSS?U=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),U=!1);const M=e=>t=>{const i=F(t.type,e);let s=A.get(i);void 0===s&&(s={stringsArray:new WeakMap,keyString:new Map},A.set(i,s));let o=s.stringsArray.get(t.strings);if(void 0!==o)return o;const r=t.strings.join(n);if(o=s.keyString.get(r),void 0===o){const i=t.getTemplateElement();U&&window.ShadyCSS.prepareTemplateDom(i,e),o=new a(t,i),s.keyString.set(r,o)}return s.stringsArray.set(t.strings,o),o},H=["html","svg"],L=new Set,q=(e,t,i)=>{L.add(e);const n=i?i.element:document.createElement("template"),s=t.querySelectorAll("style"),{length:o}=s;if(0===o)return void window.ShadyCSS.prepareTemplateStyles(n,e);const a=document.createElement("style");for(let e=0;e{H.forEach((t=>{const i=A.get(F(t,e));void 0!==i&&i.keyString.forEach((e=>{const{element:{content:t}}=e,i=new Set;Array.from(t.querySelectorAll("style")).forEach((e=>{i.add(e)})),u(e,i)}))}))})(e);const r=n.content;i?function(e,t,i=null){const{element:{content:n},parts:s}=e;if(null==i)return void n.appendChild(t);const o=document.createTreeWalker(n,133,null,!1);let a=p(s),r=0,l=-1;for(;o.nextNode();)for(l++,o.currentNode===i&&(r=h(t),i.parentNode.insertBefore(t,i));-1!==a&&s[a].index===l;){if(r>0){for(;-1!==a;)s[a].index+=r,a=p(s,a);return}a=p(s,a)}}(i,a,r.firstChild):r.insertBefore(a,r.firstChild),window.ShadyCSS.prepareTemplateStyles(n,e);const l=r.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)t.insertBefore(l.cloneNode(!0),t.firstChild);else if(i){r.insertBefore(a,r.firstChild);const e=new Set;e.add(a),u(i,e)}};window.JSCompiler_renameProperty=(e,t)=>e;const W={toAttribute(e,t){switch(t){case Boolean:return e?"":null;case Object:case Array:return null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){switch(t){case Boolean:return null!==e;case Number:return null===e?null:Number(e);case Object:case Array:return JSON.parse(e)}return e}},D=(e,t)=>t!==e&&(t==t||e==e),B={attribute:!0,type:String,converter:W,reflect:!1,hasChanged:D};class J extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const e=[];return this._classProperties.forEach(((t,i)=>{const n=this._attributeNameForProperty(i,t);void 0!==n&&(this._attributeToPropertyMap.set(n,i),e.push(n))})),e}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const e=Object.getPrototypeOf(this)._classProperties;void 0!==e&&e.forEach(((e,t)=>this._classProperties.set(t,e)))}}static createProperty(e,t=B){if(this._ensureClassProperties(),this._classProperties.set(e,t),t.noAccessor||this.prototype.hasOwnProperty(e))return;const i="symbol"==typeof e?Symbol():`__${e}`,n=this.getPropertyDescriptor(e,i,t);void 0!==n&&Object.defineProperty(this.prototype,e,n)}static getPropertyDescriptor(e,t,i){return{get(){return this[t]},set(n){const s=this[e];this[t]=n,this.requestUpdateInternal(e,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this._classProperties&&this._classProperties.get(e)||B}static finalize(){const e=Object.getPrototypeOf(this);if(e.hasOwnProperty("finalized")||e.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const e=this.properties,t=[...Object.getOwnPropertyNames(e),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e):[]];for(const i of t)this.createProperty(i,e[i])}}static _attributeNameForProperty(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}static _valueHasChanged(e,t,i=D){return i(e,t)}static _propertyValueFromAttribute(e,t){const i=t.type,n=t.converter||W,s="function"==typeof n?n:n.fromAttribute;return s?s(e,i):e}static _propertyValueToAttribute(e,t){if(void 0===t.reflect)return;const i=t.type,n=t.converter;return(n&&n.toAttribute||W.toAttribute)(e,i)}initialize(){this._updateState=0,this._updatePromise=new Promise((e=>this._enableUpdatingResolver=e)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((e,t)=>{if(this.hasOwnProperty(t)){const e=this[t];delete this[t],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(t,e)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((e,t)=>this[t]=e)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(e,t,i){t!==i&&this._attributeToProperty(e,i)}_propertyToAttribute(e,t,i=B){const n=this.constructor,s=n._attributeNameForProperty(e,i);if(void 0!==s){const e=n._propertyValueToAttribute(t,i);if(void 0===e)return;this._updateState=8|this._updateState,null==e?this.removeAttribute(s):this.setAttribute(s,e),this._updateState=-9&this._updateState}}_attributeToProperty(e,t){if(8&this._updateState)return;const i=this.constructor,n=i._attributeToPropertyMap.get(e);if(void 0!==n){const e=i.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=i._propertyValueFromAttribute(t,e),this._updateState=-17&this._updateState}}requestUpdateInternal(e,t,i){let n=!0;if(void 0!==e){const s=this.constructor;i=i||s.getPropertyOptions(e),s._valueHasChanged(this[e],t,i.hasChanged)?(this._changedProperties.has(e)||this._changedProperties.set(e,t),!0!==i.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(e,i))):n=!1}!this._hasRequestedUpdate&&n&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(e,t){return this.requestUpdateInternal(e,t),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(e){}const e=this.performUpdate();return null!=e&&await e,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let e=!1;const t=this._changedProperties;try{e=this.shouldUpdate(t),e?this.update(t):this._markUpdated()}catch(t){throw e=!1,this._markUpdated(),t}e&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(t)),this.updated(t))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(e){return!0}update(e){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((e,t)=>this._propertyToAttribute(t,this[t],e))),this._reflectingProperties=void 0),this._markUpdated()}updated(e){}firstUpdated(e){}}J.finalized=!0;const G=(e,t)=>"method"===t.kind&&t.descriptor&&!("value"in t.descriptor)?Object.assign(Object.assign({},t),{finisher(i){i.createProperty(t.key,e)}}):{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof t.initializer&&(this[t.key]=t.initializer.call(this))},finisher(i){i.createProperty(t.key,e)}};function Y(e){return(t,i)=>void 0!==i?((e,t,i)=>{t.constructor.createProperty(i,e)})(e,t,i):G(e,t)}const K=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Q=Symbol();class X{constructor(e,t){if(t!==Q)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e}get styleSheet(){return void 0===this._styleSheet&&(K?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const Z={};class ee extends J{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const e=this.getStyles();if(Array.isArray(e)){const t=(e,i)=>e.reduceRight(((e,i)=>Array.isArray(i)?t(i,e):(e.add(i),e)),i),i=t(e,new Set),n=[];i.forEach((e=>n.unshift(e))),this._styles=n}else this._styles=void 0===e?[]:[e];this._styles=this._styles.map((e=>{if(e instanceof CSSStyleSheet&&!K){const t=Array.prototype.slice.call(e.cssRules).reduce(((e,t)=>e+t.cssText),"");return new X(String(t),Q)}return e}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const e=this.constructor._styles;0!==e.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?K?this.renderRoot.adoptedStyleSheets=e.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(e.map((e=>e.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(e){const t=this.render();super.update(e),t!==Z&&this.constructor.render(t,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((e=>{const t=document.createElement("style");t.textContent=e.cssText,this.renderRoot.appendChild(t)})))}render(){return Z}}ee.finalized=!0,ee.render=(e,t,n)=>{if(!n||"object"!=typeof n||!n.scopeName)throw new Error("The `scopeName` option is required.");const s=n.scopeName,o=V.has(t),a=U&&11===t.nodeType&&!!t.host,r=a&&!L.has(s),l=r?document.createDocumentFragment():t;if(((e,t,n)=>{let s=V.get(t);void 0===s&&(i(t,t.firstChild),V.set(t,s=new P(Object.assign({templateFactory:z},n))),s.appendInto(t)),s.setValue(e),s.commit()})(e,l,Object.assign({templateFactory:M(s)},n)),r){const e=V.get(l);V.delete(l);const n=e.value instanceof y?e.value.template:void 0;q(s,l,n),i(t,t.firstChild),t.appendChild(l),V.set(t,e)}!o&&a&&window.ShadyCSS.styleElement(t.host)};var te=((e,...t)=>{const i=t.reduce(((t,i,n)=>t+(e=>{if(e instanceof X)return e.cssText;if("number"==typeof e)return e;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${e}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+e[n+1]),e[0]);return new X(i,Q)})`:host { --st-default-spacing: 4px; } ha-card { @@ -29,7 +29,8 @@ ha-card.no-header { .body { display: grid; grid-auto-flow: column; - grid-auto-columns: auto; + grid-auto-columns: minmax(-webkit-min-content, auto); + grid-auto-columns: minmax(min-content, auto); align-items: center; justify-items: center; place-items: center; @@ -73,7 +74,6 @@ ha-card.no-header { .sensors { display: grid; - grid: auto-flow / auto auto; grid-gap: var(--st-default-spacing); grid-gap: var(--st-spacing, var(--st-default-spacing)); font-size: 16px; @@ -82,6 +82,26 @@ ha-card.no-header { var(--paper-font-subhead_-_font-size, 16px) ); } +.sensors.as-list { + grid-auto-flow: column; + grid-template-columns: -webkit-min-content; + grid-template-columns: min-content; +} + +.sensors.as-table.without-labels { + grid: auto-flow / 100%; + align-items: start; + justify-items: start; + place-items: start; + } + +.sensors.as-table.with-labels { + grid: auto-flow / auto auto; + align-items: start; + justify-items: start; + place-items: start; + } + .sensor-value { display: flex; align-items: center; @@ -227,24 +247,31 @@ header { } .mode-item.active.off { background: var(--off-color); + background: var(--st-mode-active-background, var(--off-color)); } .mode-item.active.heat { background: var(--heat-color); + background: var(--st-mode-active-background, var(--heat-color)); } .mode-item.active.cool { background: var(--cool-color); + background: var(--st-mode-active-background, var(--cool-color)); } .mode-item.active.heat_cool { background: var(--heat_cool-color); + background: var(--st-mode-active-background, var(--heat_cool-color)); } .mode-item.active.auto { background: var(--auto-color); + background: var(--st-mode-active-background, var(--auto-color)); } .mode-item.active.dry { background: var(--dry-color); + background: var(--st-mode-active-background, var(--dry-color)); } .mode-item.active.fan_only { background: var(--fan_only-color); + background: var(--st-mode-active-background, var(--fan_only-color)); } .mode-icon { --iron-icon-width: 24px; @@ -262,4 +289,4 @@ ha-switch { flex: 1; padding-right: 4px; } -`;function lt(t,{decimals:e=1,fallback:i="N/A"}={}){return null===t||""===t||["boolean","undefined"].includes(typeof t)?i:Number(t).toFixed(e)}!function(t,e){void 0===e&&(e={});var i=e.insertAt;if(t&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===i&&n.firstChild?n.insertBefore(s,n.firstChild):n.appendChild(s),s.styleSheet?s.styleSheet.cssText=t:s.appendChild(document.createTextNode(t))}}(ct);const dt=["0","1"],ht=["0.5","1"],ut=["column","row"],pt=["climate"];customElements.define("simple-thermostat-editor",class extends X{static get styles(){return ct}static get properties(){return{hass:{},config:{}}}setConfig(t){this.config=t}_openLink(){window.open("https://github.com/nervetattoo/simple-thermostat/blob/master/README.md")}get _show_header(){return!1!==this.config.show_header}render(){return this.hass?U`
${Object.values(dt).map((t=>U`${t}`))}
${Object.values(ut).map((t=>U`${t}`))}${Object.values(ht).map((t=>U`${t}`))}
Configuration OptionsSettings for label, control, sensors, faults and hiding UI elements can only be configured in the code editor
`:U``}valueChanged(t){if(!this.config||!this.hass)return;const{target:e}=t;this["_"+e.configValue]!==e.value&&(e.configValue&&(""===e.value?delete this.config[e.configValue]:this.config={...this.config,[e.configValue]:void 0!==e.checked?e.checked:e.value}),((t,e,i={},n={})=>{const s=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});s.detail=i,t.dispatchEvent(s)})(this,"config-changed",{config:this.config}))}}),function(t){console.info("%csimple-thermostat: "+t,"font-weight: bold")}("0.41.1");const ft=["entity","toggle_entity","sensors","faults","_values","_updatingValues","modes"],gt=["hvac","fan","preset","swing"],_t=["off","heat","cool","heat_cool","auto","dry","fan_only"],mt=["hvac","preset"],vt="hass:chevron-up",yt="hass:chevron-down",bt="mdi:plus",wt="mdi:minus",St={auto:"hass:autorenew",cool:"hass:snowflake",dry:"hass:water-percent",fan_only:"hass:fan",heat_cool:"hass:autorenew",heat:"hass:fire",off:"hass:power"},xt={auto:"mdi:radiator",cooling:"mdi:snowflake",fan:"mdi:fan",heating:"mdi:radiator",idle:"mdi:radiator-disabled",off:"mdi:radiator-off"},$t={temperature:!1,setpoint:!1,state:!1};function jt(t,e,i={}){return e[t+"_modes"].filter((t=>function(t,e){if(void 0===e)return!0;if(Array.isArray(e))return e.includes(t);const i=typeof e[t];return"boolean"===i?e[t]:"object"!==i||!1!==e[t].include}(t,i))).map((t=>{const{include:e,...n}="object"==typeof i[t]?i[t]:{};return{icon:St[t],value:t,name:t,...n}}))}class Pt extends X{static get styles(){return ct}static get properties(){return{i:Number,_hass:Object,config:Object,entity:Object,toggle_entity:Object,sensors:Array,faults:Array,modes:Object,icon:String,_values:Object,_updatingValues:Boolean,_mode:String,_hide:Object,name:String}}constructor(){super(),this._debouncedSetTemperature=rt((t=>{this._hass.callService("climate","set_temperature",{entity_id:this.config.entity,...t})}),{wait:1e3}),this._hass=null,this.entity=null,this.toggle_entity=null,this.toggle_entity_label=null,this.icon=null,this.sensors=[],this.faults=[],this._stepSize=.5,this._values={},this._updatingValues=!1,this._hide=$t,this.modeOptions={names:!0,icons:!0,headings:!0}}static getConfigElement(){return window.document.createElement("simple-thermostat-editor")}setConfig(t){if(!t.entity)throw new Error("You need to define an entity");if(!1===t.show_header&&t.faults)throw new Error("Faults are not supported when header is hidden");this.config={decimals:1,...t}}set hass(t){const e=t.states[this.config.entity];if(void 0===e)return;if(this._hass=t,this.entity!==e&&(this.entity=e),"string"==typeof this.config.toggle_entity){const e=t.states[this.config.toggle_entity];this.toggle_entity!==e&&(this.toggle_entity=e)}else if("object"==typeof this.config.toggle_entity){const e=t.states[this.config.toggle_entity.entity_id];this.toggle_entity!==e&&(this.toggle_entity=e),"string"==typeof this.config.toggle_entity.name?this.toggle_entity_label=this.config.toggle_entity.name:!0===this.config.toggle_entity.name?this.toggle_entity_label=this.toggle_entity.attributes.name:this.toggle_entity_label=void 0}const i=e.attributes;let n;this.entityType=function(t){return"number"==typeof t.target_temp_high&&"number"==typeof t.target_temp_low?"dual":"single"}(i),n="dual"===this.entityType?{target_temp_low:i.target_temp_low,target_temp_high:i.target_temp_high}:{temperature:i.temperature},this._updatingValues&&at(n,this._values)?this._updatingValues=!1:this._updatingValues||(this._values=n);const s=t=>gt.includes(t)&&i[t+"_modes"],r=t=>t.filter(s).map((t=>({type:t,list:jt(t,i,{})})));let o=[];if(!1===this.config.control)o=[];else if(Array.isArray(this.config.control))o=r(this.config.control);else if("object"==typeof this.config.control){const{_names:t,_icons:e,_headings:n,...a}=this.config.control;"boolean"==typeof t&&(this.modeOptions.names=t),"boolean"==typeof e&&(this.modeOptions.icons=e),"boolean"==typeof n&&(this.modeOptions.headings=n);const c=Object.entries(a);o=c.length>0?c.filter((([t])=>s(t))).map((([t,{_name:e,_hide_when_off:n,...s}])=>({type:t,hide_when_off:n,name:e,list:jt(t,i,s)}))):r(mt)}else o=r(mt);this.modes=o.map((t=>{if("hvac"===t.type){const i=[];return t.list.forEach((t=>{const e=_t.indexOf(t.value);i[e]=t})),{...t,list:i,mode:e.state}}const n=i[t.type+"_mode"];return{...t,mode:n}})),void 0!==this.config.icon?this.icon=this.config.icon:this.entity.attributes.hvac_action?this.icon=xt:this.icon=St,this.config.step_size&&(this._stepSize=+this.config.step_size),this.config.hide&&(this._hide={...this._hide,...this.config.hide}),!1===this.config.show_header?this.show_header=!1:this.show_header=!0,"string"==typeof this.config.name?this.name=this.config.name:!1===this.config.name?this.name=!1:this.name=e.attributes.friendly_name,!1===this.config.sensors?this.sensors=!1:this.config.sensors&&(this.sensors=this.config.sensors.map((({name:e,entity:i,attribute:n,unit:s="",...r})=>{let o;const a=[e];return i?(o=t.states[i],a.push(o&&o.attributes&&o.attributes.friendly_name),n&&(o=o.attributes[n]+s)):n&&n in this.entity.attributes&&(o=this.entity.attributes[n]+s,a.push(n)),a.push(i),{...r,name:a.find((t=>!!t)),state:o,entity:i,unit:s}}))),!1===this.config.faults?this.faults=!1:this.config.faults&&(this.faults=this.config.faults.map((({entity:e,...i})=>({...i,state:t.states[e],entity:e}))))}shouldUpdate(t){return ft.some((e=>t.has(e)))}localize(t,e=""){const i=this._hass.selectedLanguage||this._hass.language,n=`${e}${t}`,s=this._hass.resources[i];return n in s?s[n]:t}render({_hass:t,_hide:e,_values:i,_updatingValues:n,config:s,entity:r,sensors:o,faults:a}=this){if(!r)return U`Entity not available: ${s.entity}`;const{state:c,attributes:{min_temp:l=null,max_temp:d=null,hvac_action:h}}=r,u=this.getUnit(),p=this.config.step_layout||"column",f="row"===p,g=[!this.show_header&&"no-header",h].filter((t=>!!t));return U`${this.renderHeader()}
${!1!==this.sensors?this.renderSensors():""} ${e.setpoint?"":Object.entries(i).map((([t,e])=>U`

${lt(e,s)} ${!1!==u?U`${u}`:""}

`))}
${this.modes.map((t=>this.renderModeType(r.state,t)))}
`}renderHeader(){if(!1===this.show_header||!1===this.name)return"";let t=this.icon;const e=this.entity.attributes.hvac_action||this.entity.state;return"object"==typeof this.icon&&(t=e in this.icon&&this.icon[e]),U`
${t&&U``||""}

${this.name}

${!1!==this.faults?this.renderFaults():""} ${this.toggle_entity?this.renderToggle():""}
`}toggleEntityChanged(t){const e=t.target.checked;this._hass.callService("homeassistant",e?"turn_on":"turn_off",{entity_id:this.toggle_entity.entity_id})}renderSensors({_hide:t,entity:e,sensors:i}=this){const{state:n,attributes:{hvac_action:s,current_temperature:r}}=e,o=this.getUnit(),a=[this.renderInfoItem(t.temperature,`${lt(r,this.config)}${o}`,{heading:this.config.label&&this.config.label.temperature||"Temperature"}),this.renderInfoItem(t.state,this.localize(s,"state_attributes.climate.hvac_action."),{heading:this.config.label&&this.config.label.state||"State"}),i.map((({name:t,icon:e,state:i,unit:n})=>i&&this.renderInfoItem(!1,i,{heading:t,icon:e,unit:n})))||null].filter((t=>null!==t));return U`
${a}
`}renderToggle({_hide:t,entity:e,faults:i}=this){return U`
${this.toggle_entity_label}
`}renderFaults({_hide:t,entity:e,faults:i}=this){const n=i.map((({icon:t,hide_inactive:e,state:i})=>U``));return U`
${n}
`}renderModeType(t,{type:e,hide_when_off:i,mode:n="none",list:s,name:r}){if(0===s.length||i&&t===_t[0])return null;let o=`state_attributes.climate.${e}_mode.`;"hvac"===e&&(o="state.climate.");const a=t=>!1===t||!1===this.modeOptions.names?null:this.localize(t,o),c=t=>t?!1===this.modeOptions.icons?null:U``:null,l="hvac"==e?"operation":e+"_mode",d=r||this.localize("ui.card.climate."+l),{headings:h}=this.modeOptions;return U`
${h?U`
${d}
`:null} ${s.map((({value:t,icon:i,name:s})=>U`
${c(i)} ${a(s)}
`))}
`}renderInfoItem(t,e,{heading:i,icon:n,unit:s}){if(t||!e)return;let r;if("object"==typeof e){let t=e.state;if("device_class"in e.attributes){const[i]=e.entity_id.split("."),n=["state",i,e.attributes.device_class,""].join(".");t=this.localize(e.state,n)}r=U`
${t} ${s||e.attributes.unit_of_measurement}
`}else r=U`
${e}
`;return i=n?U``:U`${i}:`,U`
${i}
${r}`}setTemperature(t,e="temperature"){this._updatingValues=!0,this._values={...this._values,[e]:+lt(this._values[e]+t,this.config.decimals)},this._debouncedSetTemperature({...this._values})}setMode(t,e){t&&e?(this._hass.callService("climate",`set_${t}_mode`,{entity_id:this.config.entity,[t+"_mode"]:e}),this.fire("haptic","light")):this.fire("haptic","failure")}openEntityPopover(t=this.config.entity){this.fire("hass-more-info",{entityId:t})}fire(t,e,i){i=i||{},e=null==e?{}:e;const n=new Event(t,{bubbles:void 0===i.bubbles||i.bubbles,cancelable:Boolean(i.cancelable),composed:void 0===i.composed||i.composed});return n.detail=e,this.dispatchEvent(n),n}getCardSize(){return 3}getUnit(){return["boolean","string"].includes(typeof this.config.unit)?this.config.unit:this._hass.config.unit_system.temperature}}return window.customElements.define("simple-thermostat",Pt),window.customCards=window.customCards||[],window.customCards.push({type:"simple-thermostat",name:"Simple Thermostat",preview:!1,description:"A different take on the thermostat card"}),Pt})); +`;function ie(e,t,i,n={}){n=n||{},i=null==i?{}:i;const s=new Event(t,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return s.detail=i,e.dispatchEvent(s),s}!function(e,t){void 0===t&&(t={});var i=t.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===i&&n.firstChild?n.insertBefore(s,n.firstChild):n.appendChild(s),s.styleSheet?s.styleSheet.cssText=e:s.appendChild(document.createTextNode(e))}}(te);const ne=[0,1],se=[.5,1],oe=["column","row"],ae=["climate"],re={header:{},layout:{mode:{}}};function le(e,t){var i={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(i[n]=e[n]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var s=0;for(n=Object.getOwnPropertySymbols(e);s=0;r--)(s=e[r])&&(a=(o<3?s(a):o>3?s(t,i,a):s(t,i))||a);return o>3&&a&&Object.defineProperty(t,i,a),a}const de=(e,t,i,n)=>{if("length"===i||"prototype"===i)return;if("arguments"===i||"caller"===i)return;const s=Object.getOwnPropertyDescriptor(e,i),o=Object.getOwnPropertyDescriptor(t,i);!ue(s,o)&&n||Object.defineProperty(e,i,o)},ue=function(e,t){return void 0===e||e.configurable||e.writable===t.writable&&e.enumerable===t.enumerable&&e.configurable===t.configurable&&(e.writable||e.value===t.value)},he=(e,t)=>`/* Wrapped ${e}*/\n${t}`,pe=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),fe=Object.getOwnPropertyDescriptor(Function.prototype.toString,"name");var ge=(e,t,{ignoreNonConfigurable:i=!1}={})=>{const{name:n}=e;for(const n of Reflect.ownKeys(t))de(e,t,n,i);return((e,t)=>{const i=Object.getPrototypeOf(t);i!==Object.getPrototypeOf(e)&&Object.setPrototypeOf(e,i)})(e,t),((e,t,i)=>{const n=""===i?"":`with ${i.trim()}() `,s=he.bind(null,n,t.toString());Object.defineProperty(s,"name",fe),Object.defineProperty(e,"toString",{...pe,value:s})})(e,t,n),e};const ve=(e,t={})=>{if("function"!=typeof e)throw new TypeError(`Expected the first argument to be a function, got \`${typeof e}\``);const{wait:i=0,maxWait:n=Number.Infinity,before:s=!1,after:o=!0}=t;if(!s&&!o)throw new Error("Both `before` and `after` are false, function wouldn't be called.");let a,r,l;const c=function(...t){const c=this,d=()=>{r=void 0,a&&(clearTimeout(a),a=void 0),o&&(l=e.apply(c,t))},u=s&&!a;return clearTimeout(a),a=setTimeout((()=>{a=void 0,r&&(clearTimeout(r),r=void 0),o&&(l=e.apply(c,t))}),i),n>0&&n!==Number.Infinity&&!r&&(r=setTimeout(d,n)),u&&(l=e.apply(c,t)),l};return ge(c,e),c.cancel=()=>{a&&(clearTimeout(a),a=void 0),r&&(clearTimeout(r),r=void 0)},c};function me(e,{decimals:t=1,fallback:i="N/A"}={}){return null===e||""===e||["boolean","undefined"].includes(typeof e)?i:Number(e).toFixed(t)}function ye({header:e,toggleEntityChanged:t,entity:i,openEntityPopover:n}){var s,o;if(!1===e)return m;const a=i.attributes.hvac_action||i.state;let r=e.icon;"object"==typeof e.icon&&(r=null!==(s=null==r?void 0:r[a])&&void 0!==s&&s);const l=null!==(o=null==e?void 0:e.name)&&void 0!==o&&o;return I`
${function(e){return e?I``:m}(r)} ${function(e){return e?I`

${e}

`:m}(l)}
${function(e,t){if(0===e.length)return m;const i=e.map((({icon:e,hide_inactive:i,state:n})=>I``));return I`
${i}
`}(e.faults,n)} ${function(e,t,i){var n;return e?I`
${e.label}
`:m}(e.toggle,n,t)}
`}"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var be,_e=(function(e,t){!function(e){function t(e){var i,n,s=new Error(e);return i=s,n=t.prototype,Object.setPrototypeOf?Object.setPrototypeOf(i,n):i.__proto__=n,s}function i(e,i,n){var s=i.slice(0,n).split(/\n/),o=s.length,a=s[o-1].length+1;throw t(e+=" at line "+o+" col "+a+":\n\n "+i.split(/\n/)[o-1]+"\n "+Array(a).join(" ")+"^")}t.prototype=Object.create(Error.prototype,{name:{value:"Squirrelly Error",enumerable:!1}});var n=new Function("return this")().Promise,s=!1;try{s=new Function("return (async function(){}).constructor")()}catch(e){if(!(e instanceof SyntaxError))throw e}function o(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function a(e,t,i){for(var n in t)o(t,n)&&(null==t[n]||"object"!=typeof t[n]||"storage"!==n&&"prefixes"!==n||i?e[n]=t[n]:e[n]=a({},t[n]));return e}var r=/^async +/,l=/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g,c=/'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g,d=/"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g,u=/[.*+\-?^${}()|[\]\\]/g;function h(e){return u.test(e)?e.replace(u,"\\$&"):e}function p(e,n){n.rmWhitespace&&(e=e.replace(/[\r\n]+/g,"\n").replace(/^\s+|\s+$/gm,"")),l.lastIndex=0,c.lastIndex=0,d.lastIndex=0;var s=n.prefixes,o=[s.h,s.b,s.i,s.r,s.c,s.e].reduce((function(e,t){return e&&t?e+"|"+h(t):t?h(t):e}),""),a=new RegExp("([|()]|=>)|('|\"|`|\\/\\*)|\\s*((\\/)?(-|_)?"+h(n.tags[1])+")","g"),u=new RegExp("([^]*?)"+h(n.tags[0])+"(-|_)?\\s*("+o+")?\\s*","g"),p=0,f=!1;function g(t,s){var o,h={f:[]},g=0,v="c";function m(t){var s=e.slice(p,t),o=s.trim();if("f"===v)"safe"===o?h.raw=!0:n.async&&r.test(o)?(o=o.replace(r,""),h.f.push([o,"",!0])):h.f.push([o,""]);else if("fp"===v)h.f[h.f.length-1][1]+=o;else if("err"===v){if(o){var a=s.search(/\S/);i("invalid syntax",e,p+a)}}else h[v]=o;p=t+1}for("h"===s||"b"===s||"c"===s?v="n":"r"===s&&(h.raw=!0,s="i"),a.lastIndex=p;null!==(o=a.exec(e));){var y=o[1],b=o[2],_=o[3],w=o[4],S=o[5],x=o.index;if(y)"("===y?(0===g&&("n"===v?(m(x),v="p"):"f"===v&&(m(x),v="fp")),g++):")"===y?0==--g&&"c"!==v&&(m(x),v="err"):0===g&&"|"===y?(m(x),v="f"):"=>"===y&&(m(x),p+=1,v="res");else if(b)if("/*"===b){var $=e.indexOf("*/",a.lastIndex);-1===$&&i("unclosed comment",e,o.index),a.lastIndex=$+2}else"'"===b?(c.lastIndex=o.index,c.exec(e)?a.lastIndex=c.lastIndex:i("unclosed string",e,o.index)):'"'===b?(d.lastIndex=o.index,d.exec(e)?a.lastIndex=d.lastIndex:i("unclosed string",e,o.index)):"`"===b&&(l.lastIndex=o.index,l.exec(e)?a.lastIndex=l.lastIndex:i("unclosed string",e,o.index));else if(_)return m(x),p=x+o[0].length,u.lastIndex=p,f=S,w&&"h"===s&&(s="s"),h.t=s,h}return i("unclosed tag",e,t),h}var v=function o(a,l){a.b=[],a.d=[];var c,d=!1,h=[];function v(e,t){e&&(e=function(e,t,i,n){var s,o;return"string"==typeof t.autoTrim?s=o=t.autoTrim:Array.isArray(t.autoTrim)&&(s=t.autoTrim[1],o=t.autoTrim[0]),(i||!1===i)&&(s=i),(n||!1===n)&&(o=n),"slurp"===s&&"slurp"===o?e.trim():("_"===s||"slurp"===s?e=String.prototype.trimLeft?e.trimLeft():e.replace(/^[\s\uFEFF\xA0]+/,""):"-"!==s&&"nl"!==s||(e=e.replace(/^(?:\n|\r|\r\n)/,"")),"_"===o||"slurp"===o?e=String.prototype.trimRight?e.trimRight():e.replace(/[\s\uFEFF\xA0]+$/,""):"-"!==o&&"nl"!==o||(e=e.replace(/(?:\n|\r|\r\n)$/,"")),e)}(e,n,f,t))&&(e=e.replace(/\\|'/g,"\\$&").replace(/\r\n|\n|\r/g,"\\n"),h.push(e))}for(;null!==(c=u.exec(e));){var m,y=c[1],b=c[2],_=c[3]||"";for(var w in s)if(s[w]===_){m=w;break}v(y,b),p=c.index+c[0].length,m||i("unrecognized tag type: "+_,e,p);var S=g(c.index,m),x=S.t;if("h"===x){var $=S.n||"";n.async&&r.test($)&&(S.a=!0,S.n=$.replace(r,"")),S=o(S),h.push(S)}else if("c"===x){if(a.n===S.n)return d?(d.d=h,a.b.push(d)):a.d=h,a;i("Helper start and end don't match",e,c.index+c[0].length)}else if("b"===x){d?(d.d=h,a.b.push(d)):a.d=h;var O=S.n||"";n.async&&r.test(O)&&(S.a=!0,S.n=O.replace(r,"")),d=S,h=[]}else if("s"===x){var P=S.n||"";n.async&&r.test(P)&&(S.a=!0,S.n=P.replace(r,"")),h.push(S)}else h.push(S)}if(!l)throw t('unclosed helper "'+a.n+'"');return v(e.slice(p,e.length),!1),a.d=h,a}({f:[]},!0);if(n.plugins)for(var m=0;m0)throw t((s?"Native":"")+"Helper '"+e+"' doesn't accept blocks");if(n&&n.length>0)throw t((s?"Native":"")+"Helper '"+e+"' doesn't accept filters")}var S={"&":"&","<":"<",">":">",'"':""","'":"'"};function x(e){return S[e]}var $=new _({}),O=new _({each:function(e,t){var i="",n=e.params[0];if(w("each",t,!1),e.async)return new Promise((function(t){!function e(t,i,n,s,o){n(t[i],i).then((function(a){s+=a,i===t.length-1?o(s):e(t,i+1,n,s,o)}))}(n,0,e.exec,i,t)}));for(var s=0;s"']/.test(t)?t.replace(/[&<>"']/g,x):t}}),C={varName:"it",autoTrim:[!1,"nl"],autoEscape:!0,defaultFilter:!1,tags:["{{","}}"],l:function(e,i){if("H"===e){var n=this.storage.helpers.get(i);if(n)return n;throw t("Can't find helper '"+i+"'")}if("F"===e){var s=this.storage.filters.get(i);if(s)return s;throw t("Can't find filter '"+i+"'")}},async:!1,storage:{helpers:O,nativeHelpers:P,filters:k,templates:$},prefixes:{h:"@",b:"#",i:"",r:"*",c:"/",e:"!"},cache:!1,plugins:[],useWith:!1};function E(e,t){var i={};return a(i,C),t&&a(i,t),e&&a(i,e),i.l.bind(i),i}function j(e,i){var n=E(i||{}),o=Function;if(n.async){if(!s)throw t("This environment doesn't support async/await");o=s}try{return new o(n.varName,"c","cb",f(e,n))}catch(i){throw i instanceof SyntaxError?t("Bad template syntax\n\n"+i.message+"\n"+Array(i.message.length+1).join("=")+"\n"+f(e,n)):i}}function N(e,t){var i;return t.cache&&t.name&&t.storage.templates.get(t.name)?t.storage.templates.get(t.name):(i="function"==typeof e?e:j(e,t),t.cache&&t.name&&t.storage.templates.define(t.name,i),i)}C.l.bind(C),e.compile=j,e.compileScope=b,e.compileScopeIntoFunction=y,e.compileToString=f,e.defaultConfig=C,e.filters=k,e.getConfig=E,e.helpers=O,e.nativeHelpers=P,e.parse=p,e.render=function(e,i,s,o){var a=E(s||{});if(!a.async)return N(e,a)(i,a);if(!o){if("function"==typeof n)return new n((function(t,n){try{t(N(e,a)(i,a))}catch(e){n(e)}}));throw t("Please provide a callback function, this env doesn't support Promises")}try{N(e,a)(i,a,o)}catch(e){return o(e)}},e.templates=$,Object.defineProperty(e,"__esModule",{value:!0})}(t)}(be={exports:{}},be.exports),be.exports);const we=new WeakMap,Se=(xe=e=>t=>{if(!(t instanceof P))throw new Error("unsafeHTML can only be used in text bindings");const i=we.get(t);if(void 0!==i&&S(e)&&e===i.value&&t.value===i.fragment)return;const n=document.createElement("template");n.innerHTML=e;const s=document.importNode(n.content,!0);t.setValue(s),we.set(t,{value:e,fragment:s})},(...e)=>{const t=xe(...e);return f.set(t,!0),t});var xe;const $e=e=>``;function Oe(e,t){var i,n;const{type:s,labels:o}=null!==(n=null===(i=null==e?void 0:e.layout)||void 0===i?void 0:i.sensors)&&void 0!==n?n:{type:"table",labels:!0};return I`
${t}
`}function Pe({hide:e=!1,hass:t,state:i,details:n,localize:s,openEntityPopover:o}){var a,r;if(e||void 0===i)return;const{type:l,heading:c,icon:d,unit:u,decimals:h}=n;let p;if(process.env.DEBUG&&console.log("ST: infoItem",{state:i,details:n}),"relativetime"===l)p=I`
`;else if("object"==typeof i){const[e]=i.entity_id.split("."),t=["component",e,"state",null!==(r=null===(a=i.attributes)||void 0===a?void 0:a.device_class)&&void 0!==r?r:"_",""].join(".");let n=s(i.state,t);"number"==typeof h&&(n=me(n,{decimals:h})),p=I`
${n} ${u||i.attributes.unit_of_measurement}
`}else{let e="number"==typeof h?me(i,{decimals:h}):i;p=I`
${e}${u}
`}if(!1===c)return p;const f=d?I``:I`${c}:`;return I`
${f}
${p}`}var ke;function Ce({state:e,mode:t,modeOptions:i,localize:n,setMode:s}){var o;const{type:a,hide_when_off:r,mode:l="none",list:c,name:d}=t;if(0===c.length||r&&e===ke.OFF)return null;let u=`state_attributes.climate.${a}_mode.`;"hvac"===a&&(u="component.climate.state._.");const h=d||n(`ui.card.climate.${"hvac"==a?"operation":`${a}_mode`}`),p=null===(o=null==i?void 0:i.headings)||void 0===o||o;return I`
${p?I`
${h}
`:""} ${c.map((({value:e,icon:t,name:o})=>I`
${(e=>e?!1===(null==i?void 0:i.icons)?null:I``:null)(t)} ${(e=>!1===e||!1===(null==i?void 0:i.names)?null:n(e,u))(o)}
`))}
`}_e.defaultConfig.autoEscape=!1,_e.filters.define("icon",$e),_e.filters.define("join",((e,t=", ")=>e.join(t))),_e.filters.define("css",((e,t)=>`${e}`)),_e.filters.define("debug",(e=>{try{return JSON.stringify(e)}catch(t){return`Not able to read valid JSON object from: ${e}`}})),function(e){e.OFF="off",e.HEAT="heat",e.COOL="cool",e.HEAT_COOL="heat_cool",e.AUTO="auto",e.DRY="dry",e.FAN_ONLY="fan_only"}(ke||(ke={}));const Ee={auto:"mdi:radiator",cooling:"mdi:snowflake",fan:"mdi:fan",heating:"mdi:radiator",idle:"mdi:radiator-disabled",off:"mdi:radiator-off"},je={auto:"hass:autorenew",cool:"hass:snowflake",dry:"hass:water-percent",fan_only:"hass:fan",heat_cool:"hass:autorenew",heat:"hass:fire",off:"hass:power"};function Ne(e,t){var i;const n=t.states[e.entity];let s="";return s=!0===(null==e?void 0:e.name)?n.attributes.name:null!==(i=null==e?void 0:e.name)&&void 0!==i?i:"",{entity:n,label:s}}function Te(e,t){return Array.isArray(e)?e.map((e=>{var{entity:i}=e,n=le(e,["entity"]);return Object.assign(Object.assign({},n),{state:t.states[i],entity:i})})):[]}var ze;!function(e){e.HVAC="hvac",e.FAN="fan",e.PRESET="preset",e.SWING="swing"}(ze||(ze={}));const Ae=Object.values(ze),Ve=[ze.HVAC,ze.PRESET],Re="hass:chevron-up",Ie="hass:chevron-down",Fe="mdi:plus",Ue="mdi:minus",Me={temperature:!1,state:!1};function He(e,t,i={}){return t[`${e}_modes`].filter((e=>function(e,t){var i;if("object"==typeof t[e])return!1!==t[e].include;return null===(i=null==t?void 0:t[e])||void 0===i||i}(e,i))).map((e=>{const t="object"==typeof i[e]?i[e]:{};return Object.assign({icon:je[e],value:e,name:e},t)}))}class Le extends ee{constructor(){super(...arguments),this.modes=[],this._hass={},this.sensors=[],this.showSensors=!0,this.name="",this.stepSize=.5,this._values={},this._updatingValues=!1,this._hide=Me,this._debouncedSetTemperature=ve((e=>{const{domain:t,service:i,data:n={}}=this.service;this._hass.callService(t,i,Object.assign(Object.assign({entity_id:this.config.entity},n),e))}),{wait:500}),this.localize=(e,t="")=>{var i;const n=this._hass.selectedLanguage||this._hass.language,s=`${t}${e}`,o=this._hass.resources[n];return null!==(i=null==o?void 0:o[s])&&void 0!==i?i:e},this.toggleEntityChanged=e=>{var t,i,n,s;if(!this.header||!(null===(t=null==this?void 0:this.header)||void 0===t?void 0:t.toggle))return;const o=e.target;this._hass.callService("homeassistant",o.checked?"turn_on":"turn_off",{entity_id:null===(s=null===(n=null===(i=this.header)||void 0===i?void 0:i.toggle)||void 0===n?void 0:n.entity)||void 0===s?void 0:s.entity_id})},this.setMode=(e,t)=>{e&&t?(this._hass.callService("climate",`set_${e}_mode`,{entity_id:this.config.entity,[`${e}_mode`]:t}),ie(this,"haptic","light")):ie(this,"haptic","failure")},this.openEntityPopover=(e=null)=>{ie(this,"hass-more-info",{entityId:e||this.config.entity})}}static get styles(){return te}static getConfigElement(){return window.document.createElement(`${e}-editor`)}setConfig(e){this.config=Object.assign({decimals:1},e)}updated(){super.connectedCallback();const e=Array.from(this.renderRoot.querySelectorAll("[with-hass]"));for(const t of Array.from(e))Array.from(t.attributes).forEach((e=>{e.name.startsWith("fwd-")&&(t[e.name.replace("fwd-","")]=e.value)})),t.hass=this._hass}set hass(e){var t,i,n,s;if(!this.config.entity)return;const o=e.states[this.config.entity];if(void 0===typeof o)return;this._hass=e,this.entity!==o&&(this.entity=o),this.header=function(e,t,i){if(!1===e)return!1;let n;n="string"==typeof(null==e?void 0:e.name)?e.name:!1!==(null==e?void 0:e.name)&&t.attributes.friendly_name;let s=t.attributes.hvac_action?Ee:je;return void 0!==(null==e?void 0:e.icon)&&(s=e.icon),{name:n,icon:s,toggle:(null==e?void 0:e.toggle)?Ne(e.toggle,i):null,faults:Te(null==e?void 0:e.faults,i)}}(this.config.header,o,e),this.service=null!==(i=null===(t=this.config)||void 0===t?void 0:t.service)&&void 0!==i&&i||{domain:"climate",service:"set_temperature"};const a=o.attributes;let r=function(e,t){if(!1===e)return{};if(e)return Object.keys(e).reduce(((i,n)=>{const s=e[n];return(null==s?void 0:s.hide)?i:Object.assign(Object.assign({},i),{[n]:null==t?void 0:t[n]})}),{});return"dual"===function(e){return"number"==typeof e.target_temp_high&&"number"==typeof e.target_temp_low?"dual":"single"}(t)?{target_temp_low:t.target_temp_low,target_temp_high:t.target_temp_high}:{temperature:t.temperature}}(null!==(s=null===(n=this.config)||void 0===n?void 0:n.setpoints)&&void 0!==s?s:null,a);this._updatingValues&&function(e,t){const i=Object.keys(e);return i.length===Object.keys(t).length&&!i.some((i=>(null==e?void 0:e[i])!==(null==t?void 0:t[i])))}(r,this._values)?this._updatingValues=!1:this._updatingValues||(this._values=r);const l=e=>Ae.includes(e)&&a[`${e}_modes`],c=e=>e.filter(l).map((e=>({type:e,hide_when_off:!1,list:He(e,a)})));let d=[];if(!1===this.config.control)d=[];else if(Array.isArray(this.config.control))d=c(this.config.control);else if("object"==typeof this.config.control){const e=Object.entries(this.config.control);d=e.length>0?e.filter((([e])=>l(e))).map((([e,t])=>{const{_name:i,_hide_when_off:n}=t,s=le(t,["_name","_hide_when_off"]);return{type:e,hide_when_off:n,name:i,list:He(e,a,s)}})):c(Ve)}else d=c(Ve);if(this.modes=d.map((e=>{if(e.type===ze.HVAC){const t=[],i=Object.values(ke);return e.list.forEach((e=>{const n=i.indexOf(e.value);t[n]=e})),Object.assign(Object.assign({},e),{list:t,mode:o.state})}const t=a[`${e.type}_mode`];return Object.assign(Object.assign({},e),{mode:t})})),this.config.step_size&&(this.stepSize=+this.config.step_size),this.config.hide&&(this._hide=Object.assign(Object.assign({},this._hide),this.config.hide)),!1===this.config.sensors)this.showSensors=!1;else if(3===this.config.version){this.sensors=[];const e=this.config.sensors.map(((e,t)=>{var i,n;const s=null!==(i=null==e?void 0:e.entity)&&void 0!==i?i:this.config.entity;let o=this.entity;return(null==e?void 0:e.entity)&&(o=this._hass.states[e.entity]),{id:null!==(n=null==e?void 0:e.id)&&void 0!==n?n:String(t),label:null==e?void 0:e.label,template:e.template,show:!1!==(null==e?void 0:e.show),entityId:s,context:o}})),t=e.map((e=>e.id)),i=[];t.includes("state")||i.push({id:"state",label:"{{ui.operation}}",template:"{{state.text}}",entityId:this.config.entity,context:this.entity}),t.includes("temperature")||i.push({id:"temperature",label:"{{ui.currently}}",template:"{{current_temperature|formatNumber}}",entityId:this.config.entity,context:this.entity}),this.sensors=[...i,...e]}else this.config.sensors&&(this.sensors=this.config.sensors.map((t=>{var i,{name:n,entity:s,attribute:o,unit:a=""}=t,r=le(t,["name","entity","attribute","unit"]);let l;const c=[n];return s?(l=e.states[s],c.push(null===(i=null==l?void 0:l.attributes)||void 0===i?void 0:i.friendly_name),o&&(l=l.attributes[o])):o&&o in this.entity.attributes&&(l=this.entity.attributes[o],c.push(o)),c.push(s),Object.assign(Object.assign({},r),{name:c.find((e=>!!e)),state:l,entity:s,unit:a})})))}render({_hide:e,_values:t,_updatingValues:i,config:n,entity:s}=this){var o,a,r;const l=[];if(this.stepSize<1&&0===this.config.decimals&&l.push(I`Decimals is set to 0 and step_size is lower than 1. Decrementing a setpoint will likely not work. Change one of the settings to clear this warning.`),!s)return I`Entity not available: ${n.entity}`;const{attributes:{min_temp:c=null,max_temp:d=null,hvac_action:u}}=s,h=this.getUnit(),p=null!==(r=null===(a=null===(o=this.config)||void 0===o?void 0:o.layout)||void 0===a?void 0:a.step)&&void 0!==r?r:"column",f="row"===p,g=[!this.header&&"no-header",u].filter((e=>!!e));let v;return 3===this.config.version?(v=this.sensors.filter((e=>!1!==e.show)).map((e=>function({context:e,entityId:t,template:i="{{state.text}}",label:n,hass:s,variables:o={},config:a,localize:r,openEntityPopover:l}){var c,d;const{state:u,attributes:h}=e,[p]=t.split("."),f=s.selectedLanguage||s.language,g="ui.card.climate.",v=Object.entries(s.resources[f]).reduce(((e,[t,i])=>(t.startsWith(g)&&(e[t.replace(g,"")]=i),e)),{}),m=Object.assign(Object.assign({},h),{state:{raw:u,text:r(u,`component.${p}.state._.`)},ui:v,v:o});_e.filters.define("formatNumber",((e,t={decimals:a.decimals})=>String(me(e,t)))),_e.filters.define("relativetime",((e,t={})=>``)),_e.filters.define("translate",((e,t="")=>r(e,t||"climate"!==p&&"humidifier"!==p?t:`state_attributes.${p}.${e}`)));const y=e=>_e.render(e,m,{useWith:!0}),b=y(i);if(!1===n||!1===(null===(d=null===(c=null==a?void 0:a.layout)||void 0===c?void 0:c.sensors)||void 0===d?void 0:d.labels))return I`
${Se(b)}
`;const _=n||"{{friendly_name}}",w=_.match(/^(mdi|hass):.*/)?$e(_):y(_);return I`
${Se(w)}
${Se(b)}
`}(Object.assign(Object.assign({},e),{variables:this.config.variables,hass:this._hass,config:this.config,localize:this.localize,openEntityPopover:this.openEntityPopover})))),v=Oe(this.config,v)):v=this.showSensors?function({_hide:e,entity:t,unit:i,hass:n,sensors:s,config:o,localize:a,openEntityPopover:r}){var l,c,d,u,h,p,f;const{state:g,attributes:{hvac_action:v,current_temperature:m}}=t,y=null===(d=null===(c=null===(l=null==o?void 0:o.layout)||void 0===l?void 0:l.sensors)||void 0===c?void 0:c.labels)||void 0===d||d;let b=a(g,"component.climate.state._.");return v&&(b=[a(v,"state_attributes.climate.hvac_action."),` (${b})`].join("")),Oe(o,[Pe({hide:e.temperature,state:`${me(m,o)}${i||""}`,hass:n,details:{heading:!!y&&(null!==(h=null===(u=null==o?void 0:o.label)||void 0===u?void 0:u.temperature)&&void 0!==h?h:a("ui.card.climate.currently"))}}),Pe({hide:e.state,state:b,hass:n,details:{heading:!!y&&(null!==(f=null===(p=null==o?void 0:o.label)||void 0===p?void 0:p.state)&&void 0!==f?f:a("ui.panel.lovelace.editor.card.generic.state"))}}),...s.map((e=>{var{name:t,state:i}=e,s=le(e,["name","state"]);return Pe({state:i,hass:n,localize:a,openEntityPopover:r,details:Object.assign(Object.assign({},s),{heading:y&&t})})}))||null].filter((e=>null!==e)))}({_hide:this._hide,unit:h,hass:this._hass,entity:this.entity,sensors:this.sensors,config:this.config,localize:this.localize,openEntityPopover:this.openEntityPopover}):"",I`${l} ${ye({header:this.header,toggleEntityChanged:this.toggleEntityChanged,entity:this.entity,openEntityPopover:this.openEntityPopover})}
${v} ${Object.entries(t).map((([e,t])=>{const s=["string","number"].includes(typeof t),o=!1!==h&&s;return I`

${me(t,n)} ${o?I`${h}`:m}

`}))}
${this.modes.map((e=>{var t,i,n;return Ce({state:s.state,mode:e,localize:this.localize,modeOptions:null!==(n=null===(i=null===(t=this.config)||void 0===t?void 0:t.layout)||void 0===i?void 0:i.mode)&&void 0!==n?n:{},setMode:this.setMode})}))}
`}setTemperature(e,t){this._updatingValues=!0;const i=this._values[t],n=Number(i)+e,{decimals:s}=this.config;this._values=Object.assign(Object.assign({},this._values),{[t]:+me(n,{decimals:s})}),this._debouncedSetTemperature(this._values)}getCardSize(){return 3}getUnit(){var e,t,i,n;return["boolean","string"].includes(typeof this.config.unit)?null===(e=this.config)||void 0===e?void 0:e.unit:null!==(n=null===(i=null===(t=this._hass.config)||void 0===t?void 0:t.unit_system)||void 0===i?void 0:i.temperature)&&void 0!==n&&n}}ce([Y()],Le.prototype,"config",void 0),ce([Y()],Le.prototype,"header",void 0),ce([Y()],Le.prototype,"service",void 0),ce([Y()],Le.prototype,"modes",void 0),ce([Y()],Le.prototype,"entity",void 0),ce([Y()],Le.prototype,"sensors",void 0),ce([Y()],Le.prototype,"showSensors",void 0),ce([Y()],Le.prototype,"name",void 0),ce([Y({type:Object})],Le.prototype,"_values",void 0),ce([Y()],Le.prototype,"_updatingValues",void 0),ce([Y()],Le.prototype,"_hide",void 0),customElements.define(e,Le),customElements.define(`${e}-editor`,class extends ee{static get styles(){return te}static get properties(){return{hass:{},config:{}}}static getStubConfig(){return Object.assign({},re)}setConfig(e){this.config=e||Object.assign({},re)}_openLink(){window.open("https://github.com/nervetattoo/simple-thermostat/blob/master/README.md")}render(){var e,t,i,n,s,o,a,r,l,c,d,u,h,p,f,g,v,m;return this.hass?I`
${!1!==this.config.header?I`
`:""}
${Object.values(ne).map((e=>I`${e}`))}
${Object.values(oe).map((e=>I`${e}`))}${Object.values(se).map((e=>I`${e}`))}
Configuration OptionsSettings for label, control, sensors, faults and hiding UI elements can only be configured in the code editor
`:I``}valueChanged(e){if(!this.config||!this.hass)return;const{target:t}=e,i=(n=this.config,JSON.parse(JSON.stringify(n)));var n;t.configValue&&(""===t.value?delete i[t.configValue]:function(e,t,i){const n=t.split(".");let s=e;for(;n.length-1;){var o=n.shift();s.hasOwnProperty(o)||(s[o]={}),s=s[o]}s[n[0]]=i}(i,t.configValue,void 0!==t.checked?t.checked:t.value)),ie(this,"config-changed",{config:i})}toggleHeader(e){this.config.header=!!e.target.checked&&{},ie(this,"config-changed",{config:this.config})}}),console.info(`%c${e}: 2.5.0`,"font-weight: bold"),window.customCards=window.customCards||[],window.customCards.push({type:e,name:"Simple Thermostat",preview:!1,description:"A different take on the thermostat card"}); From 097a346f13c626f442fbd0cdea9e9c7421ff2ca0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:49:41 +0100 Subject: [PATCH 047/158] Fix min power of lights --- esphome/common/bathroom_pwm_common.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 75921b7334096c6eed8eccdc3fad8d1c3f11b76b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:50:07 +0100 Subject: [PATCH 048/158] Living room blinds remote tweak --- packages/living_room_blinds.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: From 04c88c65644734f5925514d7de3045a76fdc96ab Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:50:36 +0100 Subject: [PATCH 049/158] Add Tin Hut shelving group --- lights.yaml | 7 +++++++ 1 file changed, 7 insertions(+) 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 From 36a708d37eabc4c4eaac88d030b5da92d6135681 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:51:21 +0100 Subject: [PATCH 050/158] Adjust living room grouping --- packages/living_room_lights.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index d5c6252e..f5cc3072 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -114,6 +114,12 @@ automation: - 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_group + state: "off" - condition: numeric_state entity_id: sensor.average_external_light_level below: input_number.living_room_light_level_trigger From 5759ab8358f282af897c9271f8fe8c1ffd3b8c76 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:51:40 +0100 Subject: [PATCH 051/158] Include Zigbee2MQTT in sidebar --- configuration.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/configuration.yaml b/configuration.yaml index 938ffc2b..6b25a86c 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -325,6 +325,10 @@ panel_iframe: title: "ESPHome" url: "http://viewpoint.house:6052" icon: mdi:car-esp + zigbee2mqtt: + title: "Z2M" + url: "http://viewpoint.house:8081" + icon: mdi:antenna cctv: title: "CCTV" url: "http://viewpoint.house:4999" From f1668361cec7b70b617e50078766118ad0710b25 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 16:51:51 +0100 Subject: [PATCH 052/158] Fix shouty box --- packages/alarm.yaml | 6 +++--- packages/ensuite.yaml | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) 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/ensuite.yaml b/packages/ensuite.yaml index 7f15a2a3..cc0df653 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -71,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: @@ -104,13 +104,14 @@ 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 From 44e590a1ba065923002db7f20157052e4229fe59 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 17:29:59 +0100 Subject: [PATCH 053/158] Fix yamllint --- packages/ensuite.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index cc0df653..ce7786ff 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -164,3 +164,4 @@ automation: action: service: light.turn_off entity_id: light.ensuite + From 012ad1883776f1c6e28172ff9480a02a3f6bd6a0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Wed, 5 Apr 2023 17:38:57 +0100 Subject: [PATCH 054/158] Moar fix --- packages/ensuite.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index ce7786ff..f55ce55c 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -110,8 +110,7 @@ automation: data: type: announce # method: all - message: - Hello! Enjoy your shower! The hot tank is currently {{ states('sensor.hot_tank_temperature') }} degrees. + 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 @@ -164,4 +163,3 @@ automation: action: service: light.turn_off entity_id: light.ensuite - From db2058a6ed89b7e161de351b4a47146fd181c3db Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:30:43 +0200 Subject: [PATCH 055/158] Numberbox 4.7 --- www/numberbox-card.js | 96 +++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/www/numberbox-card.js b/www/numberbox-card.js index 9e826e55..359fac6a 100644 --- a/www/numberbox-card.js +++ b/www/numberbox-card.js @@ -1,6 +1,6 @@ ((LitElement) => { -console.info('NUMBERBOX_CARD 4.3'); +console.info('NUMBERBOX_CARD 4.7'); const html = LitElement.prototype.html; const css = LitElement.prototype.css; class NumberBox extends LitElement { @@ -16,28 +16,31 @@ constructor() { render() { if(!this.stateObj){return html`Missing:'${this.config.entity}'`;} - if( this.config.name === undefined && this.stateObj.attributes.friendly_name ){ - this.config.name=this.stateObj.attributes.friendly_name; - } - if( this.config.icon === undefined && this.stateObj.attributes.icon ){ - this.config.icon=this.stateObj.attributes.icon; - } - if( this.config.picture === undefined && this.stateObj.attributes.entity_picture ){ - this.config.picture=this.stateObj.attributes.entity_picture; + + const k={name:'friendly_name',icon:'icon',picture:'entity_picture',unit:'unit_of_measurement'}; + for(const n of Object.keys(k)) { + if( this.config[n] === undefined && this.stateObj.attributes[k[n]] ){ + this.config[n]=this.stateObj.attributes[k[n]]; + } } - if( this.config.unit === undefined && this.stateObj.attributes.unit_of_measurement ){ - this.config.unit=this.stateObj.attributes.unit_of_measurement; + + const d={min:0,max:9e9,step:1,toggle:null}; + for(const j of Object.keys(d)) { + const b=j+'_entity'; + if(b in this.config && this.config[b] in this._hass.states ) { + const c=this._hass.states[this.config[b]]; this.old.t[this.config[b]]=c.last_updated + if( d[j]!==null && !isNaN(parseFloat(c.state)) ){this.config[j]=c.state;} + if(j=='toggle'){this.config[j]=c;} + } + if(d[j]!==null){ + if(this.config[j] === undefined){ this.config[j]=this.stateObj.attributes[j];} + if(isNaN(parseFloat(this.config[j]))){this.config[j]=d[j];} + } } - if(this.config.min === undefined){ this.config.min=this.stateObj.attributes.min;} - if(isNaN(parseFloat(this.config.min))){this.config.min=0;} - if(this.config.max === undefined){ this.config.max=this.stateObj.attributes.max;} - if(isNaN(parseFloat(this.config.max))){this.config.max=9e9;} - if('step_entity' in this.config && this.config.step_entity in this._hass.states && !isNaN(parseFloat(this._hass.states[this.config.step_entity].state))) {this.config.step=this._hass.states[this.config.step_entity].state;} - if(this.config.step === undefined){ this.config.step=this.stateObj.attributes.step;} return html` - ${(this.config.icon || this.config.picture || this.config.name) ? html`
+ ${(this.config.icon || this.config.picture || this.config.name) ? html`
${this.config.picture ? html` -
${this.renderNum()}
` : this.renderNum() } +
${this.renderNum()}
+ ${this.config.toggle ? html`
` : null } + ` : this.renderNum() }
`; } @@ -168,24 +174,28 @@ setNumb(c){ if( v===false ){ v=this.timeNum(this.state); v=isNaN(v)?this.config.min:v;} let adval=c?(v + Number(this.config.step)):(v - Number(this.config.step)); adval=Math.round(adval*1e9)/1e9; - if( adval <= Number(this.config.max) && adval >= Number(this.config.min)){ - this.pending=(adval); - if(this.config.delay){ - clearTimeout(this.bounce); - this.bounce = setTimeout(this.publishNum, this.config.delay, this); - }else{ - this.publishNum(this); + if(adval==this.state){ + clearTimeout(this.bounce);this.pending=false; + }else{ + if(adval <= Number(this.config.max) && adval >= Number(this.config.min)){ + this.pending = adval; + if(this.config.delay){ + clearTimeout(this.bounce); + this.bounce = setTimeout(this.publishNum, this.config.delay, this); + }else{ + this.publishNum(this); + } } } } publishNum(dhis){ + if(dhis.pending===false){return;} const s=dhis.config.service.split('.'); if(s[0]=='input_datetime'){dhis.pending=dhis.numTime(dhis.pending,1);} const v={entity_id: dhis.config.entity, [dhis.config.param]: dhis.pending}; dhis.pending=false; dhis.old.state=dhis.state; - dhis._hass.callService(s[0], s[1], v); } @@ -271,6 +281,10 @@ static get styles() { display: grid; grid-template-columns: repeat(2, auto); } + .gridt { + display: grid; + grid-template-columns: repeat(3, auto); + } .grid-content { display: grid; align-items: center; } @@ -537,6 +551,24 @@ render() { step="any" > +
+ + +
+ +
+
Date: Tue, 11 Apr 2023 22:35:03 +0200 Subject: [PATCH 056/158] Localtuya 5.1.0 --- custom_components/localtuya/climate.py | 4 + custom_components/localtuya/common.py | 81 +- custom_components/localtuya/config_flow.py | 78 +- custom_components/localtuya/const.py | 1 + custom_components/localtuya/cover.py | 4 +- custom_components/localtuya/fan.py | 2 +- custom_components/localtuya/manifest.json | 2 +- custom_components/localtuya/number.py | 7 +- .../localtuya/pytuya/__init__.py | 837 ++++++++++++++---- custom_components/localtuya/select.py | 10 +- custom_components/localtuya/switch.py | 6 +- .../localtuya/translations/en.json | 4 +- .../localtuya/translations/it.json | 4 +- .../localtuya/translations/pt-BR.json | 4 +- 14 files changed, 769 insertions(+), 275 deletions(-) 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..d4272d41 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, @@ -45,7 +47,6 @@ DATA_DISCOVERY, DOMAIN, PLATFORMS, - CONF_MANUAL_DPS, ) from .discovery import discover @@ -82,26 +83,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,13 +133,16 @@ 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), @@ -247,6 +242,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 +256,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 +494,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 = { @@ -564,6 +561,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 +587,28 @@ 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}"} + schema = schema_defaults(options_schema(self.entities), **defaults) else: defaults[CONF_PROTOCOL_VERSION] = "3.3" defaults[CONF_HOST] = "" @@ -611,7 +625,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": ""} diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 8010d18c..3a6c2529 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -28,6 +28,7 @@ # 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" 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..3f1e00bd 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "4.1.1", + "version": "5.0.0", "documentation": "https://github.com/rospogrigio/localtuya/", "dependencies": [], "codeowners": [ 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..746cc547 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 ) - payload = data[header_len:-end_len] - crc, _ = struct.unpack(MESSAGE_END_FMT, data[-end_len:]) - return TuyaMessage(seqno, cmd, retcode, payload, crc) + + +def parse_header(data): + """Unpack bytes into a TuyaHeader.""" + header_len = struct.calcsize(MESSAGE_HEADER_FMT) + + if len(data) < header_len: + raise DecodeError("Not enough data to unpack header") + + prefix, seqno, cmd, payload_len = struct.unpack( + MESSAGE_HEADER_FMT, data[:header_len] + ) + + if prefix != PREFIX_VALUE: + # self.debug('Header prefix wrong! %08X != %08X', prefix, PREFIX_VALUE) + raise DecodeError("Header prefix wrong! %08X != %08X" % (prefix, PREFIX_VALUE)) + + # sanity check. currently the max payload length is somewhere around 300 bytes + if payload_len > 1000: + raise DecodeError( + "Header claims the packet size is over 1000 bytes! It is most likely corrupt. Claimed size: %d bytes" + % payload_len + ) + + return TuyaHeader(prefix, seqno, cmd, payload_len) class AESCipher: @@ -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.warning( + "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 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) - def _setup_dispatcher(self): + 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,193 @@ 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: + self.debug("incomplete payload=%r (len:%d)", payload, len(payload)) + 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: + self.debug("incomplete payload=%r (len:%d)", payload, len(payload)) + 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: + raise DecodeError("could not decrypt data: wrong local_key?") + # 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: + self.debug( + "session key step 2 decrypt failed, payload=%r (len:%d)", + payload, + len(payload), + ) + 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 +1075,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 +1161,7 @@ async def connect( device_id, local_key, protocol_version, + enable_debug, listener=None, port=6668, timeout=5, @@ -706,6 +1174,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..947141cb 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,6 +96,7 @@ "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)", "manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (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)" } From dc33870733c5b37a44290b60b83f321496fdb9bd Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:36:32 +0200 Subject: [PATCH 057/158] Alexa media 4.6.1 --- custom_components/alexa_media/__init__.py | 41 ++- .../alexa_media/alarm_control_panel.py | 32 +- custom_components/alexa_media/alexa_entity.py | 199 ++++++++--- custom_components/alexa_media/alexa_media.py | 2 - .../alexa_media/binary_sensor.py | 28 +- custom_components/alexa_media/config_flow.py | 31 +- custom_components/alexa_media/const.py | 35 +- custom_components/alexa_media/helpers.py | 62 ++-- custom_components/alexa_media/light.py | 140 ++++---- custom_components/alexa_media/manifest.json | 12 +- custom_components/alexa_media/media_player.py | 12 +- custom_components/alexa_media/notify.py | 15 +- custom_components/alexa_media/sensor.py | 316 +++++++++++------- custom_components/alexa_media/services.py | 10 +- custom_components/alexa_media/switch.py | 13 +- .../alexa_media/translations/de.json | 16 +- 16 files changed, 583 insertions(+), 381 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index bd321dda..323af03e 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) @@ -331,9 +333,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 +403,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 +455,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 +492,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 +540,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 +559,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 +636,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( @@ -687,7 +697,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 +707,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 +855,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 +880,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), @@ -906,7 +921,7 @@ async def ws_handler(message_obj): f"{DOMAIN}_{hide_email(email)}"[0:32], {"push_activity": json_payload}, ) - except (AlexapyConnectionError): + except AlexapyConnectionError: # Catch case where activities doesn't report valid json pass elif command in ( diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 7841d1e1..a1b0262e 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 ( @@ -132,7 +132,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 +143,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 +153,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 +187,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 +217,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 +235,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..b9cca0b3 100644 --- a/custom_components/alexa_media/binary_sensor.py +++ b/custom_components/alexa_media/binary_sensor.py @@ -8,7 +8,6 @@ """ import logging -from typing import List # noqa pylint: disable=unused-import from alexapy import hide_serial from homeassistant.components.binary_sensor import ( @@ -30,9 +29,10 @@ _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] = [] + devices: list[BinarySensorEntity] = [] account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] include_filter = config.get(CONF_INCLUDE_DEVICES, []) @@ -40,13 +40,13 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf 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 +75,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..8bb27043 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.1" 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..cdd9a66c 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,7 +250,7 @@ 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 @@ -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..d996a5a6 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 ( @@ -74,7 +68,7 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa sensor platform.""" - devices: List[LightEntity] = [] + devices: list[LightEntity] = [] account = config[CONF_EMAIL] if config else discovery_info["config"][CONF_EMAIL] account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] include_filter = config.get(CONF_INCLUDE_DEVICES, []) @@ -85,20 +79,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 +121,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 +158,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 +176,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 +280,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 +319,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 +357,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 +513,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 +528,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 +546,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..a9d6596f 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.5", "packaging>=20.3", "wrapt>=1.12.1"], + "version": "4.6.1" } diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index 589fb1d7..ebaf40ba 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,7 +89,6 @@ # @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] @@ -188,7 +186,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 +280,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 +376,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 +517,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 +592,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 +1310,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..26e290ae 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 @@ -205,6 +203,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 +267,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 +321,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..4b341772 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 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,9 +56,10 @@ 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, @@ -73,7 +81,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 +89,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 +132,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 +171,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 +189,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 +245,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 +341,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 +390,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 +403,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 +439,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 +465,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 +484,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: @@ -451,32 +563,12 @@ def _handle_event(self, event): == 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 +577,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 +601,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 +613,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(), @@ -599,22 +657,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 +682,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 +696,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 +704,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/switch.py b/custom_components/alexa_media/switch.py index 5a2e95a3..ba62d7cd 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,7 +36,7 @@ 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), @@ -62,7 +62,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 +139,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 +188,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 +291,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/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", From 861ef1a488b74feff9befd1156ac9756b15272f0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:39:06 +0200 Subject: [PATCH 058/158] Thermal comfort 2.1.1 --- custom_components/thermal_comfort/__init__.py | 26 + .../thermal_comfort/config_flow.py | 12 +- .../thermal_comfort/manifest.json | 8 +- custom_components/thermal_comfort/sensor.py | 667 +++++++++++++----- .../thermal_comfort/translations/ca.json | 121 ++++ .../thermal_comfort/translations/cs.json | 39 + .../thermal_comfort/translations/da.json | 121 ++++ .../thermal_comfort/translations/de.json | 76 ++ .../thermal_comfort/translations/el.json | 121 ++++ .../thermal_comfort/translations/en.json | 76 ++ .../thermal_comfort/translations/es.json | 80 ++- .../thermal_comfort/translations/fr.json | 39 + .../thermal_comfort/translations/hu.json | 39 + .../thermal_comfort/translations/it.json | 121 ++++ .../thermal_comfort/translations/nb.json | 18 + .../thermal_comfort/translations/nl.json | 121 ++++ .../thermal_comfort/translations/pl.json | 121 ++++ .../thermal_comfort/translations/pt-BR.json | 39 + .../thermal_comfort/translations/pt.json | 39 + .../thermal_comfort/translations/ro.json | 121 ++++ .../thermal_comfort/translations/ru.json | 121 ++++ .../thermal_comfort/translations/sk.json | 121 ++++ .../thermal_comfort/translations/sv.json | 39 + .../thermal_comfort/translations/uk.json | 39 + 24 files changed, 2138 insertions(+), 187 deletions(-) create mode 100644 custom_components/thermal_comfort/translations/ca.json create mode 100644 custom_components/thermal_comfort/translations/cs.json create mode 100644 custom_components/thermal_comfort/translations/da.json create mode 100644 custom_components/thermal_comfort/translations/el.json create mode 100644 custom_components/thermal_comfort/translations/fr.json create mode 100644 custom_components/thermal_comfort/translations/hu.json create mode 100644 custom_components/thermal_comfort/translations/it.json create mode 100644 custom_components/thermal_comfort/translations/nb.json create mode 100644 custom_components/thermal_comfort/translations/nl.json create mode 100644 custom_components/thermal_comfort/translations/pl.json create mode 100644 custom_components/thermal_comfort/translations/pt-BR.json create mode 100644 custom_components/thermal_comfort/translations/pt.json create mode 100644 custom_components/thermal_comfort/translations/ro.json create mode 100644 custom_components/thermal_comfort/translations/ru.json create mode 100644 custom_components/thermal_comfort/translations/sk.json create mode 100644 custom_components/thermal_comfort/translations/sv.json create mode 100644 custom_components/thermal_comfort/translations/uk.json 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": "Можливі судинні розлади" + } + } + } + } +} From 970bb9efbd5b6c35533dba05176446ae76a37875 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:41:02 +0200 Subject: [PATCH 059/158] Better thermostat 1.0.2 --- .../better_thermostat/adapters/deconz.py | 2 +- .../better_thermostat/climate.py | 1 - .../better_thermostat/config_flow.py | 3 --- .../better_thermostat/events/trv.py | 1 - .../better_thermostat/manifest.json | 20 ++++++++-------- .../better_thermostat/model_fixes/SPZB0001.py | 4 ++++ .../better_thermostat/utils/controlling.py | 23 ++++++++++++++++--- .../better_thermostat/utils/helpers.py | 8 +++---- 8 files changed, 39 insertions(+), 23 deletions(-) diff --git a/custom_components/better_thermostat/adapters/deconz.py b/custom_components/better_thermostat/adapters/deconz.py index 6cc07f17..696d159c 100644 --- a/custom_components/better_thermostat/adapters/deconz.py +++ b/custom_components/better_thermostat/adapters/deconz.py @@ -54,7 +54,7 @@ async def set_offset(self, entity_id, offset): await self.hass.services.async_call( "deconz", "configure", - {"entity_id": entity_id, "offset": offset}, + {"entity": entity_id, "field": "/config", "data": {"offset": offset}}, blocking=True, limit=None, context=self._context, diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index b941c0b1..aa9fa801 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -768,7 +768,6 @@ async def startup(self): break async def calculate_heating_power(self): - if ( self.heating_start_temp is not None and self.heating_end_temp is not None diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py index 2526168a..d4982256 100644 --- a/custom_components/better_thermostat/config_flow.py +++ b/custom_components/better_thermostat/config_flow.py @@ -126,7 +126,6 @@ async def async_step_confirm(self, user_input=None, confirm_type=None): 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 @@ -354,7 +353,6 @@ async def async_step_advanced( ): """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 @@ -484,7 +482,6 @@ async def async_step_advanced( ) 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) diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index 91f7dc2f..2b6d21d1 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -319,7 +319,6 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]: _new_heating_setpoint = self.bt_target_temp elif _calibration_type == 1: - _round_calibration = self.real_trvs[entity_id]["advanced"].get( "calibration_round" ) diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json index dd0fd3bf..b81bb160 100644 --- a/custom_components/better_thermostat/manifest.json +++ b/custom_components/better_thermostat/manifest.json @@ -1,20 +1,20 @@ { "domain": "better_thermostat", "name": "Better Thermostat", - "documentation": "https://github.com/KartoffelToby/better_thermostat", - "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", - "iot_class": "local_push", - "version": "1.0.1", - "config_flow": true, - "dependencies": [ - "climate", - "recorder" - ], "after_dependencies": [ "climate" ], "codeowners": [ "@kartoffeltoby" ], - "requirements": [] + "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.0.2" } diff --git a/custom_components/better_thermostat/model_fixes/SPZB0001.py b/custom_components/better_thermostat/model_fixes/SPZB0001.py index f3f27188..19d5d61f 100644 --- a/custom_components/better_thermostat/model_fixes/SPZB0001.py +++ b/custom_components/better_thermostat/model_fixes/SPZB0001.py @@ -1,4 +1,8 @@ def fix_local_calibration(self, entity_id, offset): + if offset > 5: + offset = 5 + elif offset < -5: + offset = -5 return offset diff --git a/custom_components/better_thermostat/utils/controlling.py b/custom_components/better_thermostat/utils/controlling.py index 92045171..dfed705e 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -42,11 +42,28 @@ async def control_queue(self): self.ignore_states = True result = True for trv in self.real_trvs.keys(): - _temp = await control_trv(self, trv) - if _temp is False: + 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: - await self.control_queue_task.put(self) + 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 diff --git a/custom_components/better_thermostat/utils/helpers.py b/custom_components/better_thermostat/utils/helpers.py index 4788e4af..8fac4b59 100644 --- a/custom_components/better_thermostat/utils/helpers.py +++ b/custom_components/better_thermostat/utils/helpers.py @@ -147,8 +147,8 @@ def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]: ) if _overheating_protection is True: - if (self.cur_temp + 0.10) >= self.bt_target_temp: - _new_local_calibration += 1.0 + 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) @@ -229,8 +229,8 @@ def calculate_setpoint_override(self, entity_id) -> Union[float, None]: ) if _overheating_protection is True: - if (self.cur_temp + 0.10) >= self.bt_target_temp: - _calibrated_setpoint -= 1.0 + 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) From f03954646b3b28ceef4ba3a3e646db872d3fd9ba Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:47:35 +0200 Subject: [PATCH 060/158] Octopus energy v6.6.1 --- custom_components/octopus_energy/__init__.py | 89 +- .../octopus_energy/api_client.py | 286 +++- .../octopus_energy/binary_sensor.py | 302 +--- .../octopus_energy/binary_sensors/__init__.py | 236 +++ .../binary_sensors/saving_sessions.py | 100 ++ .../binary_sensors/target_rate.py | 233 +++ .../octopus_energy/config_flow.py | 58 +- custom_components/octopus_energy/const.py | 17 +- .../octopus_energy/diagnostics.py | 12 +- .../octopus_energy/manifest.json | 18 +- custom_components/octopus_energy/sensor.py | 1316 ++--------------- .../octopus_energy/sensors/__init__.py | 276 ++++ .../sensors/electricity/__init__.py | 0 .../sensors/electricity/base.py | 41 + .../electricity/current_consumption.py | 91 ++ .../sensors/electricity/current_demand.py | 88 ++ .../sensors/electricity/current_rate.py | 114 ++ .../sensors/electricity/next_rate.py | 105 ++ .../previous_accumulative_consumption.py | 110 ++ .../electricity/previous_accumulative_cost.py | 130 ++ .../sensors/electricity/previous_rate.py | 105 ++ .../sensors/electricity/standing_charge.py | 98 ++ .../octopus_energy/sensors/gas/__init__.py | 0 .../octopus_energy/sensors/gas/base.py | 35 + .../sensors/gas/current_consumption.py | 91 ++ .../sensors/gas/current_rate.py | 106 ++ .../gas/previous_accumulative_consumption.py | 115 ++ .../previous_accumulative_consumption_kwh.py | 113 ++ .../sensors/gas/previous_accumulative_cost.py | 134 ++ .../sensors/gas/standing_charge.py | 98 ++ .../sensors/saving_sessions/__init__.py | 0 .../sensors/saving_sessions/points.py | 73 + .../octopus_energy/translations/en.json | 28 +- .../octopus_energy/utils/__init__.py | 125 ++ .../octopus_energy/utils/check_tariff.py | 41 + 35 files changed, 3180 insertions(+), 1604 deletions(-) create mode 100644 custom_components/octopus_energy/binary_sensors/__init__.py create mode 100644 custom_components/octopus_energy/binary_sensors/saving_sessions.py create mode 100644 custom_components/octopus_energy/binary_sensors/target_rate.py create mode 100644 custom_components/octopus_energy/sensors/__init__.py create mode 100644 custom_components/octopus_energy/sensors/electricity/__init__.py create mode 100644 custom_components/octopus_energy/sensors/electricity/base.py create mode 100644 custom_components/octopus_energy/sensors/electricity/current_consumption.py create mode 100644 custom_components/octopus_energy/sensors/electricity/current_demand.py create mode 100644 custom_components/octopus_energy/sensors/electricity/current_rate.py create mode 100644 custom_components/octopus_energy/sensors/electricity/next_rate.py create mode 100644 custom_components/octopus_energy/sensors/electricity/previous_accumulative_consumption.py create mode 100644 custom_components/octopus_energy/sensors/electricity/previous_accumulative_cost.py create mode 100644 custom_components/octopus_energy/sensors/electricity/previous_rate.py create mode 100644 custom_components/octopus_energy/sensors/electricity/standing_charge.py create mode 100644 custom_components/octopus_energy/sensors/gas/__init__.py create mode 100644 custom_components/octopus_energy/sensors/gas/base.py create mode 100644 custom_components/octopus_energy/sensors/gas/current_consumption.py create mode 100644 custom_components/octopus_energy/sensors/gas/current_rate.py create mode 100644 custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption.py create mode 100644 custom_components/octopus_energy/sensors/gas/previous_accumulative_consumption_kwh.py create mode 100644 custom_components/octopus_energy/sensors/gas/previous_accumulative_cost.py create mode 100644 custom_components/octopus_energy/sensors/gas/standing_charge.py create mode 100644 custom_components/octopus_energy/sensors/saving_sessions/__init__.py create mode 100644 custom_components/octopus_energy/sensors/saving_sessions/points.py create mode 100644 custom_components/octopus_energy/utils/__init__.py create mode 100644 custom_components/octopus_energy/utils/check_tariff.py diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 768ff034..5671fcdc 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -8,11 +8,15 @@ DataUpdateCoordinator ) +from homeassistant.helpers import issue_registry as ir + 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, @@ -31,14 +35,21 @@ get_active_tariff_code ) +from .utils.check_tariff import (async_check_valid_tariff) + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry): """This is called from the config flow.""" hass.data.setdefault(DOMAIN, {}) - if CONFIG_MAIN_API_KEY in entry.data: - await async_setup_dependencies(hass, entry.data) + 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( @@ -48,7 +59,7 @@ async def async_setup_entry(hass, entry): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") ) - elif CONFIG_TARGET_NAME in entry.data: + 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 @@ -61,12 +72,31 @@ async def async_setup_entry(hass, entry): return True -async def async_get_current_electricity_agreement_tariff_codes(client, config): - account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) +async def async_get_current_electricity_agreement_tariff_codes(hass, client: OctopusEnergyApiClient, account_id: str): + account_info = None + try: + account_info = await client.async_get_account(account_id) + except: + # count exceptions as failure to retrieve account + _LOGGER.debug('Failed to retrieve account') + + 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: + ir.async_delete_issue(hass, DOMAIN, f"account_not_found_{account_id}") tariff_codes = {} current = now() - if len(account_info["electricity_meter_points"]) > 0: + 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 @@ -77,33 +107,52 @@ async def async_get_current_electricity_agreement_tariff_codes(client, config): key = (point["mpan"], is_smart_meter) if key not in tariff_codes: tariff_codes[(point["mpan"], is_smart_meter)] = active_tariff_code + await async_check_valid_tariff(hass, client, active_tariff_code, True) return tariff_codes async def async_setup_dependencies(hass, config): """Setup the coordinator and api client which will be shared by various entities""" - if DATA_CLIENT not in hass.data[DOMAIN]: - client = OctopusEnergyApiClient(config[CONFIG_MAIN_API_KEY]) - hass.data[DOMAIN][DATA_CLIENT] = client - hass.data[DOMAIN][DATA_ACCOUNT_ID] = 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] + + gas_price_cap = None + if CONFIG_MAIN_GAS_PRICE_CAP in config: + gas_price_cap = config[CONFIG_MAIN_GAS_PRICE_CAP] - setup_rates_coordinator(hass, client, config) + _LOGGER.info(f'electricity_price_cap: {electricity_price_cap}') + _LOGGER.info(f'gas_price_cap: {gas_price_cap}') - setup_saving_sessions_coordinators(hass, client) - - account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + 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] - hass.data[DOMAIN][DATA_ACCOUNT] = account_info + setup_rates_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) -def setup_rates_coordinator(hass, client, config): + setup_saving_sessions_coordinators(hass) + + account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) + + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + +def setup_rates_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_RATES] = [] + + if DATA_ELECTRICITY_RATES_COORDINATOR in hass.data[DOMAIN]: + _LOGGER.info("Rates coordinator has already been configured, so skipping") + return + async def async_update_electricity_rates_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_RATES not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][DATA_RATES]) == 0): - tariff_codes = await async_get_current_electricity_agreement_tariff_codes(client, config) + tariff_codes = await async_get_current_electricity_agreement_tariff_codes(hass, client, account_id) _LOGGER.debug(f'tariff_codes: {tariff_codes}') period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) @@ -133,11 +182,15 @@ async def async_update_electricity_rates_data(): update_interval=timedelta(minutes=1), ) -def setup_saving_sessions_coordinators(hass, client: OctopusEnergyApiClient): +def setup_saving_sessions_coordinators(hass): + if DATA_SAVING_SESSIONS_COORDINATOR in hass.data[DOMAIN]: + return + 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: savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) diff --git a/custom_components/octopus_energy/api_client.py b/custom_components/octopus_energy/api_client.py index 1f88c96d..5b00aa82 100644 --- a/custom_components/octopus_energy/api_client.py +++ b/custom_components/octopus_energy/api_client.py @@ -24,12 +24,20 @@ meterPoint {{ mpan meters(includeInactive: false) {{ + makeAndType serialNumber + makeAndType smartExportElectricityMeter {{ deviceId + manufacturer + model + firmwareVersion }} smartImportElectricityMeter {{ deviceId + manufacturer + model + firmwareVersion }} }} agreements {{ @@ -38,18 +46,23 @@ tariff {{ ...on StandardTariff {{ tariffCode + productCode }} ...on DayNightTariff {{ tariffCode + productCode }} ...on ThreeRateTariff {{ tariffCode + productCode }} ...on HalfHourlyTariff {{ tariffCode + productCode }} ...on PrepayTariff {{ tariffCode + productCode }} }} }} @@ -61,12 +74,20 @@ meters(includeInactive: false) {{ serialNumber consumptionUnits + modelName + smartGasMeter {{ + deviceId + manufacturer + model + firmwareVersion + }} }} agreements {{ validFrom validTo tariff {{ tariffCode + productCode }} }} }} @@ -92,9 +113,23 @@ }} }}''' +live_consumption_query = '''query {{ + smartMeterTelemetry( + deviceId: "{device_id}" + grouping: ONE_MINUTE + start: "{period_from}" + end: "{period_to}" + ) {{ + readAt + consumptionDelta + demand + }} +}}''' + + class OctopusEnergyApiClient: - def __init__(self, api_key): + def __init__(self, api_key, electricity_price_cap = None, gas_price_cap = None): if (api_key == None): raise Exception('API KEY is not set') @@ -106,9 +141,12 @@ def __init__(self, api_key): 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 != None and (self._graphql_expiration - timedelta(minutes=5)) > now()): + if (self._graphql_expiration is not None and (self._graphql_expiration - timedelta(minutes=5)) > now()): return async with aiohttp.ClientSession() as client: @@ -116,12 +154,16 @@ async def async_refresh_token(self): 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 != None and "data" in token_response_body): + 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") - raise Exception('Failed to refresh token') async def async_get_account(self, account_id): """Get the user's account""" @@ -135,36 +177,92 @@ async def async_get_account(self, account_id): 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(account_response_body) + _LOGGER.debug(f'account: {account_response_body}') - if (account_response_body != None and "data" in 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"] != None, - "is_smart_meter": m["smartImportElectricityMeter"] != None or m["smartExportElectricityMeter"] != None, - }, mp["meterPoint"]["meters"])), - "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, - }, mp["meterPoint"]["agreements"])) - }, account_response_body["data"]["account"]["electricityAgreements"])), - "gas_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": m["smartImportElectricityMeter"] is not None or m["smartExportElectricityMeter"] is not None, + "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"], - }, mp["meterPoint"]["meters"])), + "serial_number": m["serialNumber"], + "consumption_units": m["consumptionUnits"], + "is_smart_meter": m["smartGasMeter"] is not None, + "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, - }, mp["meterPoint"]["agreements"])) - }, account_response_body["data"]["account"]["gasAgreements"])), - } + "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") @@ -182,7 +280,7 @@ async def async_get_saving_sessions(self, account_id): 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 != None and "data" in response_body): + 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: { @@ -195,6 +293,29 @@ async def async_get_saving_sessions(self, account_id): return None + async def async_get_smart_meter_consumption(self, device_id, period_from, period_to): + """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, period_to=period_to) } + 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"]), + "demand": float(mp["demand"]) if "demand" in mp and mp["demand"] is not None else None, + "startAt": parse_datetime(mp["readAt"]) + }, 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 = [] @@ -205,8 +326,9 @@ async def async_get_electricity_standard_rates(self, product_code, tariff_code, try: data = await self.__async_read_response(response, url) if data == None: - return None - results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) + + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._electricity_price_cap) except: _LOGGER.error(f'Failed to extract standard rates: {url}') raise @@ -223,10 +345,10 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code, try: data = await self.__async_read_response(response, url) if data == None: - return None + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) # 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) + 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) @@ -242,7 +364,7 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code, 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) + 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) @@ -252,7 +374,6 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code, # 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) - _LOGGER.debug(results) return results @@ -260,10 +381,13 @@ async def async_get_electricity_rates(self, tariff_code, is_smart_meter, period_ """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 (await self.__async_is_tracker_tariff(tariff_code)): - return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + if (self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) elif (tariff_parts["rate"].startswith("1")): return await self.async_get_electricity_standard_rates(product_code, tariff_code, period_from, period_to) else: @@ -277,7 +401,7 @@ async def async_get_electricity_consumption(self, mpan, serial_number, period_fr async with client.get(url, auth=auth) as response: data = await self.__async_read_response(response, url) - if (data != None and "results" in data): + if (data is not None and "results" in data): data = data["results"] results = [] for item in data: @@ -296,10 +420,13 @@ async def async_get_electricity_consumption(self, mpan, serial_number, period_fr 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"] - if (await self.__async_is_tracker_tariff(tariff_code)): - return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to) + if (self.__async_is_tracker_tariff(tariff_code)): + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._gas_price_cap) results = [] async with aiohttp.ClientSession() as client: @@ -309,9 +436,9 @@ async def async_get_gas_rates(self, tariff_code, period_from, period_to): try: data = await self.__async_read_response(response, url) if data == None: - return None + return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._gas_price_cap) - results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code) + results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._gas_price_cap) except: _LOGGER.error(f'Failed to extract standard gas rates: {url}') raise @@ -325,7 +452,7 @@ async def async_get_gas_consumption(self, mprn, serial_number, period_from, peri 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 != None and "results" in data): + if (data is not None and "results" in data): data = data["results"] results = [] for item in data: @@ -341,24 +468,25 @@ async def async_get_gas_consumption(self, mprn, serial_number, period_from, peri return None - async def async_get_products(self, is_variable): + 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?is_variable={is_variable}' + url = f'{self._base_url}/v1/products/{product_code}' async with client.get(url, auth=auth) as response: - data = await self.__async_read_response(response, url) - if (data != None and "results" in data): - return data["results"] + return await self.__async_read_response(response, url) - return [] + return None 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 await self.__async_is_tracker_tariff(tariff_code): + if self.__async_is_tracker_tariff(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) result = None @@ -368,9 +496,11 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from, async with client.get(url, auth=auth) as response: try: data = await self.__async_read_response(response, url) - if (data != None and "results" in data and len(data["results"]) > 0): + if data is None: + return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) + + if ("results" in data and len(data["results"]) > 0): result = { - "value_exc_vat": float(data["results"][0]["value_exc_vat"]), "value_inc_vat": float(data["results"][0]["value_inc_vat"]) } except: @@ -382,9 +512,12 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from, 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 await self.__async_is_tracker_tariff(tariff_code): + if self.__async_is_tracker_tariff(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) result = None @@ -394,9 +527,11 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t async with client.get(url, auth=auth) as response: try: data = await self.__async_read_response(response, url) - if (data != None and "results" in data and len(data["results"]) > 0): + if data is None: + return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) + + if ("results" in data and len(data["results"]) > 0): result = { - "value_exc_vat": float(data["results"][0]["value_exc_vat"]), "value_inc_vat": float(data["results"][0]["value_inc_vat"]) } except: @@ -405,27 +540,29 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t return result - async def __async_is_tracker_tariff(self, tariff_code): + def __async_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] - async with aiohttp.ClientSession() as client: - auth = aiohttp.BasicAuth(self._api_key, '') - url = f'https://api.octopus.energy/v1/products/{product_code}' - async with client.get(url, auth=auth) as response: - data = await self.__async_read_response(response, url) - if data == None: - return False - - is_tracker = "is_tracker" in data and data["is_tracker"] - self._product_tracker_cache[product_code] = is_tracker - return is_tracker + return False - async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to): + async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to, price_cap: float = None): """Get the tracker rates""" + tariff_parts = get_tariff_parts(tariff_code) + if tariff_parts is None: + return None + + product_code = tariff_parts["product_code"] + + # If we know our tariff is not a tracker rate, then don't bother asking + if product_code in self._product_tracker_cache and self._product_tracker_cache[product_code] == False: + return None results = [] async with aiohttp.ClientSession() as client: @@ -441,20 +578,18 @@ async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to) for period in data["periods"]: valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) - vat = float(period["breakdown"]["standing_charge"]["VAT"]) if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): - vat = float(period["breakdown"]["unit_charge"]["VAT"]) items.append( { "valid_from": valid_from.strftime("%Y-%m-%dT%H:%M:%SZ"), "valid_to": valid_to.strftime("%Y-%m-%dT%H:%M:%SZ"), - "value_exc_vat": float(period["unit_rate"]) - vat, "value_inc_vat": float(period["unit_rate"]), } ) - results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code) + results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code, price_cap) + self._product_tracker_cache[product_code] = True except: _LOGGER.error(f'Failed to extract tracker gas rates: {url}') raise @@ -478,9 +613,7 @@ async def __async_get_tracker_standing_charge__(self, tariff_code, period_from, valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): - vat = float(period["breakdown"]["standing_charge"]["VAT"]) return { - "value_exc_vat": float(period["standing_charge"]) - vat, "value_inc_vat": float(period["standing_charge"]) } except: @@ -536,7 +669,10 @@ async def __async_read_response(self, response, url): text = await response.text() if response.status >= 400: - _LOGGER.error(f'Request failed ({url}): {response.status}; {text}') + if response.status >= 500: + _LOGGER.error(f'Octopus Energy server error ({url}): {response.status}; {text}') + else: + _LOGGER.error(f'Failed to send request ({url}): {response.status}; {text}') return None try: diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py index d81b4a41..696e57e9 100644 --- a/custom_components/octopus_energy/binary_sensor.py +++ b/custom_components/octopus_energy/binary_sensor.py @@ -1,53 +1,23 @@ from datetime import timedelta import logging -from custom_components.octopus_energy.utils import apply_offset +from .binary_sensors.saving_sessions import OctopusEnergySavingSessions +from .binary_sensors.target_rate import OctopusEnergyTargetRate -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 homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import config_validation as cv, entity_platform, service +from homeassistant.helpers import config_validation as cv, entity_platform from .const import ( - CONFIG_TARGET_OFFSET, DOMAIN, CONFIG_MAIN_API_KEY, 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, DATA_ELECTRICITY_RATES_COORDINATOR, DATA_SAVING_SESSIONS_COORDINATOR, DATA_ACCOUNT ) -from .target_sensor_utils import ( - calculate_continuous_times, - calculate_intermittent_times, - is_target_rate_active -) - -from .sensor_utils import ( - is_saving_sessions_event_active, - get_next_saving_sessions_event -) - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=1) @@ -115,269 +85,3 @@ async def async_setup_target_sensors(hass, entry, async_add_entities): entities = [OctopusEnergyTargetRate(coordinator, config, is_export)] async_add_entities(entities, True) - -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, - offset, - 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, - offset, - 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 = is_target_rate_active(current_date, self._target_rates, offset) - - if offset != None and active_result["next_time"] != None: - self._attributes["next_time"] = apply_offset(active_result["next_time"], offset) - else: - self._attributes["next_time"] = active_result["next_time"] - - self._attributes["current_duration_in_hours"] = active_result["current_duration_in_hours"] - self._attributes["next_duration_in_hours"] = active_result["next_duration_in_hours"] - - 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() - -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() - self._state = is_saving_sessions_event_active(current_date, self._events) - 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/__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 index 0d7605fd..05b125f8 100644 --- a/custom_components/octopus_energy/config_flow.py +++ b/custom_components/octopus_energy/config_flow.py @@ -13,6 +13,12 @@ CONFIG_MAIN_API_KEY, CONFIG_MAIN_ACCOUNT_ID, + CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION, + 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, @@ -81,7 +87,15 @@ async def async_setup_initial_account(self, user_input): """Setup the initial account based on the provided user input""" errors = {} - client = OctopusEnergyApiClient(user_input[CONFIG_MAIN_API_KEY]) + 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 == None): errors[CONFIG_MAIN_ACCOUNT_ID] = "account_not_found" @@ -91,7 +105,7 @@ async def async_setup_initial_account(self, user_input): # Setup our basic sensors return self.async_create_entry( - title="Octopus Energy", + title="Account", data=user_input ) @@ -101,7 +115,7 @@ async def async_setup_target_rate_schema(self): meters = [] now = utcnow() - if len(account_info["electricity_meter_points"]) > 0: + 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 != None: @@ -183,10 +197,12 @@ def __init__(self, entry) -> None: 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" meters = [] now = utcnow() - if len(account_info["electricity_meter_points"]) > 0: + 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 != None: @@ -215,7 +231,12 @@ async def __async_setup_target_rate_schema(self, config, errors): 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 ), @@ -234,10 +255,32 @@ async def async_step_init(self, user_input): 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] + + 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_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: @@ -255,6 +298,13 @@ async def async_step_user(self, user_input): 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") diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index a0f69ac0..f97985e9 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -1,9 +1,16 @@ 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_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" @@ -23,14 +30,22 @@ DATA_ACCOUNT = "ACCOUNT" DATA_SAVING_SESSIONS = "SAVING_SESSIONS" DATA_SAVING_SESSIONS_COORDINATOR = "SAVING_SESSIONS_COORDINATOR" +DATA_KNOWN_TARIFF = "KNOWN_TARIFF" +DATA_GAS_RATES = "GAS_RATES" 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_]+$" -REGEX_TARIFF_PARTS = "^([A-Z])-([0-9A-Z]+)-([A-Z0-9-]+)-([A-Z])$" +# 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_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/diagnostics.py b/custom_components/octopus_energy/diagnostics.py index eaa2f23b..23943d95 100644 --- a/custom_components/octopus_energy/diagnostics.py +++ b/custom_components/octopus_energy/diagnostics.py @@ -21,21 +21,21 @@ async def async_get_device_diagnostics(hass, config_entry, device): account_info = await client.async_get_account(hass.data[DOMAIN][DATA_ACCOUNT_ID]) - points_length = len(account_info["electricity_meter_points"]) - if points_length > 0: + 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" }) + 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 = len(account_info["gas_meter_points"]) - if points_length > 0: + 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" }) + 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)') diff --git a/custom_components/octopus_energy/manifest.json b/custom_components/octopus_energy/manifest.json index f0b47316..7f0d4b64 100644 --- a/custom_components/octopus_energy/manifest.json +++ b/custom_components/octopus_energy/manifest.json @@ -1,16 +1,18 @@ { "domain": "octopus_energy", "name": "Octopus Energy", + "codeowners": [ + "@bottlecapdave" + ], "config_flow": true, + "dependencies": [ + "repairs" + ], "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/", + "homekit": {}, + "iot_class": "cloud_polling", "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], - "codeowners": [ - "@bottlecapdave" - ], - "version": "5.4.1", - "iot_class": "cloud_polling" + "version": "6.6.1", + "zeroconf": [] } \ No newline at end of file diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index dfdd3272..173904be 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,28 +1,32 @@ from datetime import timedelta import logging +from .utils.check_tariff import async_check_valid_tariff +from .sensors.electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption +from .sensors.electricity.current_demand import OctopusEnergyCurrentElectricityDemand +from .sensors.electricity.current_rate import OctopusEnergyElectricityCurrentRate +from .sensors.electricity.next_rate import OctopusEnergyElectricityNextRate +from .sensors.electricity.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeElectricityConsumption +from .sensors.electricity.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeElectricityCost +from .sensors.electricity.previous_rate import OctopusEnergyElectricityPreviousRate +from .sensors.electricity.standing_charge import OctopusEnergyElectricityCurrentStandingCharge +from .sensors.gas.current_rate import OctopusEnergyGasCurrentRate +from .sensors.gas.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeGasConsumption +from .sensors.gas.previous_accumulative_consumption_kwh import OctopusEnergyPreviousAccumulativeGasConsumptionKwh +from .sensors.gas.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeGasCost +from .sensors.gas.current_consumption import OctopusEnergyCurrentGasConsumption +from .sensors.gas.standing_charge import OctopusEnergyGasCurrentStandingCharge + +from .sensors.saving_sessions.points import OctopusEnergySavingSessionPoints + from homeassistant.util.dt import (utcnow, now, as_utc, parse_datetime) from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, DataUpdateCoordinator ) -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass -) -from homeassistant.const import ( - ENERGY_KILO_WATT_HOUR, - VOLUME_CUBIC_METERS -) -from homeassistant.helpers.restore_state import RestoreEntity -from .sensor_utils import ( +from .sensors import ( async_get_consumption_data, - calculate_electricity_consumption, - async_calculate_electricity_cost, - calculate_gas_consumption, - async_calculate_gas_cost + async_get_live_consumption ) from .utils import (get_active_tariff_code) @@ -30,18 +34,25 @@ DOMAIN, CONFIG_MAIN_API_KEY, + CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION, + CONFIG_MAIN_CALORIFIC_VALUE, + CONFIG_MAIN_ELECTRICITY_PRICE_CAP, + CONFIG_MAIN_GAS_PRICE_CAP, DATA_ELECTRICITY_RATES_COORDINATOR, DATA_SAVING_SESSIONS_COORDINATOR, DATA_CLIENT, - DATA_ACCOUNT + DATA_ACCOUNT, + DATA_GAS_RATES ) +from .api_client import (OctopusEnergyApiClient) + _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=1) -def create_reading_coordinator(hass, client, is_electricity, identifier, serial_number): +def create_reading_coordinator(hass, client: OctopusEnergyApiClient, is_electricity: bool, identifier: str, serial_number: str): """Create reading coordinator""" async def async_update_data(): @@ -75,7 +86,7 @@ async def async_update_data(): coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="rates", + 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 @@ -86,6 +97,61 @@ async def async_update_data(): return coordinator +def create_current_consumption_coordinator(hass, client: OctopusEnergyApiClient, device_id: str, is_electricity: bool): + """Create current consumption coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + previous_current_consumption_date_key = f'{device_id}_previous_current_consumption_date' + last_date = None + if previous_current_consumption_date_key in hass.data[DOMAIN]: + last_date = hass.data[DOMAIN][previous_current_consumption_date_key] + elif is_electricity == False: + last_date = (now() - timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) + + data = await async_get_live_consumption(client, device_id, utcnow(), last_date) + if data is not None: + hass.data[DOMAIN][previous_current_consumption_date_key] = data["startAt"] + + return data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"current_consumption_{device_id}", + update_method=async_update_data, + update_interval=timedelta(minutes=1), + ) + + return coordinator + +def create_gas_rate_coordinator(hass, client: OctopusEnergyApiClient, tariff_code: str): + """Create gas rate coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + current = utcnow() + + rate_key = f'{DATA_GAS_RATES}_{tariff_code}' + if (rate_key not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][rate_key]) == 0): + period_from = as_utc(parse_datetime(current.strftime("%Y-%m-%dT00:00:00Z"))) + period_to = as_utc(parse_datetime((current + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00Z"))) + + hass.data[DOMAIN][rate_key] = await client.async_get_gas_rates(tariff_code, period_from, period_to) + await async_check_valid_tariff(hass, client, tariff_code, False) + + return hass.data[DOMAIN][rate_key] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"gas_rates_{tariff_code}", + update_method=async_update_data, + update_interval=timedelta(minutes=1), + ) + + return coordinator + async def async_setup_entry(hass, entry, async_add_entities): """Setup sensors based on our entry""" @@ -115,19 +181,30 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): now = utcnow() if len(account_info["electricity_meter_points"]) > 0: + 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 != None: for meter in point["meters"]: _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') - coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"]) - entities.append(OctopusEnergyPreviousAccumulativeElectricityReading(coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) - entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(coordinator, client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) - entities.append(OctopusEnergyElectricityCurrentRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) - entities.append(OctopusEnergyElectricityPreviousRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) - entities.append(OctopusEnergyElectricityNextRate(rate_coordinator, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) - entities.append(OctopusEnergyElectricityCurrentStandingCharge(client, electricity_tariff_code, point["mpan"], meter["serial_number"], meter["is_export"], meter["is_smart_meter"])) + entities.append(OctopusEnergyElectricityCurrentRate(rate_coordinator, meter, point, electricity_price_cap)) + entities.append(OctopusEnergyElectricityPreviousRate(rate_coordinator, meter, point)) + entities.append(OctopusEnergyElectricityNextRate(rate_coordinator, meter, point)) + entities.append(OctopusEnergyElectricityCurrentStandingCharge(client, electricity_tariff_code, meter, point)) + + if meter["is_smart_meter"] == True: + coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(coordinator, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(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: + consumption_coordinator = create_current_consumption_coordinator(hass, client, meter["device_id"], True) + entities.append(OctopusEnergyCurrentElectricityConsumption(consumption_coordinator, meter, point)) + entities.append(OctopusEnergyCurrentElectricityDemand(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"]}') @@ -136,18 +213,34 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): _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] + 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 != None: for meter in point["meters"]: _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') - coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"]) - entities.append(OctopusEnergyPreviousAccumulativeGasReading(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) - entities.append(OctopusEnergyPreviousAccumulativeGasReadingKwh(coordinator, point["mprn"], meter["serial_number"], meter["consumption_units"])) - entities.append(OctopusEnergyPreviousAccumulativeGasCost(coordinator, client, gas_tariff_code, point["mprn"], meter["serial_number"], meter["consumption_units"])) - entities.append(OctopusEnergyGasCurrentRate(client, gas_tariff_code, point["mprn"], meter["serial_number"])) - entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, point["mprn"], meter["serial_number"])) + rate_coordinator = create_gas_rate_coordinator(hass, client, gas_tariff_code) + entities.append(OctopusEnergyGasCurrentRate(rate_coordinator, gas_tariff_code, meter, point, gas_price_cap)) + entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, meter, point)) + + if meter["is_smart_meter"] == True: + previous_consumption_coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"]) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(previous_consumption_coordinator, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumptionKwh(previous_consumption_coordinator, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasCost(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: + consumption_coordinator = create_current_consumption_coordinator(hass, client, meter["device_id"], False) + entities.append(OctopusEnergyCurrentGasConsumption(consumption_coordinator, 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"]}') @@ -156,1162 +249,3 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): _LOGGER.info('No gas meters available') async_add_entities(entities, True) - -class OctopusEnergyElectricitySensor(SensorEntity, RestoreEntity): - def __init__(self, mpan, serial_number, is_export, is_smart_meter): - """Init sensor""" - self._mpan = mpan - self._serial_number = serial_number - self._is_export = is_export - self._is_smart_meter = is_smart_meter - - 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": "Electricity Meter", - } - -class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): - """Sensor for displaying the current rate.""" - - def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - # Pass coordinator to base class - super().__init__(coordinator) - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - self._state = None - - @property - def unique_id(self): - """The id of the sensor.""" - return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_current_rate" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: - _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 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"] - }, rate)) - self._attributes = { - "rate": current_rate, - "is_export": self._is_export, - "is_smart_meter": self._is_smart_meter, - "rates": ratesAttributes - } - - self._state = current_rate["value_inc_vat"] / 100 - 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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor): - """Sensor for displaying the previous rate.""" - - def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - # Pass coordinator to base class - super().__init__(coordinator) - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - self._state = None - - @property - def unique_id(self): - """The id of the sensor.""" - return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_previous_rate" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: - _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 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 = {} - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor): - """Sensor for displaying the next rate.""" - - def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - # Pass coordinator to base class - super().__init__(coordinator) - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - self._state = None - - @property - def unique_id(self): - """The id of the sensor.""" - return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}_next_rate" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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 (now.minute % 30) == 0 or self._state == None: - _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 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 = {} - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyElectricityCurrentStandingCharge(OctopusEnergyElectricitySensor): - """Sensor for displaying the current standing charge.""" - - def __init__(self, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - 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}_current_standing_charge'; - - @property - def name(self): - """Name of the sensor.""" - return f'Octopus Energy Electricity {self._serial_number} {self._mpan} 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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyPreviousAccumulativeElectricityReading(CoordinatorEntity, OctopusEnergyElectricitySensor): - """Sensor for displaying the previous days accumulative electricity reading.""" - - def __init__(self, coordinator, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - super().__init__(coordinator) - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - 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}_previous_accumulative_consumption" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor): - """Sensor for displaying the previous days accumulative electricity cost.""" - - def __init__(self, coordinator, client, tariff_code, mpan, serial_number, is_export, is_smart_meter): - """Init sensor.""" - super().__init__(coordinator) - OctopusEnergyElectricitySensor.__init__(self, mpan, serial_number, is_export, is_smart_meter) - - 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}_previous_accumulative_cost" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Electricity {self._serial_number} {self._mpan} 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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): - def __init__(self, mprn, serial_number): - """Init sensor""" - self._mprn = mprn - self._serial_number = 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", - } - -class OctopusEnergyGasCurrentRate(OctopusEnergyGasSensor): - """Sensor for displaying the current rate.""" - - def __init__(self, client, tariff_code, mprn, serial_number): - """Init sensor.""" - OctopusEnergyGasSensor.__init__(self, mprn, serial_number) - - 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_rate'; - - @property - def name(self): - """Name of the sensor.""" - return f'Octopus Energy 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""" - 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 OctopusEnergyGasCurrentRate') - - 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"))) - - rates = await self._client.async_get_gas_rates(self._tariff_code, period_from, period_to) - - current_rate = None - if rates != None: - for period in rates: - if utc_now >= period["valid_from"] and utc_now <= period["valid_to"]: - current_rate = period - break - - if current_rate != None: - self._latest_date = period_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"] = period_from - current_rate["valid_to"] = period_to - self._attributes = current_rate - else: - self._state = None - self._attributes = {} - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyGasCurrentStandingCharge(OctopusEnergyGasSensor): - """Sensor for displaying the current standing charge.""" - - def __init__(self, client, tariff_code, mprn, serial_number): - """Init sensor.""" - OctopusEnergyGasSensor.__init__(self, mprn, serial_number) - - 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'Octopus Energy 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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyPreviousAccumulativeGasReading(CoordinatorEntity, OctopusEnergyGasSensor): - """Sensor for displaying the previous days accumulative gas reading.""" - - def __init__(self, coordinator, mprn, serial_number, native_consumption_units): - """Init sensor.""" - super().__init__(coordinator) - OctopusEnergyGasSensor.__init__(self, mprn, serial_number) - - self._native_consumption_units = native_consumption_units - 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}_previous_accumulative_consumption" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy 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 - ) - - 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"] - } - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyPreviousAccumulativeGasReadingKwh(CoordinatorEntity, OctopusEnergyGasSensor): - """Sensor for displaying the previous days accumulative gas reading in kwh.""" - - def __init__(self, coordinator, mprn, serial_number, native_consumption_units): - """Init sensor.""" - super().__init__(coordinator) - OctopusEnergyGasSensor.__init__(self, mprn, serial_number) - - self._native_consumption_units = native_consumption_units - 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}_previous_accumulative_consumption_kwh" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy Gas {self._serial_number} {self._mprn} Previous Accumulative Consumption (kWh)" - - @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: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 - ) - - 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"] - } - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): - """Sensor for displaying the previous days accumulative gas cost.""" - - def __init__(self, coordinator, client, tariff_code, mprn, serial_number, native_consumption_units): - """Init sensor.""" - super().__init__(coordinator) - OctopusEnergyGasSensor.__init__(self, mprn, serial_number) - - self._client = client - self._tariff_code = tariff_code - self._native_consumption_units = native_consumption_units - - 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}_previous_accumulative_cost" - - @property - def name(self): - """Name of the sensor.""" - return f"Octopus Energy 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 - ) - - 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"] - } - - 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 - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') - -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: - self._state = state.state - self._attributes = {} - for x in state.attributes.keys(): - self._attributes[x] = state.attributes[x] - - if (self._state is None): - self._state = 0 - - _LOGGER.debug(f'Restored state: {self._state}') \ 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/translations/en.json b/custom_components/octopus_energy/translations/en.json index f0538d05..19b78df9 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -6,7 +6,11 @@ "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)" + "Account Id": "Your account Id (e.g. A-AAAA1111)", + "supports_live_consumption": "I have a Home Mini", + "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": { @@ -40,7 +44,13 @@ "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" + "Api key": "Api key", + "supports_live_consumption": "I have a Home Mini", + "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": { @@ -64,5 +74,19 @@ "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." + } } } \ 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..328fd353 --- /dev/null +++ b/custom_components/octopus_energy/utils/__init__.py @@ -0,0 +1,125 @@ +from datetime import 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: + 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 { + "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, 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"] != 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_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 \ 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 From aa5420f57dbb17d2ef2f2cb702976fbe22ac3cee Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:49:43 +0200 Subject: [PATCH 061/158] Adaptive lighting 1.11.0 --- .../adaptive_lighting/__init__.py | 3 + .../adaptive_lighting/_docs_helpers.py | 116 ++ custom_components/adaptive_lighting/const.py | 195 +++- .../adaptive_lighting/manifest.json | 12 +- .../adaptive_lighting/services.yaml | 245 +++- .../adaptive_lighting/strings.json | 57 +- custom_components/adaptive_lighting/switch.py | 1015 ++++++++++++----- .../adaptive_lighting/translations/cs.json | 57 + .../adaptive_lighting/translations/en.json | 57 +- .../adaptive_lighting/translations/pl.json | 2 +- 10 files changed, 1421 insertions(+), 338 deletions(-) create mode 100644 custom_components/adaptive_lighting/_docs_helpers.py create mode 100644 custom_components/adaptive_lighting/translations/cs.json diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 33881c75..dc928a6b 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -6,6 +6,7 @@ from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service import voluptuous as vol from .const import ( @@ -37,6 +38,8 @@ def _all_unique_names(value): async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" + # This will reload any changes the user made to any YAML configurations. + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if DOMAIN in config: for entry in config[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/const.py b/custom_components/adaptive_lighting/const.py index 62c71fa2..64620927 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,50 +1,180 @@ """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. ⏲️" +) SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" @@ -53,16 +183,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 +226,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 +265,12 @@ 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 + ), ] @@ -159,3 +319,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/manifest.json b/custom_components/adaptive_lighting/manifest.json index d6580942..2dec4c83 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"], "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.11.0" } 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..54af5e11 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,35 @@ "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. ⏲️" } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ac1acc96..98747a17 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 @@ -21,10 +21,8 @@ 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, @@ -41,7 +39,6 @@ SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - VALID_TRANSITION, is_on, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -74,7 +71,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,8 +85,10 @@ 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 .const import ( @@ -99,7 +98,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, @@ -126,16 +128,23 @@ 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, ) @@ -160,10 +169,8 @@ 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, } @@ -176,21 +183,58 @@ } # 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_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) +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 + + 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,12 +242,16 @@ 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) @@ -212,7 +260,7 @@ 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 f":{_DOMAIN_SHORT}:" in context.id def _split_service_data(service_data, adapt_brightness, adapt_color): @@ -237,57 +285,131 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): 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 + + +def find_switch_for_lights( + hass: HomeAssistant, + lights: list[str], + is_on: bool = False, +) -> 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 ValueError( + 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 ValueError( + 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, service_call) + 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, + ) + _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 +424,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}, @@ -319,10 +442,15 @@ async def async_setup_entry( 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) + 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,6 +460,9 @@ 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 @@ -342,42 +473,118 @@ async def async_setup_entry( 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 # pylint: disable=protected-access + 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], + force=True, + 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 # pylint: disable=protected-access + 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 +625,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 } @@ -464,6 +671,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 +714,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 +773,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 +789,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 +797,94 @@ def __init__( self.adapt_brightness_switch = adapt_brightness_switch data = validate(config_entry) + self._name = data[CONF_NAME] + self._interval = data[CONF_INTERVAL] self._lights = 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 = [] + _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._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._expand_light_groups() # updates manual control timers _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -589,6 +896,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 +914,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, data, ) @@ -674,6 +959,9 @@ async def async_will_remove_from_hass(self): def _expand_light_groups(self) -> None: all_lights = _expand_light_groups(self.hass, self._lights) self.turn_on_off_listener.lights.update(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: @@ -715,14 +1003,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 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 @@ -790,9 +1088,6 @@ async def _adapt_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) - if transition is None: transition = self._transition if adapt_brightness is None: @@ -802,14 +1097,18 @@ 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 - # 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 + if "transition" in features and transition > 0: + 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,20 +1135,19 @@ 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, + + # See #80. Doesn't check if transitions differ but it does the job. + last_service_data = self.turn_on_off_listener.last_service_data + if not force and last_service_data.get(light) == service_data: + _LOGGER.debug( + "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", + self._name, light, - adapt_brightness, - adapt_color, - context, + context.id, ) - ): return - self.turn_on_off_listener.last_service_data[light] = service_data + else: + self.turn_on_off_listener.last_service_data[light] = service_data async def turn_on(service_data): _LOGGER.debug( @@ -895,17 +1193,42 @@ async def _update_attrs_and_maybe_adapt_lights( context.id, ) 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: + + 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 +1237,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, + adapt_brightness, + adapt_color, + context, ) - ): - _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", - self._name, - light, - context.id, - ) - 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, force=force, context=context) async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): @@ -1028,12 +1370,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 +1436,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 +1586,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 +1614,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, @@ -1289,16 +1645,17 @@ 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]] = {} - # When a state is different `max_cnt_significant_changes` times in a row, - # mark it as manually_controlled. - self.max_cnt_significant_changes = 2 + # 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.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -1307,14 +1664,106 @@ def __init__(self, hass: HomeAssistant): 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 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 async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -1369,11 +1818,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 +1842,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 +1855,33 @@ 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] + _LOGGER.debug( + "Last transition: %s", + self.last_service_data[entity_id].get(ATTR_TRANSITION), + ) + 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 +1909,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 +1936,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 +2084,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/en.json b/custom_components/adaptive_lighting/translations/en.json index be556ec0..38fb7b0e 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,35 @@ "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. ⏲️" } } }, 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)" From 99fc9b4546f9392c4d01788042014747fcff69b5 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 11 Apr 2023 22:53:50 +0200 Subject: [PATCH 062/158] Mushroom 2.6.3 --- www/mushroom.js | 1448 +++++++++++++++++++++++++++-------------------- 1 file changed, 822 insertions(+), 626 deletions(-) diff --git a/www/mushroom.js b/www/mushroom.js index 0af5df98..36f0c1da 100644 --- a/www/mushroom.js +++ b/www/mushroom.js @@ -1,81 +1,67 @@ -var t=function(e,i){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])},t(e,i)}; -/*! ***************************************************************************** -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 e(e,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=e}t(e,i),e.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(t){for(var e,i=1,n=arguments.length;i=0;l--)(o=t[l])&&(a=(r<3?o(a):r>3?o(e,i,a):o(e,i))||a);return r>3&&a&&Object.defineProperty(e,i,a),a}function o(t){var e="function"==typeof Symbol&&Symbol.iterator,i=e&&t[e],n=0;if(i)return i.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&n>=t.length&&(t=void 0),{value:t&&t[n++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")} +var t="https://github.com/piitaya/lovelace-mushroom",e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i])},e(t,i)};function i(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function o(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(o.prototype=i.prototype,new o)}var o=function(){return o=Object.assign||function(t){for(var e,i=1,o=arguments.length;i=0;l--)(n=t[l])&&(a=(r<3?n(a):r>3?n(e,i,a):n(e,i))||a);return r>3&&a&&Object.defineProperty(e,i,a),a}function r(t){var e="function"==typeof Symbol&&Symbol.iterator,i=e&&t[e],o=0;if(i)return i.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")} /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */const r=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,a=Symbol(),l=new Map;class s{constructor(t,e){if(this._$cssResult$=!0,e!==a)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){let t=l.get(this.cssText);return r&&void 0===t&&(l.set(this.cssText,t=new CSSStyleSheet),t.replaceSync(this.cssText)),t}toString(){return this.cssText}}const c=t=>new s("string"==typeof t?t:t+"",a),d=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1]),t[0]);return new s(i,a)},u=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return c(e)})(t):t + */const a=window,l=a.ShadowRoot&&(void 0===a.ShadyCSS||a.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),c=new WeakMap;let d=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==s)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(l&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=c.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&c.set(e,t))}return t}toString(){return this.cssText}};const u=t=>new d("string"==typeof t?t:t+"",void 0,s),h=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,o)=>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[o+1]),t[0]);return new d(i,t,s)},m=l?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return u(e)})(t):t /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */;var h;const m=window.trustedTypes,p=m?m.emptyScript:"",f=window.reactiveElementPolyfillSupport,g={toAttribute(t,e){switch(e){case Boolean:t=t?p: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}},_=(t,e)=>e!==t&&(e==e||t==t),v={attribute:!0,type:String,converter:g,reflect:!1,hasChanged:_};class b 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 n=this._$Eh(i,e);void 0!==n&&(this._$Eu.set(n,i),t.push(n))})),t}static createProperty(t,e=v){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const o=this[t];this[e]=n,this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||v}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(u(t))}else void 0!==t&&e.push(u(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)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),n=window.litNonce;void 0!==n&&i.setAttribute("nonce",n),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=v){var n,o;const r=this.constructor._$Eh(t,i);if(void 0!==r&&!0===i.reflect){const a=(null!==(o=null===(n=i.converter)||void 0===n?void 0:n.toAttribute)&&void 0!==o?o:g.toAttribute)(e,i.type);this._$Ei=t,null==a?this.removeAttribute(r):this.setAttribute(r,a),this._$Ei=null}}_$AK(t,e){var i,n,o;const r=this.constructor,a=r._$Eu.get(t);if(void 0!==a&&this._$Ei!==a){const t=r.getPropertyOptions(a),l=t.converter,s=null!==(o=null!==(n=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==n?n:"function"==typeof l?l:null)&&void 0!==o?o:g.fromAttribute;this._$Ei=a,this[a]=s(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||_)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$Ep=this._$E_())}async _$E_(){this.isUpdatePending=!0;try{await this._$Ep}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,e)=>this[e]=t)),this._$Et=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$Eg)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$ES(e,this[e],t))),this._$EC=void 0),this._$EU()}updated(t){}firstUpdated(t){}} + */;var p;const f=window,g=f.trustedTypes,_=g?g.emptyScript:"",v=f.reactiveElementPolyfillSupport,b={toAttribute(t,e){switch(e){case Boolean:t=t?_: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}},y=(t,e)=>e!==t&&(e==e||t==t),x={attribute:!0,type:String,converter:b,reflect:!1,hasChanged:y};let w=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,i)=>{const o=this._$Ep(i,e);void 0!==o&&(this._$Ev.set(o,i),t.push(o))})),t}static createProperty(t,e=x){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,o=this.getPropertyDescriptor(t,i,e);void 0!==o&&Object.defineProperty(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(o){const n=this[t];this[e]=o,this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||x}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(m(t))}else void 0!==t&&e.push(m(t));return e}static _$Ep(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._$E_=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$ES)||void 0===e||e.splice(this._$ES.indexOf(t)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Ei.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,e)=>{l?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),o=a.litNonce;void 0!==o&&i.setAttribute("nonce",o),i.textContent=e.cssText,t.appendChild(i)}))})(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$EO(t,e,i=x){var o;const n=this.constructor._$Ep(t,i);if(void 0!==n&&!0===i.reflect){const r=(void 0!==(null===(o=i.converter)||void 0===o?void 0:o.toAttribute)?i.converter:b).toAttribute(e,i.type);this._$El=t,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$El=null}}_$AK(t,e){var i;const o=this.constructor,n=o._$Ev.get(t);if(void 0!==n&&this._$El!==n){const t=o.getPropertyOptions(n),r="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(i=t.converter)||void 0===i?void 0:i.fromAttribute)?t.converter:b;this._$El=n,this[n]=r.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,i){let o=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||y)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):o=!1),!this.isUpdatePending&&o&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((t,e)=>this[e]=t)),this._$Ei=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$ES)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$EO(e,this[e],t))),this._$EC=void 0),this._$Ek()}updated(t){}firstUpdated(t){}}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -var y;b.finalized=!0,b.elementProperties=new Map,b.elementStyles=[],b.shadowRootOptions={mode:"open"},null==f||f({ReactiveElement:b}),(null!==(h=globalThis.reactiveElementVersions)&&void 0!==h?h:globalThis.reactiveElementVersions=[]).push("1.3.0");const x=globalThis.trustedTypes,w=x?x.createPolicy("lit-html",{createHTML:t=>t}):void 0,k=`lit$${(Math.random()+"").slice(9)}$`,C="?"+k,$=`<${C}>`,E=document,A=(t="")=>E.createComment(t),S=t=>null===t||"object"!=typeof t&&"function"!=typeof t,I=Array.isArray,T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,z=/-->/g,O=/>/g,M=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,L=/'/g,D=/"/g,j=/^(?:script|style|textarea|title)$/i,P=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),N=P(1),V=P(2),R=Symbol.for("lit-noChange"),F=Symbol.for("lit-nothing"),B=new WeakMap,U=E.createTreeWalker(E,129,null,!1),H=(t,e)=>{const i=t.length-1,n=[];let o,r=2===e?"":"",a=T;for(let e=0;e"===s[0]?(a=null!=o?o:T,c=-1):void 0===s[1]?c=-2:(c=a.lastIndex-s[2].length,l=s[1],a=void 0===s[3]?M:'"'===s[3]?D:L):a===D||a===L?a=M:a===z||a===O?a=T:(a=M,o=void 0);const u=a===M&&t[e+1].startsWith("/>")?" ":"";r+=a===T?i+$:c>=0?(n.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+k+u):i+k+(-2===c?(n.push(void 0),e):u)}const l=r+(t[i]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==w?w.createHTML(l):l,n]};class Y{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let o=0,r=0;const a=t.length-1,l=this.parts,[s,c]=H(t,e);if(this.el=Y.createElement(s,i),U.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(n=U.nextNode())&&l.length0){n.textContent=x?x.emptyScript:"";for(let i=0;i{var e;return I(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])})(t)?this.S(t):this.$(t)}A(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}k(t){this._$AH!==t&&(this._$AR(),this._$AH=this.A(t))}$(t){this._$AH!==F&&S(this._$AH)?this._$AA.nextSibling.data=t:this.k(E.createTextNode(t)),this._$AH=t}T(t){var e;const{values:i,_$litType$:n}=t,o="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=Y.createElement(n.h,this.options)),n);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===o)this._$AH.m(i);else{const t=new W(o,this),e=t.p(this.options);t.m(i),this.k(e),this._$AH=t}}_$AC(t){let e=B.get(t.strings);return void 0===e&&B.set(t.strings,e=new Y(t)),e}S(t){I(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,n=0;for(const o of t)n===e.length?e.push(i=new q(this.A(A()),this.A(A()),this,this.options)):i=e[n],i._$AI(o),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=F}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,n){const o=this.strings;let r=!1;if(void 0===o)t=X(this,t,e,0),r=!S(t)||t!==this._$AH&&t!==R,r&&(this._$AH=t);else{const n=t;let a,l;for(t=o[0],a=0;at}):void 0,A=`lit$${(Math.random()+"").slice(9)}$`,S="?"+A,I=`<${S}>`,T=document,z=(t="")=>T.createComment(t),O=t=>null===t||"object"!=typeof t&&"function"!=typeof t,M=Array.isArray,D=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,L=/-->/g,j=/>/g,P=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),N=/'/g,R=/"/g,V=/^(?:script|style|textarea|title)$/i,F=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),B=F(1),U=F(2),H=Symbol.for("lit-noChange"),Y=Symbol.for("lit-nothing"),X=new WeakMap,W=T.createTreeWalker(T,129,null,!1),q=(t,e)=>{const i=t.length-1,o=[];let n,r=2===e?"":"",a=D;for(let e=0;e"===s[0]?(a=null!=n?n:D,c=-1):void 0===s[1]?c=-2:(c=a.lastIndex-s[2].length,l=s[1],a=void 0===s[3]?P:'"'===s[3]?R:N):a===R||a===N?a=P:a===L||a===j?a=D:(a=P,n=void 0);const u=a===P&&t[e+1].startsWith("/>")?" ":"";r+=a===D?i+I:c>=0?(o.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+A+u):i+A+(-2===c?(o.push(void 0),e):u)}const l=r+(t[i]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==E?E.createHTML(l):l,o]};class K{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let n=0,r=0;const a=t.length-1,l=this.parts,[s,c]=q(t,e);if(this.el=K.createElement(s,i),W.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(o=W.nextNode())&&l.length0){o.textContent=$?$.emptyScript:"";for(let i=0;iM(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!==Y&&O(this._$AH)?this._$AA.nextSibling.data=t:this.T(T.createTextNode(t)),this._$AH=t}$(t){var e;const{values:i,_$litType$:o}=t,n="number"==typeof o?this._$AC(t):(void 0===o.el&&(o.el=K.createElement(o.h,this.options)),o);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===n)this._$AH.p(i);else{const t=new Z(n,this),e=t.v(this.options);t.p(i),this.T(e),this._$AH=t}}_$AC(t){let e=X.get(t.strings);return void 0===e&&X.set(t.strings,e=new K(t)),e}k(t){M(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const n of t)o===e.length?e.push(i=new J(this.O(z()),this.O(z()),this,this.options)):i=e[o],i._$AI(n),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Y}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,o){const n=this.strings;let r=!1;if(void 0===n)t=G(this,t,e,0),r=!O(t)||t!==this._$AH&&t!==H,r&&(this._$AH=t);else{const o=t;let a,l;for(t=n[0],a=0;a{var n,o;const r=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:e;let a=r._$litPart$;if(void 0===a){const t=null!==(o=null==i?void 0:i.renderBefore)&&void 0!==o?o:null;r._$litPart$=a=new q(e.insertBefore(A(),t),t,void 0,null!=i?i:{})}return a._$AI(t),a})(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 R}}ot.finalized=!0,ot._$litElement$=!0,null===(it=globalThis.litElementHydrateSupport)||void 0===it||it.call(globalThis,{LitElement:ot});const rt=globalThis.litElementPolyfillSupport;null==rt||rt({LitElement:ot}),(null!==(nt=globalThis.litElementVersions)&&void 0!==nt?nt:globalThis.litElementVersions=[]).push("3.2.0"); +var at,lt;let st=class extends w{constructor(){super(...arguments),this.renderOptions={host:this},this._$Dt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Dt=((t,e,i)=>{var o,n;const r=null!==(o=null==i?void 0:i.renderBefore)&&void 0!==o?o:e;let a=r._$litPart$;if(void 0===a){const t=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:null;r._$litPart$=a=new J(e.insertBefore(z(),t),t,void 0,null!=i?i:{})}return a._$AI(t),a})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!1)}render(){return H}};st.finalized=!0,st._$litElement$=!0,null===(at=globalThis.litElementHydrateSupport)||void 0===at||at.call(globalThis,{LitElement:st});const ct=globalThis.litElementPolyfillSupport;null==ct||ct({LitElement:st}),(null!==(lt=globalThis.litElementVersions)&&void 0!==lt?lt:globalThis.litElementVersions=[]).push("3.2.0"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:n}=e;return{kind:i,elements:n,finisher(e){window.customElements.define(t,e)}}})(t,e) +const dt=t=>e=>"function"==typeof e?((t,e)=>(customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:o}=e;return{kind:i,elements:o,finisher(e){customElements.define(t,e)}}})(t,e) /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */,lt=(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 st(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):lt(t,e) + */,ut=(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 ht(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):ut(t,e)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */}function ct(t){return st({...t,state:!0})} + */function mt(t){return ht({...t,state:!0})} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */const dt=({finisher:t,descriptor:e})=>(i,n)=>{var o;if(void 0===n){const n=null!==(o=i.originalKey)&&void 0!==o?o:i.key,r=null!=e?{kind:"method",placement:"prototype",key:n,descriptor:e(i.key)}:{...i,key:n};return null!=t&&(r.finisher=function(e){t(e,n)}),r}{const o=i.constructor;void 0!==e&&Object.defineProperty(i,n,e(n)),null==t||t(o,n)}} + */const pt=({finisher:t,descriptor:e})=>(i,o)=>{var n;if(void 0===o){const o=null!==(n=i.originalKey)&&void 0!==n?n:i.key,r=null!=e?{kind:"method",placement:"prototype",key:o,descriptor:e(i.key)}:{...i,key:o};return null!=t&&(r.finisher=function(e){t(e,o)}),r}{const n=i.constructor;void 0!==e&&Object.defineProperty(i,o,e(o)),null==t||t(n,o)}} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */;function ut(t){return dt({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})} + */;function ft(t){return pt({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function ht(t,e){return dt({descriptor:i=>{const n={get(){var e,i;return null!==(i=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;n.get=function(){var i,n;return void 0===this[e]&&(this[e]=null!==(n=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null),this[e]}}return n}})} + */function gt(t,e){return pt({descriptor:i=>{const o={get(){var e,i;return null!==(i=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;o.get=function(){var i,o;return void 0===this[e]&&(this[e]=null!==(o=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==o?o:null),this[e]}}return o}})} /** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */var mt;null===(mt=window.HTMLSlotElement)||void 0===mt||mt.prototype.assignedElements;const pt=["closed","locked","off"];var ft=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function gt(t,e){if(t.length!==e.length)return!1;for(var i=0;inew Intl.DateTimeFormat(t.language,{weekday:"long",month:"long",day:"numeric"})));const vt=(t,e)=>bt(e).format(t),bt=_t((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric"})));var yt,xt;_t((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"numeric",day:"numeric"}))),_t((t=>new Intl.DateTimeFormat(t.language,{day:"numeric",month:"short"}))),_t((t=>new Intl.DateTimeFormat(t.language,{month:"long",year:"numeric"}))),_t((t=>new Intl.DateTimeFormat(t.language,{month:"long"}))),_t((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"}(yt||(yt={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(xt||(xt={}));const wt=_t((t=>{if(t.time_format===xt.language||t.time_format===xt.system){const e=t.time_format===xt.language?t.language:void 0,i=(new Date).toLocaleString(e);return i.includes("AM")||i.includes("PM")}return t.time_format===xt.am_pm})),kt=(t,e)=>Ct(e).format(t),Ct=_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:wt(t)?"numeric":"2-digit",minute:"2-digit",hour12:wt(t)})));_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:wt(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:wt(t)}))),_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit",hour12:wt(t)})));const $t=(t,e)=>Et(e).format(t),Et=_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{hour:"numeric",minute:"2-digit",hour12:wt(t)})));_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{hour:wt(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:wt(t)}))),_t((t=>new Intl.DateTimeFormat("en"!==t.language||wt(t)?t.language:"en-u-hc-h23",{weekday:"long",hour:wt(t)?"numeric":"2-digit",minute:"2-digit",hour12:wt(t)})));const At=(t,e,i,n)=>{n=n||{},i=null==i?{}:i;const o=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return o.detail=i,t.dispatchEvent(o),o},St="ha-main-window"===window.name?window:"ha-main-window"===parent.name?parent:top,It=t=>t.substr(0,t.indexOf(".")),Tt="unavailable",zt="unknown",Ot="off",Mt=[Tt,zt,Ot];function Lt(t){const e=It(t.entity_id),i=t.state;if(["button","input_button","scene"].includes(e))return i!==Tt;if(Mt.includes(i))return!1;switch(e){case"cover":return!["closed","closing"].includes(i);case"device_tracker":case"person":return"not_home"!==i;case"media_player":return"standby"!==i;case"vacuum":return!["idle","docked","paused"].includes(i);case"plant":return"problem"===i;default:return!0}}function Dt(t){return t.state!==Tt}function jt(t){return t.state===Ot}function Pt(t){return t.attributes.entity_picture_local||t.attributes.entity_picture}const Nt=(t,e)=>0!=(t.attributes.supported_features&e),Vt=t=>(t=>Nt(t,4)&&"number"==typeof t.attributes.in_progress)(t)||!!t.attributes.in_progress,Rt=(t,e=2)=>Math.round(t*10**e)/10**e,Ft=t=>!!t.unit_of_measurement||!!t.state_class,Bt=(t,e,i)=>{const n=e?(t=>{switch(t.number_format){case yt.comma_decimal:return["en-US","en"];case yt.decimal_comma:return["de","es","it"];case yt.space_comma:return["fr","sv","cs"];case yt.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)!==yt.none&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(n,Ut(t,i)).format(Number(t))}catch(e){return console.error(e),new Intl.NumberFormat(void 0,Ut(t,i)).format(Number(t))}return"string"==typeof t?t:`${Rt(t,null==i?void 0:i.maximumFractionDigits).toString()}${"currency"===(null==i?void 0:i.style)?` ${i.currency}`:""}`},Ut=(t,e)=>{const i=Object.assign({maximumFractionDigits:2},e);if("string"!=typeof t)return i;if(!e||!e.minimumFractionDigits&&!e.maximumFractionDigits){const e=t.indexOf(".")>-1?t.split(".")[1].length:0;i.minimumFractionDigits=e,i.maximumFractionDigits=e}return i},Ht=(t,e,i,n)=>{var o;const r=void 0!==n?n:e.state;if(r===zt||r===Tt)return t(`state.default.${r}`);if((t=>Ft(t.attributes))(e)){if("monetary"===e.attributes.device_class)try{return Bt(r,i,{style:"currency",currency:e.attributes.unit_of_measurement})}catch(t){}return`${Bt(r,i)}${e.attributes.unit_of_measurement?" "+e.attributes.unit_of_measurement:""}`}const a=(t=>It(t.entity_id))(e);if("input_datetime"===a){if(void 0===n){let t;return e.attributes.has_date&&e.attributes.has_time?(t=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day,e.attributes.hour,e.attributes.minute),kt(t,i)):e.attributes.has_date?(t=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day),vt(t,i)):e.attributes.has_time?(t=new Date,t.setHours(e.attributes.hour,e.attributes.minute),$t(t,i)):e.state}try{const t=n.split(" ");if(2===t.length)return kt(new Date(t.join("T")),i);if(1===t.length){if(n.includes("-"))return vt(new Date(`${n}T00:00`),i);if(n.includes(":")){const t=new Date;return $t(new Date(`${t.toISOString().split("T")[0]}T${n}`),i)}}return n}catch(t){return n}}if("humidifier"===a&&"on"===r&&e.attributes.humidity)return`${e.attributes.humidity} %`;if("counter"===a||"number"===a||"input_number"===a)return Bt(r,i);if("button"===a||"input_button"===a||"scene"===a||"sensor"===a&&"timestamp"===e.attributes.device_class)try{return kt(new Date(r),i)}catch(t){return r}return"update"===a?"on"===r?Vt(e)?Nt(e,4)?t("ui.card.update.installing_with_progress",{progress:e.attributes.in_progress}):t("ui.card.update.installing"):e.attributes.latest_version:e.attributes.skipped_version===e.attributes.latest_version?null!==(o=e.attributes.latest_version)&&void 0!==o?o:t("state.default.unavailable"):t("ui.card.update.up_to_date"):e.attributes.device_class&&t(`component.${a}.state.${e.attributes.device_class}.${r}`)||t(`component.${a}.state._.${r}`)||r};class Yt extends TypeError{constructor(t,e){let i;const{message:n,...o}=t,{path:r}=t;super(0===r.length?n:"At path: "+r.join(".")+" -- "+n),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,o),this.name=this.constructor.name,this.failures=()=>{var n;return null!=(n=i)?n:i=[t,...e()]}}}function Xt(t){return"object"==typeof t&&null!=t}function Wt(t){return"string"==typeof t?JSON.stringify(t):""+t}function qt(t,e,i,n){if(!0===t)return;!1===t?t={}:"string"==typeof t&&(t={message:t});const{path:o,branch:r}=e,{type:a}=i,{refinement:l,message:s="Expected a value of type `"+a+"`"+(l?" with refinement `"+l+"`":"")+", but received: `"+Wt(n)+"`"}=t;return{value:n,type:a,refinement:l,key:o[o.length-1],path:o,branch:r,...t,message:s}}function*Kt(t,e,i,n){(function(t){return Xt(t)&&"function"==typeof t[Symbol.iterator]})(t)||(t=[t]);for(const o of t){const t=qt(o,e,i,n);t&&(yield t)}}function*Gt(t,e,i={}){const{path:n=[],branch:o=[t],coerce:r=!1,mask:a=!1}=i,l={path:n,branch:o};if(r&&(t=e.coercer(t,l),a&&"type"!==e.type&&Xt(e.schema)&&Xt(t)&&!Array.isArray(t)))for(const i in t)void 0===e.schema[i]&&delete t[i];let s=!0;for(const i of e.validator(t,l))s=!1,yield[i,void 0];for(let[i,c,d]of e.entries(t,l)){const e=Gt(c,d,{path:void 0===i?n:[...n,i],branch:void 0===i?o:[...o,c],coerce:r,mask:a});for(const n of e)n[0]?(s=!1,yield[n[0],void 0]):r&&(c=n[1],void 0===i?t=c:t instanceof Map?t.set(i,c):t instanceof Set?t.add(c):Xt(t)&&(t[i]=c))}if(s)for(const i of e.refiner(t,l))s=!1,yield[i,void 0];s&&(yield[void 0,t])}class Zt{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:i,validator:n,refiner:o,coercer:r=(t=>t),entries:a=function*(){}}=t;this.type=e,this.schema=i,this.entries=a,this.coercer=r,this.validator=n?(t,e)=>Kt(n(t,e),e,this,t):()=>[],this.refiner=o?(t,e)=>Kt(o(t,e),e,this,t):()=>[]}assert(t){return Jt(t,this)}create(t){return function(t,e){const i=Qt(t,e,{coerce:!0});if(i[0])throw i[0];return i[1]}(t,this)}is(t){return function(t,e){return!Qt(t,e)[0]}(t,this)}mask(t){return function(t,e){const i=Qt(t,e,{coerce:!0,mask:!0});if(i[0])throw i[0];return i[1]}(t,this)}validate(t,e={}){return Qt(t,this,e)}}function Jt(t,e){const i=Qt(t,e);if(i[0])throw i[0]}function Qt(t,e,i={}){const n=Gt(t,e,i),o=function(t){const{done:e,value:i}=t.next();return e?void 0:i}(n);if(o[0]){const t=new Yt(o[0],(function*(){for(const t of n)t[0]&&(yield t[0])}));return[t,void 0]}return[void 0,o[1]]}function te(...t){const e="type"===t[0].type,i=t.map((t=>t.schema)),n=Object.assign({},...i);return e?he(n):ce(n)}function ee(t,e){return new Zt({type:t,schema:null,validator:e})}function ie(t){return new Zt({type:"dynamic",schema:null,*entries(e,i){const n=t(e,i);yield*n.entries(e,i)},validator:(e,i)=>t(e,i).validator(e,i),coercer:(e,i)=>t(e,i).coercer(e,i),refiner:(e,i)=>t(e,i).refiner(e,i)})}function ne(){return ee("any",(()=>!0))}function oe(t){return new Zt({type:"array",schema:t,*entries(e){if(t&&Array.isArray(e))for(const[i,n]of e.entries())yield[i,n,t]},coercer:t=>Array.isArray(t)?t.slice():t,validator:t=>Array.isArray(t)||"Expected an array value, but received: "+Wt(t)})}function re(){return ee("boolean",(t=>"boolean"==typeof t))}function ae(t){const e={},i=t.map((t=>Wt(t))).join();for(const i of t)e[i]=i;return new Zt({type:"enums",schema:e,validator:e=>t.includes(e)||"Expected one of `"+i+"`, but received: "+Wt(e)})}function le(t){const e=Wt(t),i=typeof t;return new Zt({type:"literal",schema:"string"===i||"number"===i||"boolean"===i?t:null,validator:i=>i===t||"Expected the literal `"+e+"`, but received: "+Wt(i)})}function se(){return ee("number",(t=>"number"==typeof t&&!isNaN(t)||"Expected a number, but received: "+Wt(t)))}function ce(t){const e=t?Object.keys(t):[],i=ee("never",(()=>!1));return new Zt({type:"object",schema:t||null,*entries(n){if(t&&Xt(n)){const o=new Set(Object.keys(n));for(const i of e)o.delete(i),yield[i,n[i],t[i]];for(const t of o)yield[t,n[t],i]}},validator:t=>Xt(t)||"Expected an object, but received: "+Wt(t),coercer:t=>Xt(t)?{...t}:t})}function de(t){return new Zt({...t,validator:(e,i)=>void 0===e||t.validator(e,i),refiner:(e,i)=>void 0===e||t.refiner(e,i)})}function ue(){return ee("string",(t=>"string"==typeof t||"Expected a string, but received: "+Wt(t)))}function he(t){const e=Object.keys(t);return new Zt({type:"type",schema:t,*entries(i){if(Xt(i))for(const n of e)yield[n,i[n],t[n]]},validator:t=>Xt(t)||"Expected an object, but received: "+Wt(t)})}function me(t){const e=t.map((t=>t.type)).join(" | ");return new Zt({type:"union",schema:null,coercer(e,i){const n=t.find((t=>{const[i]=t.validate(e,{coerce:!0});return!i}))||ee("unknown",(()=>!0));return n.coercer(e,i)},validator(i,n){const o=[];for(const e of t){const[...t]=Gt(i,e,n),[r]=t;if(!r[0])return[];for(const[e]of t)e&&o.push(e)}return["Expected the value to satisfy a union of `"+e+"`, but received: "+Wt(i),...o]}})}function pe(t){const e=t.language||"en";return t.translationMetadata.translations[e]&&t.translationMetadata.translations[e].isRTL||!1}const fe=(t,e,i=!1)=>{let n;const o=(...o)=>{const r=i&&!n;clearTimeout(n),n=window.setTimeout((()=>{n=void 0,i||t(...o)}),e),r&&t(...o)};return o.cancel=()=>{clearTimeout(n)},o},ge=(t,e)=>{if(t===e)return!0;if(t&&e&&"object"==typeof t&&"object"==typeof e){if(t.constructor!==e.constructor)return!1;let i,n;if(Array.isArray(t)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(!ge(t[i],e[i]))return!1;return!0}if(t instanceof Map&&e instanceof Map){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;for(i of t.entries())if(!ge(i[1],e.get(i[0])))return!1;return!0}if(t instanceof Set&&e instanceof Set){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;return!0}if(ArrayBuffer.isView(t)&&ArrayBuffer.isView(e)){if(n=t.length,n!==e.length)return!1;for(i=n;0!=i--;)if(t[i]!==e[i])return!1;return!0}if(t.constructor===RegExp)return t.source===e.source&&t.flags===e.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===e.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===e.toString();const o=Object.keys(t);if(n=o.length,n!==Object.keys(e).length)return!1;for(i=n;0!=i--;)if(!Object.prototype.hasOwnProperty.call(e,o[i]))return!1;for(i=n;0!=i--;){const n=o[i];if(!ge(t[n],e[n]))return!1}return!0}return t!=t&&e!=e},_e=()=>new Promise((t=>{var e;e=t,requestAnimationFrame((()=>setTimeout(e,0)))})),ve={auto:1,heat_cool:2,heat:3,cool:4,dry:5,fan_only:6,off:7},be=(t,e)=>ve[t]-ve[e];const ye=t=>{At(window,"haptic",t)},xe=["hs","xy","rgb","rgbw","rgbww"],we=[...xe,"color_temp","brightness"],ke=(t,e,i)=>t.subscribeMessage((t=>e(t)),Object.assign({type:"render_template"},i)) + */var _t;null===(_t=window.HTMLSlotElement)||void 0===_t||_t.prototype.assignedElements;const vt=["closed","locked","off"];var bt=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function yt(t,e){if(t.length!==e.length)return!1;for(var i=0;inew Intl.DateTimeFormat(t.language,{weekday:"long",month:"long",day:"numeric"})));const wt=(t,e)=>kt(e).format(t),kt=xt((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric"})));var Ct,$t;xt((t=>new Intl.DateTimeFormat(t.language,{year:"numeric",month:"numeric",day:"numeric"}))),xt((t=>new Intl.DateTimeFormat(t.language,{day:"numeric",month:"short"}))),xt((t=>new Intl.DateTimeFormat(t.language,{month:"long",year:"numeric"}))),xt((t=>new Intl.DateTimeFormat(t.language,{month:"long"}))),xt((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"}(Ct||(Ct={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}($t||($t={}));const Et=xt((t=>{if(t.time_format===$t.language||t.time_format===$t.system){const e=t.time_format===$t.language?t.language:void 0,i=(new Date).toLocaleString(e);return i.includes("AM")||i.includes("PM")}return t.time_format===$t.am_pm})),At=(t,e)=>St(e).format(t),St=xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:Et(t)?"numeric":"2-digit",minute:"2-digit",hour12:Et(t)})));xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"long",day:"numeric",hour:Et(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:Et(t)}))),xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit",hour12:Et(t)})));const It=(t,e)=>Tt(e).format(t),Tt=xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{hour:"numeric",minute:"2-digit",hour12:Et(t)})));xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{hour:Et(t)?"numeric":"2-digit",minute:"2-digit",second:"2-digit",hour12:Et(t)}))),xt((t=>new Intl.DateTimeFormat("en"!==t.language||Et(t)?t.language:"en-u-hc-h23",{weekday:"long",hour:Et(t)?"numeric":"2-digit",minute:"2-digit",hour12:Et(t)})));const zt=(t,e,i,o)=>{o=o||{},i=null==i?{}:i;const n=new Event(e,{bubbles:void 0===o.bubbles||o.bubbles,cancelable:Boolean(o.cancelable),composed:void 0===o.composed||o.composed});return n.detail=i,t.dispatchEvent(n),n},Ot="ha-main-window"===window.name?window:"ha-main-window"===parent.name?parent:top,Mt=t=>t.substr(0,t.indexOf(".")),Dt="unavailable",Lt="unknown",jt="off",Pt=[Dt,Lt,jt];function Nt(t){const e=Mt(t.entity_id),i=t.state;if(["button","input_button","scene"].includes(e))return i!==Dt;if(Pt.includes(i))return!1;switch(e){case"cover":return!["closed","closing"].includes(i);case"device_tracker":case"person":return"not_home"!==i;case"media_player":return"standby"!==i;case"vacuum":return!["idle","docked","paused"].includes(i);case"plant":return"problem"===i;default:return!0}}function Rt(t){return t.state!==Dt}function Vt(t){return t.state===jt}function Ft(t){return t.attributes.entity_picture_local||t.attributes.entity_picture}const Bt=(t,e)=>Ut(t.attributes,e),Ut=(t,e)=>0!=(t.supported_features&e);xt((t=>new Intl.Collator(t))),xt((t=>new Intl.Collator(t,{sensitivity:"accent"})));const Ht=t=>Ut(t,4)&&"number"==typeof t.in_progress,Yt=t=>(t=>Ht(t.attributes))(t)||!!t.attributes.in_progress,Xt=(t,e,i,o)=>{const[n,r,a]=t.split(".",3);return Number(n)>e||Number(n)===e&&(void 0===o?Number(r)>=i:Number(r)>i)||void 0!==o&&Number(n)===e&&Number(r)===i&&Number(a)>=o},Wt=t=>t<10?`0${t}`:t;const qt={s:1,min:60,h:3600,d:86400},Kt=(t,e)=>function(t){const e=Math.floor(t/3600),i=Math.floor(t%3600/60),o=Math.floor(t%3600%60);return e>0?`${e}:${Wt(i)}:${Wt(o)}`:i>0?`${i}:${Wt(o)}`:o>0?""+o:null}(parseFloat(t)*qt[e])||"0",Gt=(t,e=2)=>Math.round(t*10**e)/10**e,Zt=(t,e,i)=>{const o=e?(t=>{switch(t.number_format){case Ct.comma_decimal:return["en-US","en"];case Ct.decimal_comma:return["de","es","it"];case Ct.space_comma:return["fr","sv","cs"];case Ct.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)!==Ct.none&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(o,Qt(t,i)).format(Number(t))}catch(e){return console.error(e),new Intl.NumberFormat(void 0,Qt(t,i)).format(Number(t))}return"string"==typeof t?t:`${Gt(t,null==i?void 0:i.maximumFractionDigits).toString()}${"currency"===(null==i?void 0:i.style)?` ${i.currency}`:""}`},Jt=(t,e)=>{var i;const o=null==e?void 0:e.display_precision;return null!=o?{maximumFractionDigits:o,minimumFractionDigits:o}:Number.isInteger(Number(null===(i=t.attributes)||void 0===i?void 0:i.step))&&Number.isInteger(Number(t.state))?{maximumFractionDigits:0}:void 0},Qt=(t,e)=>{const i=Object.assign({maximumFractionDigits:2},e);if("string"!=typeof t)return i;if(!e||void 0===e.minimumFractionDigits&&void 0===e.maximumFractionDigits){const e=t.indexOf(".")>-1?t.split(".")[1].length:0;i.minimumFractionDigits=e,i.maximumFractionDigits=e}return i},te=t=>{switch(t.language){case"cz":case"de":case"fi":case"fr":case"sk":case"sv":return" ";default:return""}},ee=(t,e,i,o,n,r)=>ie(t,i,o,n,e.entity_id,e.attributes,void 0!==r?r:e.state),ie=(t,e,i,o,n,r,a)=>{var l;if(a===Lt||a===Dt)return t(`state.default.${a}`);const s=i[n];if((t=>!!t.unit_of_measurement||!!t.state_class)(r)){if("duration"===r.device_class&&r.unit_of_measurement&&qt[r.unit_of_measurement])try{return Kt(a,r.unit_of_measurement)}catch(t){}if("monetary"===r.device_class)try{return Zt(a,e,{style:"currency",currency:r.unit_of_measurement,minimumFractionDigits:2})}catch(t){}const t=r.unit_of_measurement?"%"===r.unit_of_measurement?te(e)+"%":` ${r.unit_of_measurement}`:"";return`${Zt(a,e,Jt({state:a,attributes:r},s))}${t}`}const c=Mt(n);if("input_datetime"===c){if(void 0===a){let t;return r.has_date&&r.has_time?(t=new Date(r.year,r.month-1,r.day,r.hour,r.minute),At(t,e)):r.has_date?(t=new Date(r.year,r.month-1,r.day),wt(t,e)):r.has_time?(t=new Date,t.setHours(r.hour,r.minute),It(t,e)):a}try{const t=a.split(" ");if(2===t.length)return At(new Date(t.join("T")),e);if(1===t.length){if(a.includes("-"))return wt(new Date(`${a}T00:00`),e);if(a.includes(":")){const t=new Date;return It(new Date(`${t.toISOString().split("T")[0]}T${a}`),e)}}return a}catch(t){return a}}if("humidifier"===c&&"on"===a&&r.humidity)return`${r.humidity}${te(e)}%`;if("counter"===c||"number"===c||"input_number"===c)return Zt(a,e,Jt({state:a,attributes:r},s));if("button"===c||"input_button"===c||"scene"===c||"sensor"===c&&"timestamp"===r.device_class)try{return At(new Date(a),e)}catch(t){return a}return"update"===c?"on"===a?(t=>Ht(t)||!!t.in_progress)(r)?Ut(r,4)&&"number"==typeof r.in_progress?t("ui.card.update.installing_with_progress",{progress:r.in_progress}):t("ui.card.update.installing"):r.latest_version:r.skipped_version===r.latest_version?null!==(l=r.latest_version)&&void 0!==l?l:t("state.default.unavailable"):t("ui.card.update.up_to_date"):(null==s?void 0:s.translation_key)&&t(`component.${s.platform}.entity.${c}.${s.translation_key}.state.${a}`)||r.device_class&&t(Xt(o,2023,4)?`component.${c}.entity_component.${r.device_class}.state.${a}`:`component.${c}.state.${r.device_class}.${a}`)||t(Xt(o,2023,4)?`component.${c}.entity_component._.state.${a}`:`component.${c}.state._.${a}`)||a};class oe extends TypeError{constructor(t,e){let i;const{message:o,...n}=t,{path:r}=t;super(0===r.length?o:"At path: "+r.join(".")+" -- "+o),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,n),this.name=this.constructor.name,this.failures=()=>{var o;return null!=(o=i)?o:i=[t,...e()]}}}function ne(t){return"object"==typeof t&&null!=t}function re(t){return"string"==typeof t?JSON.stringify(t):""+t}function ae(t,e,i,o){if(!0===t)return;!1===t?t={}:"string"==typeof t&&(t={message:t});const{path:n,branch:r}=e,{type:a}=i,{refinement:l,message:s="Expected a value of type `"+a+"`"+(l?" with refinement `"+l+"`":"")+", but received: `"+re(o)+"`"}=t;return{value:o,type:a,refinement:l,key:n[n.length-1],path:n,branch:r,...t,message:s}}function*le(t,e,i,o){(function(t){return ne(t)&&"function"==typeof t[Symbol.iterator]})(t)||(t=[t]);for(const n of t){const t=ae(n,e,i,o);t&&(yield t)}}function*se(t,e,i){void 0===i&&(i={});const{path:o=[],branch:n=[t],coerce:r=!1,mask:a=!1}=i,l={path:o,branch:n};if(r&&(t=e.coercer(t,l),a&&"type"!==e.type&&ne(e.schema)&&ne(t)&&!Array.isArray(t)))for(const i in t)void 0===e.schema[i]&&delete t[i];let s=!0;for(const i of e.validator(t,l))s=!1,yield[i,void 0];for(let[i,c,d]of e.entries(t,l)){const e=se(c,d,{path:void 0===i?o:[...o,i],branch:void 0===i?n:[...n,c],coerce:r,mask:a});for(const o of e)o[0]?(s=!1,yield[o[0],void 0]):r&&(c=o[1],void 0===i?t=c:t instanceof Map?t.set(i,c):t instanceof Set?t.add(c):ne(t)&&(t[i]=c))}if(s)for(const i of e.refiner(t,l))s=!1,yield[i,void 0];s&&(yield[void 0,t])}class ce{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:i,validator:o,refiner:n,coercer:r=(t=>t),entries:a=function*(){}}=t;this.type=e,this.schema=i,this.entries=a,this.coercer=r,this.validator=o?(t,e)=>le(o(t,e),e,this,t):()=>[],this.refiner=n?(t,e)=>le(n(t,e),e,this,t):()=>[]}assert(t){return de(t,this)}create(t){return function(t,e){const i=ue(t,e,{coerce:!0});if(i[0])throw i[0];return i[1]}(t,this)}is(t){return function(t,e){return!ue(t,e)[0]}(t,this)}mask(t){return function(t,e){const i=ue(t,e,{coerce:!0,mask:!0});if(i[0])throw i[0];return i[1]}(t,this)}validate(t,e){return void 0===e&&(e={}),ue(t,this,e)}}function de(t,e){const i=ue(t,e);if(i[0])throw i[0]}function ue(t,e,i){void 0===i&&(i={});const o=se(t,e,i),n=function(t){const{done:e,value:i}=t.next();return e?void 0:i}(o);if(n[0]){const t=new oe(n[0],(function*(){for(const t of o)t[0]&&(yield t[0])}));return[t,void 0]}return[void 0,n[1]]}function he(){for(var t=arguments.length,e=new Array(t),i=0;it.schema)),r=Object.assign({},...n);return o?Ce(r):xe(r)}function me(t,e){return new ce({type:t,schema:null,validator:e})}function pe(t){return new ce({type:"dynamic",schema:null,*entries(e,i){const o=t(e,i);yield*o.entries(e,i)},validator:(e,i)=>t(e,i).validator(e,i),coercer:(e,i)=>t(e,i).coercer(e,i),refiner:(e,i)=>t(e,i).refiner(e,i)})}function fe(){return me("any",(()=>!0))}function ge(t){return new ce({type:"array",schema:t,*entries(e){if(t&&Array.isArray(e))for(const[i,o]of e.entries())yield[i,o,t]},coercer:t=>Array.isArray(t)?t.slice():t,validator:t=>Array.isArray(t)||"Expected an array value, but received: "+re(t)})}function _e(){return me("boolean",(t=>"boolean"==typeof t))}function ve(t){const e={},i=t.map((t=>re(t))).join();for(const i of t)e[i]=i;return new ce({type:"enums",schema:e,validator:e=>t.includes(e)||"Expected one of `"+i+"`, but received: "+re(e)})}function be(t){const e=re(t),i=typeof t;return new ce({type:"literal",schema:"string"===i||"number"===i||"boolean"===i?t:null,validator:i=>i===t||"Expected the literal `"+e+"`, but received: "+re(i)})}function ye(){return me("number",(t=>"number"==typeof t&&!isNaN(t)||"Expected a number, but received: "+re(t)))}function xe(t){const e=t?Object.keys(t):[],i=me("never",(()=>!1));return new ce({type:"object",schema:t||null,*entries(o){if(t&&ne(o)){const n=new Set(Object.keys(o));for(const i of e)n.delete(i),yield[i,o[i],t[i]];for(const t of n)yield[t,o[t],i]}},validator:t=>ne(t)||"Expected an object, but received: "+re(t),coercer:t=>ne(t)?{...t}:t})}function we(t){return new ce({...t,validator:(e,i)=>void 0===e||t.validator(e,i),refiner:(e,i)=>void 0===e||t.refiner(e,i)})}function ke(){return me("string",(t=>"string"==typeof t||"Expected a string, but received: "+re(t)))}function Ce(t){const e=Object.keys(t);return new ce({type:"type",schema:t,*entries(i){if(ne(i))for(const o of e)yield[o,i[o],t[o]]},validator:t=>ne(t)||"Expected an object, but received: "+re(t)})}function $e(t){const e=t.map((t=>t.type)).join(" | ");return new ce({type:"union",schema:null,coercer(e,i){const o=t.find((t=>{const[i]=t.validate(e,{coerce:!0});return!i}))||me("unknown",(()=>!0));return o.coercer(e,i)},validator(i,o){const n=[];for(const e of t){const[...t]=se(i,e,o),[r]=t;if(!r[0])return[];for(const[e]of t)e&&n.push(e)}return["Expected the value to satisfy a union of `"+e+"`, but received: "+re(i),...n]}})}function Ee(t){const e=t.language||"en";return t.translationMetadata.translations[e]&&t.translationMetadata.translations[e].isRTL||!1}const Ae=(t,e,i=!1)=>{let o;const n=(...n)=>{const r=i&&!o;clearTimeout(o),o=window.setTimeout((()=>{o=void 0,i||t(...n)}),e),r&&t(...n)};return n.cancel=()=>{clearTimeout(o)},n},Se=(t,e)=>{if(t===e)return!0;if(t&&e&&"object"==typeof t&&"object"==typeof e){if(t.constructor!==e.constructor)return!1;let i,o;if(Array.isArray(t)){if(o=t.length,o!==e.length)return!1;for(i=o;0!=i--;)if(!Se(t[i],e[i]))return!1;return!0}if(t instanceof Map&&e instanceof Map){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;for(i of t.entries())if(!Se(i[1],e.get(i[0])))return!1;return!0}if(t instanceof Set&&e instanceof Set){if(t.size!==e.size)return!1;for(i of t.entries())if(!e.has(i[0]))return!1;return!0}if(ArrayBuffer.isView(t)&&ArrayBuffer.isView(e)){if(o=t.length,o!==e.length)return!1;for(i=o;0!=i--;)if(t[i]!==e[i])return!1;return!0}if(t.constructor===RegExp)return t.source===e.source&&t.flags===e.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===e.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===e.toString();const n=Object.keys(t);if(o=n.length,o!==Object.keys(e).length)return!1;for(i=o;0!=i--;)if(!Object.prototype.hasOwnProperty.call(e,n[i]))return!1;for(i=o;0!=i--;){const o=n[i];if(!Se(t[o],e[o]))return!1}return!0}return t!=t&&e!=e},Ie=()=>new Promise((t=>{var e;e=t,requestAnimationFrame((()=>setTimeout(e,0)))})),Te={auto:1,heat_cool:2,heat:3,cool:4,dry:5,fan_only:6,off:7},ze=(t,e)=>Te[t]-Te[e];const Oe=t=>{zt(window,"haptic",t)},Me=["hs","xy","rgb","rgbw","rgbww"],De=[...Me,"color_temp","brightness"],Le=(t,e,i)=>t.subscribeMessage((t=>e(t)),Object.assign({type:"render_template"},i)) /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */,Ce=1,$e=3,Ee=4,Ae=t=>(...e)=>({_$litDirective$:t,values:e});class Se{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}const Ie=(t,e)=>{const i=(()=>{const t=document.body;if(t.querySelector("action-handler"))return t.querySelector("action-handler");const e=document.createElement("action-handler");return t.appendChild(e),e})();i&&i.bind(t,e)},Te=Ae(class extends Se{update(t,[e]){return Ie(t.element,e),R}render(t){}}),ze=(t,e)=>((t,e,i=!0)=>{const n=It(e),o="group"===n?"homeassistant":n;let r;switch(n){case"lock":r=i?"unlock":"lock";break;case"cover":r=i?"open_cover":"close_cover";break;case"button":case"input_button":r="press";break;case"scene":r="turn_on";break;default:r=i?"turn_on":"turn_off"}return t.callService(o,r,{entity_id:e})})(t,e,pt.includes(t.states[e].state)),Oe=async(t,e,i,n)=>{var o;let r;if("double_tap"===n&&i.double_tap_action?r=i.double_tap_action:"hold"===n&&i.hold_action?r=i.hold_action:"tap"===n&&i.tap_action&&(r=i.tap_action),r||(r={action:"more-info"}),r.confirmation&&(!r.confirmation.exemptions||!r.confirmation.exemptions.some((t=>t.user===e.user.id)))){let t;if(ye("warning"),"call-service"===r.action){const[i,n]=r.service.split(".",2),o=e.services;if(i in o&&n in o[i]){t=`${((t,e,i)=>t(`component.${e}.title`)||(null==i?void 0:i.name)||e)(await e.loadBackendTranslation("title"),i)}: ${o[i][n].name||n}`}}if(!confirm(r.confirmation.text||e.localize("ui.panel.lovelace.cards.actions.action_confirmation","action",t||e.localize("ui.panel.lovelace.editor.action-editor.actions."+r.action)||r.action)))return}switch(r.action){case"more-info":i.entity||i.camera_image?At(t,"hass-more-info",{entityId:i.entity?i.entity:i.camera_image}):(Me(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_entity_more_info")}),ye("failure"));break;case"navigate":r.navigation_path?((t,e)=>{var i;const n=(null==e?void 0:e.replace)||!1;n?St.history.replaceState((null===(i=St.history.state)||void 0===i?void 0:i.root)?{root:!0}:null,"",t):St.history.pushState(null,"",t),At(St,"location-changed",{replace:n})})(r.navigation_path):(Me(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_navigation_path")}),ye("failure"));break;case"url":r.url_path?window.open(r.url_path):(Me(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_url")}),ye("failure"));break;case"toggle":i.entity?(ze(e,i.entity),ye("light")):(Me(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_entity_toggle")}),ye("failure"));break;case"call-service":{if(!r.service)return Me(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_service")}),void ye("failure");const[i,n]=r.service.split(".",2);e.callService(i,n,null!==(o=r.data)&&void 0!==o?o:r.service_data,r.target),ye("light");break}case"fire-dom-event":At(t,"ll-custom",r)}},Me=(t,e)=>At(t,"hass-notification",e);function Le(t){return void 0!==t&&"none"!==t.action}const De=ce({user:ue()}),je=me([re(),ce({text:de(ue()),excemptions:de(oe(De))})]),Pe=ce({action:le("url"),url_path:ue(),confirmation:de(je)}),Ne=ce({action:le("call-service"),service:ue(),service_data:de(ce()),data:de(ce()),target:de(ce({entity_id:de(me([ue(),oe(ue())])),device_id:de(me([ue(),oe(ue())])),area_id:de(me([ue(),oe(ue())]))})),confirmation:de(je)}),Ve=ce({action:le("navigate"),navigation_path:ue(),confirmation:de(je)}),Re=he({action:le("fire-dom-event")}),Fe=ce({action:ae(["none","toggle","more-info","call-service","url","navigate"]),confirmation:de(je)}),Be=ie((t=>{if(t&&"object"==typeof t&&"action"in t)switch(t.action){case"call-service":return Ne;case"fire-dom-event":return Re;case"navigate":return Ve;case"url":return Pe}return Fe})),Ue=d` + */,je=1,Pe=3,Ne=4,Re=t=>(...e)=>({_$litDirective$:t,values:e});let Ve=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}};const Fe=(t,e)=>{const i=(()=>{const t=document.body;if(t.querySelector("action-handler"))return t.querySelector("action-handler");const e=document.createElement("action-handler");return t.appendChild(e),e})();i&&i.bind(t,e)},Be=Re(class extends Ve{update(t,[e]){return Fe(t.element,e),H}render(t){}}),Ue=(t,e)=>((t,e,i=!0)=>{const o=Mt(e),n="group"===o?"homeassistant":o;let r;switch(o){case"lock":r=i?"unlock":"lock";break;case"cover":r=i?"open_cover":"close_cover";break;case"button":case"input_button":r="press";break;case"scene":r="turn_on";break;default:r=i?"turn_on":"turn_off"}return t.callService(n,r,{entity_id:e})})(t,e,vt.includes(t.states[e].state)),He=async(t,e,i,o)=>{var n;let r;if("double_tap"===o&&i.double_tap_action?r=i.double_tap_action:"hold"===o&&i.hold_action?r=i.hold_action:"tap"===o&&i.tap_action&&(r=i.tap_action),r||(r={action:"more-info"}),r.confirmation&&(!r.confirmation.exemptions||!r.confirmation.exemptions.some((t=>t.user===e.user.id)))){let t;if(Oe("warning"),"call-service"===r.action){const[i,o]=r.service.split(".",2),n=e.services;if(i in n&&o in n[i]){t=`${((t,e,i)=>t(`component.${e}.title`)||(null==i?void 0:i.name)||e)(await e.loadBackendTranslation("title"),i)}: ${n[i][o].name||o}`}}if(!confirm(r.confirmation.text||e.localize("ui.panel.lovelace.cards.actions.action_confirmation","action",t||e.localize("ui.panel.lovelace.editor.action-editor.actions."+r.action)||r.action)))return}switch(r.action){case"more-info":i.entity||i.camera_image?zt(t,"hass-more-info",{entityId:i.entity?i.entity:i.camera_image}):(Ye(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_entity_more_info")}),Oe("failure"));break;case"navigate":r.navigation_path?((t,e)=>{var i;const o=(null==e?void 0:e.replace)||!1;o?Ot.history.replaceState((null===(i=Ot.history.state)||void 0===i?void 0:i.root)?{root:!0}:null,"",t):Ot.history.pushState(null,"",t),zt(Ot,"location-changed",{replace:o})})(r.navigation_path):(Ye(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_navigation_path")}),Oe("failure"));break;case"url":r.url_path?window.open(r.url_path):(Ye(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_url")}),Oe("failure"));break;case"toggle":i.entity?(Ue(e,i.entity),Oe("light")):(Ye(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_entity_toggle")}),Oe("failure"));break;case"call-service":{if(!r.service)return Ye(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_service")}),void Oe("failure");const[i,o]=r.service.split(".",2);e.callService(i,o,null!==(n=r.data)&&void 0!==n?n:r.service_data,r.target),Oe("light");break}case"fire-dom-event":zt(t,"ll-custom",r)}},Ye=(t,e)=>zt(t,"hass-notification",e);function Xe(t){return void 0!==t&&"none"!==t.action}const We=xe({user:ke()}),qe=$e([_e(),xe({text:we(ke()),excemptions:we(ge(We))})]),Ke=xe({action:be("url"),url_path:ke(),confirmation:we(qe)}),Ge=xe({action:be("call-service"),service:ke(),service_data:we(xe()),data:we(xe()),target:we(xe({entity_id:we($e([ke(),ge(ke())])),device_id:we($e([ke(),ge(ke())])),area_id:we($e([ke(),ge(ke())]))})),confirmation:we(qe)}),Ze=xe({action:be("navigate"),navigation_path:ke(),confirmation:we(qe)}),Je=Ce({action:be("fire-dom-event")}),Qe=xe({action:ve(["none","toggle","more-info","call-service","url","navigate"]),confirmation:we(qe)}),ti=pe((t=>{if(t&&"object"==typeof t&&"action"in t)switch(t.action){case"call-service":return Ge;case"fire-dom-event":return Je;case"navigate":return Ze;case"url":return Ke}return Qe})),ei=h` #sortable a:nth-of-type(2n) paper-icon-item { animation-name: keyframes1; animation-iteration-count: infinite; @@ -181,24 +167,7 @@ const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e color: var(--secondary-text-color); cursor: pointer; } -`,He=(t,e,i,n)=>{const[o,r,a]=t.split(".",3);return Number(o)>e||Number(o)===e&&(void 0===n?Number(r)>=i:Number(r)>i)||void 0!==n&&Number(o)===e&&Number(r)===i&&Number(a)>=n},Ye=["toggle","more-info","navigate","url","call-service","none"];let Xe=class extends ot{constructor(){super(...arguments),this.label="",this.configValue=""}_actionChanged(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:e}}))}render(){return N` - - `}};n([st()],Xe.prototype,"label",void 0),n([st()],Xe.prototype,"value",void 0),n([st()],Xe.prototype,"configValue",void 0),n([st()],Xe.prototype,"actions",void 0),n([st()],Xe.prototype,"hass",void 0),Xe=n([at("mushroom-action-picker")],Xe);let We=class extends ot{render(){return N` - - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],We.prototype,"hass",void 0),n([st()],We.prototype,"selector",void 0),n([st()],We.prototype,"value",void 0),n([st()],We.prototype,"label",void 0),We=n([at("ha-selector-mush-action")],We);var qe={form:{color_picker:{values:{default:"اللون الإفتراضي"}},info_picker:{values:{default:"المعلومات الافتراضية",name:"الإسم",state:"الحالة","last-changed":"آخر تغيير","last-updated":"آخر تحديث",none:"لا شئ"}},icon_type_picker:{values:{default:"النوع افتراضي",icon:"أيقونة","entity-picture":"صورة الكيان",none:"لا شئ"}},layout_picker:{values:{default:"تخطيط افتراضي",vertical:"تخطيط رأسي",horizontal:"تخطيط أفقي"}},alignment_picker:{values:{default:"المحاذاة الافتراضية",start:"بداية",end:"نهاية",center:"توسيط",justify:"مساواة"}}},card:{generic:{icon_color:"لون الأيقونة",layout:"التخطيط",fill_container:"ملئ الحاوية",primary_info:"المعلومات الأساسية",secondary_info:"المعلومات الفرعية",icon_type:"نوع الأيقونة",content_info:"المحتوى",use_entity_picture:"استخدم صورة الكيان؟",collapsible_controls:"تصغير عناصر التحكم عند الإيقاف",icon_animation:"تحريك الرمز عندما يكون نشطًا؟"},light:{show_brightness_control:"التحكم في السطوع؟",use_light_color:"استخدم لون فاتح",show_color_temp_control:"التحكم في حرارة اللون؟",show_color_control:"التحكم في اللون؟",incompatible_controls:"قد لا يتم عرض بعض عناصر التحكم إذا كان الضوء الخاص بك لا يدعم الميزة."},fan:{show_percentage_control:"التحكم في النسبة المئوية؟",show_oscillate_control:"التحكم في التذبذب؟"},cover:{show_buttons_control:"أزرار التحكم؟",show_position_control:"التحكم في الموقع؟"},alarm_control_panel:{show_keypad:"إظهار لوحة المفاتيح"},template:{primary:"المعلومات الأساسية",secondary:"المعلومات الثانوية",multiline_secondary:"متعدد الأسطر الثانوية؟",entity_extra:"تستخدم في القوالب والإجراءات",content:"المحتوى",badge_icon:"أيقونة الشارة",badge_color:"لون الشارة",picture:"صورة (ستحل محل الأيقونة)"},title:{title:"العنوان",subtitle:"العنوان الفرعي"},chips:{alignment:"محاذاة"},weather:{show_conditions:"الأحوال الجوية؟",show_temperature:"الطقس؟"},update:{show_buttons_control:"أزرار التحكم؟"},vacuum:{commands:"الاوامر"},"media-player":{use_media_info:"استخدم معلومات الوسائط",use_media_artwork:"استخدم صورة الوسائط",show_volume_level:"إظهار مستوى الصوت",media_controls:"التحكم في الوسائط",media_controls_list:{on_off:"تشغيل/إيقاف",shuffle:"خلط",previous:"السابق",play_pause_stop:"تشغيل/إيقاف مؤقت/إيقاف",next:"التالي",repeat:"وضع التكرار"},volume_controls:"التحكم في الصوت",volume_controls_list:{volume_buttons:"أزرار الصوت",volume_set:"مستوى الصوت",volume_mute:"كتم"}},lock:{lock:"مقفل",unlock:"إلغاء قفل",open:"مفتوح"},humidifier:{show_target_humidity_control:"التحكم في الرطوبة؟?"},climate:{show_temperature_control:"التحكم في درجة الحرارة؟",hvac_modes:"أوضاع HVAC"}},chip:{sub_element_editor:{title:"محرر الرقاقة"},conditional:{chip:"رقاقة"},"chip-picker":{chips:"رقاقات",add:"أضف رقاقة",edit:"تعديل",clear:"مسح",select:"اختر الرقاقة",types:{action:"إجراء","alarm-control-panel":"تنبيه",back:"رجوع",conditional:"مشروط",entity:"الكيان",light:"Light",menu:"القائمة",template:"قالب",weather:"الطقس"}}}},Ke={editor:qe},Ge={form:{color_picker:{values:{default:"Výchozí barva"}},info_picker:{values:{default:"Základní informace",name:"Název",state:"Stav","last-changed":"Poslední změna","last-updated":"Poslední update",none:"Nic"}},icon_type_picker:{values:{default:"Výchozí typ",icon:"Ikona","entity-picture":"Ikona entity",none:"Nic"}},layout_picker:{values:{default:"Výchozí rozložení",vertical:"Svislé rozložení",horizontal:"Vodorovné rozložení"}},alignment_picker:{values:{default:"Výchozí zarovnání",start:"Začátek",end:"Konec",center:"Na střed",justify:"Důvod"}}},card:{generic:{icon_color:"Barva ikony",layout:"Rozložení",fill_container:"Vyplnit prostor",primary_info:"Základní informace",secondary_info:"Sekundární informace",icon_type:"Typ ikony",content_info:"Obsah",use_entity_picture:"Použít ikonu entity?",collapsible_controls:"Skrýt ovládací prvky pokud je VYP",icon_animation:"Animovaná ikona, pokud je aktivní?"},light:{show_brightness_control:"Ovládání jasu?",use_light_color:"Použít ovládání světla",show_color_temp_control:"Ovládání teploty světla?",show_color_control:"Ovládání baryv světla?",incompatible_controls:"Některé ovládací prvky se nemusí zobrazit, pokud vaše světlo tuto funkci nepodporuje."},fan:{show_percentage_control:"Ovládání v procentech?",show_oscillate_control:"Oscillate control?"},cover:{show_buttons_control:"Zobrazit ovládací tlačítka?",show_position_control:"Zobrazit ovládání polohy?"},alarm_control_panel:{show_keypad:"Zobrazit klávesnici"},template:{primary:"Základní informace",secondary:"Sekundární informace",multiline_secondary:"Víceřádková sekundární informace?",entity_extra:"Použito v šablonách a akcích",content:"Obsah",badge_icon:"Ikona odznaku",badge_color:"Barva odznaku",picture:"Obrázek (nahradí ikonu)"},title:{title:"Titulek",subtitle:"Popis"},chips:{alignment:"Zarovnání"},weather:{show_conditions:"Zobrazit podmínky?",show_temperature:"Zobrazit teplot?u"},update:{show_buttons_control:"Zobrazit ovládací tlačítka?"},vacuum:{commands:"Příkazy"},"media-player":{use_media_info:"Použít informace o médiích",use_media_artwork:"Použít ilustrace médií",show_volume_level:"Zobrazit úroveň hlasitosti",media_controls:"Ovládání médií",media_controls_list:{on_off:"Vyp / Zap",shuffle:"Zamíchat",previous:"Předchozí skladba",play_pause_stop:"hrát/pauza/zastavit",next:"Další skladba",repeat:"Opakovat"},volume_controls:"Ovládání hlasitosti",volume_controls_list:{volume_buttons:"Tlačítka hlasitosti",volume_set:"Úroveň hlasitosti",volume_mute:"Ztlumit"}},lock:{lock:"Zamčeno",unlock:"Odemčeno",open:"Otevřeno"},humidifier:{show_target_humidity_control:"Ovládání vlhkosti?"},climate:{show_temperature_control:"Ovládání teploty?",hvac_modes:"HVAC Mód"}},chip:{sub_element_editor:{title:"Editor tlačítek"},conditional:{chip:"Tlačítko"},"chip-picker":{chips:"Tlačítka",add:"Přidat tlačítko",edit:"Editovat",clear:"Vymazat",select:"Vybrat tlačítko",types:{action:"Akce","alarm-control-panel":"Alarm",back:"Zpět",conditional:"Podmínky",entity:"Entita",light:"Světlo",menu:"Menu",template:"Šablona",weather:"Počasí"}}}},Ze={editor:Ge},Je={form:{color_picker:{values:{default:"Standard farve"}},info_picker:{values:{default:"Standard information",name:"Navn",state:"Status","last-changed":"Sidst ændret","last-updated":"Sidst opdateret",none:"Ingen"}},icon_type_picker:{values:{default:"Standard type",icon:"Ikon","entity-picture":"Enheds billede",none:"Ingen"}},layout_picker:{values:{default:"Standard layout",vertical:"Vertikal layout",horizontal:"Horisontal layout"}},alignment_picker:{values:{default:"Standard justering",start:"Start",end:"Slut",center:"Centrer",justify:"Lige margener"}}},card:{generic:{icon_color:"Ikon farve",layout:"Layout",fill_container:"Fyld container",primary_info:"Primær information",secondary_info:"Sekundær information",icon_type:"Ikon type",content_info:"Indhold",use_entity_picture:"Brug enheds billede?",collapsible_controls:"Skjul kontroller når slukket",icon_animation:"Animér ikon når aktiv?"},light:{show_brightness_control:"Lysstyrkekontrol?",use_light_color:"Brug lysfarve",show_color_temp_control:"Temperatur farvekontrol?",show_color_control:"Farvekontrol?",incompatible_controls:"Nogle kontroller vises muligvis ikke, hvis dit lys ikke understøtter funktionen."},fan:{show_percentage_control:"Procentvis kontrol?",show_oscillate_control:"Oscillerende kontrol?"},cover:{show_buttons_control:"Betjeningsknapper?",show_position_control:"Positionskontrol?"},alarm_control_panel:{show_keypad:"Vis tastatur"},template:{primary:"Primær information",secondary:"Sekundær information",multiline_secondary:"Multi-linje skundær?",entity_extra:"Anvendes i skabelober og handlinger",content:"Indhold",badge_icon:"Badge ikon",badge_color:"Badge farve",picture:"Billede (erstatter ikonen)"},title:{title:"Titel",subtitle:"Undertitel"},chips:{alignment:"Justering"},weather:{show_conditions:"Forhold?",show_temperature:"Temperatur?"},update:{show_buttons_control:"Betjeningsknapper?"},vacuum:{commands:"Kommandoer"},"media-player":{use_media_info:"Brug medie info",use_media_artwork:"Brug mediebilleder",show_volume_level:"Vis volumen niveau",media_controls:"Medie kontrol",media_controls_list:{on_off:"Tænd/Sluk",shuffle:"Bland",previous:"Forrige nummer",play_pause_stop:"Afspil/Pause/Stop",next:"Næste nummer",repeat:"Gentagelsestilstand"},volume_controls:"Volumen kontrol",volume_controls_list:{volume_buttons:"Volumen knapper",volume_set:"Volumenniveau",volume_mute:"Lydløs"}},lock:{lock:"Lås",unlock:"Lås op",open:"Åben"},humidifier:{show_target_humidity_control:"Luftfugtigheds kontrol?"},climate:{show_temperature_control:"Temperatur kontrol?",hvac_modes:"HVAC-tilstande"}},chip:{sub_element_editor:{title:"Chip-editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Tilføj chip",edit:"Rediger",clear:"Nulstil",select:"Vælg chip",types:{action:"Handling","alarm-control-panel":"Alarm",back:"Tilbage",conditional:"Betinget",entity:"Enhed",light:"Lys",menu:"Menu",template:"Skabelon",weather:"Vejr"}}}},Qe={editor:Je},ti={form:{color_picker:{values:{default:"Standardfarbe"}},info_picker:{values:{default:"Standard-Information",name:"Name",state:"Zustand","last-changed":"Letzte Änderung","last-updated":"Letzte Aktualisierung",none:"Keine"}},icon_type_picker:{values:{default:"Standard-Typ",icon:"Icon","entity-picture":"Entitätsbild",none:"Keines"}},layout_picker:{values:{default:"Standard-Layout",vertical:"Vertikales Layout",horizontal:"Horizontales Layout"}},alignment_picker:{values:{default:"Standard",start:"Anfang",end:"Ende",center:"Mitte",justify:"Ausrichten"}}},card:{generic:{icon_color:"Icon-Farbe",layout:"Layout",fill_container:"Container ausfüllen",primary_info:"Primäre Information",secondary_info:"Sekundäre Information",icon_type:"Icon-Typ",content_info:"Inhalt",use_entity_picture:"Entitätsbild verwenden?",collapsible_controls:"Schieberegler einklappen, wenn aus",icon_animation:"Icon animieren, wenn aktiv?"},light:{show_brightness_control:"Helligkeitsregelung?",use_light_color:"Farbsteuerung verwenden",show_color_temp_control:"Farbtemperatursteuerung?",show_color_control:"Farbsteuerung?",incompatible_controls:"Einige Steuerelemente werden möglicherweise nicht angezeigt, wenn Ihr Licht diese Funktion nicht unterstützt."},fan:{show_percentage_control:"Prozentuale Kontrolle?",show_oscillate_control:"Oszillationssteuerung?"},cover:{show_buttons_control:"Schaltflächensteuerung?",show_position_control:"Positionssteuerung?",show_tilt_position_control:"Winkelsteuerung?"},alarm_control_panel:{show_keypad:"Keypad anzeigen"},template:{primary:"Primäre Information",secondary:"Sekundäre Information",multiline_secondary:"Mehrzeilig sekundär?",entity_extra:"Wird in Vorlagen und Aktionen verwendet",content:"Inhalt",badge_icon:"Badge-Icon",badge_color:"Badge-Farbe",picture:"Bild (ersetzt das Icon)"},title:{title:"Titel",subtitle:"Untertitel"},chips:{alignment:"Ausrichtung"},weather:{show_conditions:"Bedingungen?",show_temperature:"Temperatur?"},update:{show_buttons_control:"Schaltflächensteuerung?"},vacuum:{commands:"Befehle",commands_list:{on_off:"An/Ausschalten"}},"media-player":{use_media_info:"Medieninfos verwenden",use_media_artwork:"Mediengrafik verwenden",show_volume_level:"Lautstärke-Level anzeigen",media_controls:"Mediensteuerung",media_controls_list:{on_off:"Ein/Aus",shuffle:"Zufällige Wiedergabe",previous:"Vorheriger Titel",play_pause_stop:"Play/Pause/Stop",next:"Nächster Titel",repeat:"Wiederholen"},volume_controls:"Lautstärkesteuerung",volume_controls_list:{volume_buttons:"Lautstärke-Buttons",volume_set:"Lautstärke-Level",volume_mute:"Stumm"}},lock:{lock:"Verriegeln",unlock:"Entriegeln",open:"Öffnen"},humidifier:{show_target_humidity_control:"Luftfeuchtigkeitssteuerung?"},climate:{show_temperature_control:"Temperatursteuerung?",hvac_modes:"HVAC-Modi"}},chip:{sub_element_editor:{title:"Chip Editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Chip hinzufügen",edit:"Editieren",clear:"Löschen",select:"Chip auswählen",types:{action:"Aktion","alarm-control-panel":"Alarm",back:"Zurück",conditional:"Bedingung",entity:"Entität",light:"Licht",menu:"Menü",template:"Vorlage",weather:"Wetter"}}}},ei={editor:ti},ii={form:{color_picker:{values:{default:"Προεπιλεγμένο χρώμα"}},info_picker:{values:{default:"Προεπιλεγμένες πληροφορίες",name:"Όνομα",state:"Κατάσταση","last-changed":"Τελευταία αλλαγή","last-updated":"Τελευταία ενημέρωση",none:"Τίποτα"}},layout_picker:{values:{default:"Προεπιλεγμένη διάταξη",vertical:"Κάθετη διάταξη",horizontal:"Οριζόντια διάταξη"}},alignment_picker:{values:{default:"Προεπιλεγμένη στοίχιση",start:"Στοίχιση αριστερά",end:"Στοίχιση δεξιά",center:"Στοίχιση στο κέντρο",justify:"Πλήρης στοίχιση"}}},card:{generic:{icon_color:"Χρώμα εικονιδίου",layout:"Διάταξη",primary_info:"Πρωτεύουσες πληροφορίες",secondary_info:"Δευτερεύουσες πληροφορίες",content_info:"Περιεχόμενο",use_entity_picture:"Χρήση εικόνας οντότητας;",icon_animation:"Κίνηση εικονιδίου όταν είναι ενεργό;"},light:{show_brightness_control:"Έλεγχος φωτεινότητας;",use_light_color:"Χρήση χρώματος φωτος",show_color_temp_control:"Έλεγχος χρώματος θερμοκρασίας;",show_color_control:"Έλεγχος χρώματος;",incompatible_controls:"Ορισμένα στοιχεία ελέγχου ενδέχεται να μην εμφανίζονται εάν το φωτιστικό σας δεν υποστηρίζει τη λειτουργία."},fan:{show_percentage_control:"Έλεγχος ποσοστού;",show_oscillate_control:"Έλεγχος ταλάντωσης;"},cover:{show_buttons_control:"Έλεγχος κουμπιών;",show_position_control:"Έλεγχος θέσης;"},template:{primary:"Πρωτεύουσες πληροφορίες",secondary:"Δευτερεύουσες πληροφορίες",multiline_secondary:"Δευτερεύουσες πολλαπλών γραμμών;",entity_extra:"Χρησιμοποιείται σε πρότυπα και ενέργειες",content:"Περιεχόμενο"},title:{title:"Τίτλος",subtitle:"Υπότιτλος"},chips:{alignment:"Ευθυγράμμιση"},weather:{show_conditions:"Συνθήκες;",show_temperature:"Θερμοκρασία;"},update:{show_buttons_control:"Έλεγχος κουμπιών;"},vacuum:{commands:"Εντολές"},"media-player":{use_media_info:"Χρήση πληροφοριών πολυμέσων",use_media_artwork:"Χρήση έργων τέχνης πολυμέσων",media_controls:"Έλεγχος πολυμέσων",media_controls_list:{on_off:"Ενεργοποίηση/απενεργοποίηση",shuffle:"Τυχαία σειρά",previous:"Προηγούμενο κομμάτι",play_pause_stop:"Αναπαραγωγή/παύση/διακοπή",next:"Επόμενο κομμάτι",repeat:"Λειτουργία επανάληψης"},volume_controls:"Χειριστήρια έντασης ήχου",volume_controls_list:{volume_buttons:"Κουμπιά έντασης ήχου",volume_set:"Επίπεδο έντασης ήχου",volume_mute:"Σίγαση"}}},chip:{sub_element_editor:{title:"Επεξεργαστής Chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Προσθήκη chip",edit:"Επεξεργασία",clear:"Καθαρισμός",select:"Επιλογή chip",types:{action:"Ενέργεια","alarm-control-panel":"Συναγερμός",back:"Πίσω",conditional:"Υπό προϋποθέσεις",entity:"Οντότητα",light:"Φως",menu:"Μενού",template:"Πρότυπο",weather:"Καιρός"}}}},ni={editor:ii},oi={form:{color_picker:{values:{default:"Default color"}},info_picker:{values:{default:"Default information",name:"Name",state:"State","last-changed":"Last Changed","last-updated":"Last Updated",none:"None"}},icon_type_picker:{values:{default:"Default type",icon:"Icon","entity-picture":"Entity picture",none:"None"}},layout_picker:{values:{default:"Default layout",vertical:"Vertical layout",horizontal:"Horizontal layout"}},alignment_picker:{values:{default:"Default alignment",start:"Start",end:"End",center:"Center",justify:"Justify"}}},card:{generic:{icon_color:"Icon color",layout:"Layout",fill_container:"Fill container",primary_info:"Primary information",secondary_info:"Secondary information",icon_type:"Icon type",content_info:"Content",use_entity_picture:"Use entity picture?",collapsible_controls:"Collapse controls when off",icon_animation:"Animate icon when active?"},light:{show_brightness_control:"Brightness control?",use_light_color:"Use light color",show_color_temp_control:"Temperature color control?",show_color_control:"Color control?",incompatible_controls:"Some controls may not be displayed if your light does not support the feature."},fan:{show_percentage_control:"Percentage control?",show_oscillate_control:"Oscillate control?"},cover:{show_buttons_control:"Control buttons?",show_position_control:"Position control?",show_tilt_position_control:"Tilt control?"},alarm_control_panel:{show_keypad:"Show keypad"},template:{primary:"Primary information",secondary:"Secondary information",multiline_secondary:"Multiline secondary?",entity_extra:"Used in templates and actions",content:"Content",badge_icon:"Badge icon",badge_color:"Badge color",picture:"Picture (will replace the icon)"},title:{title:"Title",subtitle:"Subtitle"},chips:{alignment:"Alignment"},weather:{show_conditions:"Conditions?",show_temperature:"Temperature?"},update:{show_buttons_control:"Control buttons?"},vacuum:{commands:"Commands",commands_list:{on_off:"Turn on/off"}},"media-player":{use_media_info:"Use media info",use_media_artwork:"Use media artwork",show_volume_level:"Show volume level",media_controls:"Media controls",media_controls_list:{on_off:"Turn on/off",shuffle:"Shuffle",previous:"Previous track",play_pause_stop:"Play/pause/stop",next:"Next track",repeat:"Repeat mode"},volume_controls:"Volume controls",volume_controls_list:{volume_buttons:"Volume buttons",volume_set:"Volume level",volume_mute:"Mute"}},lock:{lock:"Lock",unlock:"Unlock",open:"Open"},humidifier:{show_target_humidity_control:"Humidity control?"},climate:{show_temperature_control:"Temperature control?",hvac_modes:"HVAC Modes"}},chip:{sub_element_editor:{title:"Chip editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Add chip",edit:"Edit",clear:"Clear",select:"Select chip",types:{action:"Action","alarm-control-panel":"Alarm",back:"Back",conditional:"Conditional",entity:"Entity",light:"Light",menu:"Menu",template:"Template",weather:"Weather"}}}},ri={editor:oi},ai={form:{color_picker:{values:{default:"Color predeterminado"}},info_picker:{values:{default:"Informacion predeterminada",name:"Nombre",state:"Estado","last-changed":"Último cambio","last-updated":"Última actualización",none:"Ninguno"}},layout_picker:{values:{default:"Diseño predeterminado",vertical:"Diseño vertical",horizontal:"Diseño Horizontal"}},alignment_picker:{values:{default:"Alineación predeterminada",start:"Inicio",end:"Final",center:"Centrado",justify:"Justificado"}}},card:{generic:{icon_color:"Color de icono",layout:"Diseño",fill_container:"Rellenar",primary_info:"Información primaria",secondary_info:"Información secundaria",content_info:"Contenido",use_entity_picture:"¿Usar imagen de entidad?",collapsible_controls:"Contraer controles cuando está apagado",icon_animation:"¿Icono animado cuando está activo?"},light:{show_brightness_control:"¿Controlar brillo?",use_light_color:"Usar color de la luz",show_color_temp_control:"¿Controlar temperatura del color?",show_color_control:"¿Controlar Color?",incompatible_controls:"Es posible que algunos controles no se muestren si su luz no es compatible con la función."},fan:{show_percentage_control:"¿Controlar porcentaje?",show_oscillate_control:"¿Controlar oscilación?"},cover:{show_buttons_control:"¿Botones de control?",show_position_control:"¿Control de posición?"},alarm_control_panel:{show_keypad:"Mostrar teclado"},template:{primary:"Información primaria",secondary:"Información secundaria",multiline_secondary:"¿Secundaria multilínea?",entity_extra:"Utilizado en plantillas y acciones.",content:"Contenido"},title:{title:"Título",subtitle:"Subtítulo"},chips:{alignment:"Alineación"},weather:{show_conditions:"¿Condiciones?",show_temperature:"¿Temperatura?"},update:{show_buttons_control:"¿Botones de control?"},vacuum:{commands:"Comandos"},"media-player":{use_media_info:"Usar información multimedia",use_media_artwork:"Usar ilustraciones multimedia",show_volume_level:"Mostrar nivel de volumen",media_controls:"Controles multimedia",media_controls_list:{on_off:"Encender/apagar",shuffle:"Aleatoria",previous:"Pista anterior",play_pause_stop:"Play/pausa/parar",next:"Pista siguiente",repeat:"Modo de repetición"},volume_controls:"Controles de volumen",volume_controls_list:{volume_buttons:"Botones de volumen",volume_set:"Nivel de volumen",volume_mute:"Silenciar"}},lock:{lock:"Bloquear",unlock:"Desbloquear",open:"Abrir"},humidifier:{show_target_humidity_control:"¿Controlar humedad?"}},chip:{sub_element_editor:{title:"Editor de chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Añadir chip",edit:"Editar",clear:"Limpiar",select:"Seleccionar chip",types:{action:"Acción","alarm-control-panel":"Alarma",back:"Volver",conditional:"Condicional",entity:"Entidad",light:"Luz",menu:"Menú",template:"Plantilla",weather:"Clima"}}}},li={editor:ai},si={form:{color_picker:{values:{default:"Oletusväri"}},info_picker:{values:{default:"Oletustiedot",name:"Nimi",state:"Tila","last-changed":"Viimeksi muuttunut","last-updated":"Viimeksi päivittynyt",none:"Ei mitään"}},icon_type_picker:{values:{default:"Oletustyyppi",icon:"Kuvake","entity-picture":"Kohteen kuva",none:"Ei mitään"}},layout_picker:{values:{default:"Oletusasettelu",vertical:"Pystysuuntainen",horizontal:"Vaakasuuntainen"}},alignment_picker:{values:{default:"Keskitys",start:"Alku",end:"Loppu",center:"Keskitä",justify:"Sovita"}}},card:{generic:{icon_color:"Ikonin väri",layout:"Asettelu",fill_container:"Täytä alue",primary_info:"Ensisijaiset tiedot",secondary_info:"Toissijaiset tiedot",icon_type:"Kuvakkeen tyyppi",content_info:"Sisältö",use_entity_picture:"Käytä kohteen kuvaa?",collapsible_controls:"Piilota toiminnot off-tilassa",icon_animation:"Animoi kuvake, kun aktiivinen?"},light:{show_brightness_control:"Kirkkauden säätö?",use_light_color:"Käytä valaisimen väriä",show_color_temp_control:"Värilämpötilan säätö?",show_color_control:"Värin säätö?",incompatible_controls:"Jotkin toiminnot eivät näy, jos valaisimesi ei tue niitä."},fan:{show_percentage_control:"Prosentuaalinen säätö?",show_oscillate_control:"Oskillaation säätö?"},cover:{show_buttons_control:"Toimintopainikkeet?",show_position_control:"Sijainnin hallinta?"},alarm_control_panel:{show_keypad:"Näytä näppäimet"},template:{primary:"Ensisijaiset tiedot",secondary:"Toissijaiset tiedot",multiline_secondary:"Monirivinen toissijainen tieto?",entity_extra:"Käytetään malleissa ja toiminnoissa",content:"Sisältö",badge_icon:"Merkin kuvake",badge_color:"Merkin väri",picture:"Kuva (korvaa kuvakkeen)"},title:{title:"Otsikko",subtitle:"Tekstitys"},chips:{alignment:"Asettelu"},weather:{show_conditions:"Ehdot?",show_temperature:"Lämpötila?"},update:{show_buttons_control:"Toimintopainikkeet?"},vacuum:{commands:"Komennot"},"media-player":{use_media_info:"Käytä median tietoja",use_media_artwork:"Käytä median kuvituksia",show_volume_level:"Näytä äänenvoimakkuuden hallinta",media_controls:"Toiminnot",media_controls_list:{on_off:"Päälle/pois",shuffle:"Sekoita",previous:"Edellinen kappale",play_pause_stop:"Toista/keskeytä/pysäytä",next:"Seuraava kappale",repeat:"Jatkuva toisto"},volume_controls:"Äänenvoimakkuuden hallinta",volume_controls_list:{volume_buttons:"Äänenvoimakkuuspainikkeet",volume_set:"Äänenvoimakkuus",volume_mute:"Mykistä"}},lock:{lock:"Lukitse",unlock:"Poista lukitus",open:"Avaa"},humidifier:{show_target_humidity_control:"Kosteudenhallinta?"}},chip:{sub_element_editor:{title:"Merkkieditori"},conditional:{chip:"Merkki"},"chip-picker":{chips:"Merkit",add:"Lisää merkki",edit:"Muokkaa",clear:"Tyhjennä",select:"Valitse merkki",types:{action:"Toiminto","alarm-control-panel":"Hälytys",back:"Takaisin",conditional:"Ehdollinen",entity:"Kohde",light:"Valaisin",menu:"Valikko",template:"Malli",weather:"Sää"}}}},ci={editor:si},di={form:{color_picker:{values:{default:"Couleur par défaut"}},info_picker:{values:{default:"Information par défaut",name:"Nom",state:"État","last-changed":"Dernière modification","last-updated":"Dernière mise à jour",none:"Aucune"}},icon_type_picker:{values:{default:"Type par défaut",icon:"Icône","entity-picture":"Image de l'entité",none:"Aucune"}},layout_picker:{values:{default:"Disposition par défault",vertical:"Disposition verticale",horizontal:"Disposition horizontale"}},alignment_picker:{values:{default:"Alignement par défaut",start:"Début",end:"Fin",center:"Centré",justify:"Justifié"}}},card:{generic:{icon_color:"Couleur de l'icône",layout:"Disposition",fill_container:"Remplir le conteneur",primary_info:"Information principale",secondary_info:"Information secondaire",icon_type:"Type d'icône",content_info:"Contenu",use_entity_picture:"Utiliser l'image de l'entité ?",collapsible_controls:"Reduire les contrôles quand éteint",icon_animation:"Animation de l'icône ?"},light:{show_brightness_control:"Contrôle de luminosité ?",use_light_color:"Utiliser la couleur de la lumière",show_color_temp_control:"Contrôle de la température ?",show_color_control:"Contrôle de la couleur ?",incompatible_controls:"Certains contrôles peuvent ne pas être affichés si votre lumière ne supporte pas la fonctionnalité."},fan:{show_percentage_control:"Contrôle de la vitesse ?",show_oscillate_control:"Contrôle de l'oscillation ?"},cover:{show_buttons_control:"Contrôle avec boutons ?",show_position_control:"Contrôle de la position ?"},alarm_control_panel:{show_keypad:"Afficher le clavier"},template:{primary:"Information principale",secondary:"Information secondaire",multiline_secondary:"Information secondaire sur plusieurs lignes ?",entity_extra:"Utilisée pour les templates et les actions",content:"Contenu",badge_icon:"Icône du badge",badge_color:"Couleur du badge",picture:"Picture (remplacera l'icône)"},title:{title:"Titre",subtitle:"Sous-titre"},chips:{alignment:"Alignement"},weather:{show_conditons:"Conditions ?",show_temperature:"Température ?"},update:{show_buttons_control:"Contrôle avec boutons ?"},vacuum:{commands:"Commandes",commands_list:{on_off:"Allumer/Éteindre"}},"media-player":{use_media_info:"Utiliser les informations du media",use_media_artwork:"Utiliser l'illustration du media",show_volume_level:"Afficher le niveau de volume",media_controls:"Contrôles du media",media_controls_list:{on_off:"Allumer/Éteindre",shuffle:"Lecture aléatoire",previous:"Précédent",play_pause_stop:"Lecture/pause/stop",next:"Suivant",repeat:"Mode de répétition"},volume_controls:"Contrôles du volume",volume_controls_list:{volume_buttons:"Bouton de volume",volume_set:"Niveau de volume",volume_mute:"Muet"}},lock:{lock:"Verrouiller",unlock:"Déverrouiller",open:"Ouvrir"},humidifier:{show_target_humidity_control:"Contrôle d'humidité ?"},climate:{show_temperature_control:"Contrôle de la température?",hvac_modes:"Modes du thermostat"}},chip:{sub_element_editor:{title:'Éditeur de "chip"'},conditional:{chip:"Chip"},"chip-picker":{chips:'"Chips"',add:'Ajouter une "chip"',edit:"Modifier",clear:"Effacer",select:'Sélectionner une "chip"',types:{action:"Action","alarm-control-panel":"Alarme",back:"Retour",conditional:"Conditionnel",entity:"Entité",light:"Lumière",menu:"Menu",template:"Template",weather:"Météo"}}}},ui={editor:di},hi={form:{color_picker:{values:{default:"צבע ברירת מחדל"}},info_picker:{values:{default:"מידע ברירת מחדל",name:"שם",state:"מצב","last-changed":"שונה לאחרונה","last-updated":"עודכן לאחרונה",none:"ריק"}},layout_picker:{values:{default:"סידור ברירת מחדל",vertical:"סידור מאונך",horizontal:"סידור מאוזן"}},alignment_picker:{values:{default:"יישור ברירת מחדל",start:"התחלה",end:"סוף",center:"אמצע",justify:"מוצדק"}}},card:{generic:{icon_color:"צבע אייקון",layout:"סידור",fill_container:"מלא גבולות",primary_info:"מידע ראשי",secondary_info:"מידע מישני",content_info:"תוכן",use_entity_picture:"השתמש בתמונת ישות?",collapsible_controls:"הסתר שליטה כשאר מכובה?",icon_animation:"להנפיש אייקון כאשר דלוק?"},light:{show_brightness_control:"שליטה בבהירות?",use_light_color:"השתמש בצבע האור",show_color_temp_control:"שליטה בגוון האור?",show_color_control:"שליטה בצבע האור?",incompatible_controls:"יתכן וחלק מהכפתורים לא יופיעו אם התאורה אינה תומכת בתכונה."},fan:{show_percentage_control:"שליטה באחוז?",show_oscillate_control:"שליטה בהתנדנדות?"},cover:{show_buttons_control:"כפתורי שליטה?",show_position_control:"שליטה במיקום?"},alarm_control_panel:{show_keypad:"הצג מקלדת"},template:{primary:"מידע ראשי",secondary:"מידע מישני",multiline_secondary:"מידע מישני רו קווי?",entity_extra:"משמש בתבניות ופעולות",content:"תוכן"},title:{title:"כותרת",subtitle:"כתובית"},chips:{alignment:"יישור"},weather:{show_conditions:"הצג תנאים?",show_temperature:"הצג טמפרטורה?"},update:{show_buttons_control:"הצג כפתורי שליטה?"},vacuum:{commands:"פקודות",icon_animation:"להנפיש אייקון כאשר דלוק?"},"media-player":{use_media_info:"השתמש במידע מדיה",use_media_artwork:"השתמש באומנות מדיה",show_volume_level:"הצג שליטת ווליום",media_controls:"שליטה במדיה",media_controls_list:{on_off:"הדלק/כבה",shuffle:"ערבב",previous:"רצועה קודמת",play_pause_stop:"נגן/השהה/הפסק",next:"רצועה הבאה",repeat:"חזרה"},volume_controls:"שליטה בווליום",volume_controls_list:{volume_buttons:"כפתורי ווליום",volume_set:"רמת ווליום",volume_mute:"השתק"}},lock:{lock:"נעל",unlock:"בטל נעילה",open:"פתח"},humidifier:{show_target_humidity_control:"שליטה בלחות?"}},chip:{sub_element_editor:{title:"עורך שבב"},conditional:{chip:"שבב"},"chip-picker":{chips:"שבבים",add:"הוסף שבב",edit:"ערוך",clear:"נקה",select:"בחר שבב",types:{action:"פעולה","alarm-control-panel":"אזעקה",back:"חזור",conditional:"מותנה",entity:"ישות",light:"אור",menu:"תפריט",template:"תבנית",weather:"מזג אוויר"}}}},mi={editor:hi},pi={form:{color_picker:{values:{default:"Alapértelmezett szín"}},info_picker:{values:{default:"Alepértelmezett információ",name:"Név",state:"Állapot","last-changed":"Utoljára módosítva","last-updated":"Utoljára frissítve",none:"Egyik sem"}},icon_type_picker:{values:{default:"Alapértelmezett típus",icon:"Ikon","entity-picture":"Entitás kép",none:"Egyik sem"}},layout_picker:{values:{default:"Alapértelmezet elrendezés",vertical:"Függőleges elrendezés",horizontal:"Vízszintes elrendezés"}},alignment_picker:{values:{default:"Alapértelmezett rendezés",start:"Kezdete",end:"Vége",center:"Közepe",justify:"Sorkizárt"}}},card:{generic:{icon_color:"Ikon szín",layout:"Elrendezés",fill_container:"Tároló kitöltése",primary_info:"Elsődleges információ",secondary_info:"Másodlagos információ",icon_type:"Ikon típus",content_info:"Tartalom",use_entity_picture:"Entitás kép használata",collapsible_controls:"Vezérlők összezárása kikapcsolt állapotban",icon_animation:"Ikon animálása aktív állapotban"},light:{show_brightness_control:"Fényerő vezérlő",use_light_color:"Fény szín használata",show_color_temp_control:"Színhőmérséklet vezérlő",show_color_control:"Szín vezérlő",incompatible_controls:"Azok a vezérlők nem lesznek megjelenítve, amelyeket a fényforrás nem támogat."},fan:{show_percentage_control:"Százalékos vezérlő",show_oscillate_control:"Oszcilláció vezérlő"},cover:{show_buttons_control:"Vezérlő gombok",show_position_control:"Pozíció vezérlő"},alarm_control_panel:{show_keypad:"Billentyűzet mutatása"},template:{primary:"Elsődleges információ",secondary:"Másodlagos információ",multiline_secondary:"Másodlagost több sorba?",entity_extra:"Used in templates and actions",content:"Tartalom",badge_icon:"Jelvény ikon",badge_color:"Jelvény szín",picture:"Kép (helyettesíteni fogja az ikont)"},title:{title:"Fejléc",subtitle:"Alcím"},chips:{alignment:"Rendezés"},weather:{show_conditions:"Állapotok",show_temperature:"Hőmérséklet"},update:{show_buttons_control:"Vezérlő gombok"},vacuum:{commands:"Utasítások"},"media-player":{use_media_info:"Média infó használata",use_media_artwork:"Média borító használata",show_volume_level:"Hangerő mutatása",media_controls:"Média vezérlők",media_controls_list:{on_off:"Ki/bekapcsolás",shuffle:"Véletlen lejátszás",previous:"Előző szám",play_pause_stop:"Lejátszás/szünet/állj",next:"Következő szám",repeat:"Ismétlés módja"},volume_controls:"Hangerő vezérlők",volume_controls_list:{volume_buttons:"Hangerő gombok",volume_set:"Hangerő szint",volume_mute:"Némítás"}},lock:{lock:"Zár",unlock:"Nyit",open:"Nyitva"},humidifier:{show_target_humidity_control:"Páratartalom vezérlő"},climate:{show_temperature_control:"Hőmérséklet vezérlő",hvac_modes:"HVAC mód"}},chip:{sub_element_editor:{title:"Chip szerkesztő"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chip-ek",add:"Chip hozzáadása",edit:"Szerkesztés",clear:"Ürítés",select:"Chip kiválasztása",types:{action:"Művelet","alarm-control-panel":"Riasztó",back:"Vissza",conditional:"Feltételes",entity:"Entitás",light:"Fényforrás",menu:"Menü",template:"Sablon",weather:"Időjárás"}}}},fi={editor:pi},gi={form:{color_picker:{values:{default:"Colore predefinito"}},info_picker:{values:{default:"Informazione predefinita",name:"Nome",state:"Stato","last-changed":"Ultimo Cambiamento","last-updated":"Ultimo Aggiornamento",none:"Nessuno"}},icon_type_picker:{values:{default:"Tipo predefinito",icon:"Icona","entity-picture":"Immagine dell'entità",none:"Nessuna"}},layout_picker:{values:{default:"Disposizione Predefinita",vertical:"Disposizione Verticale",horizontal:"Disposizione Orizzontale"}},alignment_picker:{values:{default:"Allineamento predefinito",start:"Inizio",end:"Fine",center:"Centro",justify:"Giustificato"}}},card:{generic:{icon_color:"Colore dell'icona",layout:"Disposizione",fill_container:"Riempi il contenitore",primary_info:"Informazione primaria",secondary_info:"Informazione secondaria",icon_type:"Tipo icona",content_info:"Contenuto",use_entity_picture:"Usa l'immagine dell'entità",collapsible_controls:"Nascondi i controlli quando spento",icon_animation:"Anima l'icona quando attiva"},light:{use_light_color:"Usa il colore della luce",show_brightness_control:"Controllo luminosità",show_color_temp_control:"Controllo temperatura",show_color_control:"Controllo colore",incompatible_controls:"Alcuni controlli potrebbero non essere mostrati se la tua luce non li supporta."},fan:{show_percentage_control:"Controllo potenza",show_oscillate_control:"Controllo oscillazione"},cover:{show_buttons_control:"Pulsanti di controllo",show_position_control:"Controllo percentuale apertura"},alarm_control_panel:{show_keypad:"Mostra il tastierino numerico"},template:{primary:"Informazione primaria",secondary:"Informazione secondaria",multiline_secondary:"Abilita frasi multilinea",entity_extra:"Usato in templates ed azioni",content:"Contenuto",badge_icon:"Icona del badge",badge_color:"Colore del badge",picture:"Immagine (sostituirà l'icona)"},title:{title:"Titolo",subtitle:"Sottotitolo"},chips:{alignment:"Allineamento"},weather:{show_conditions:"Condizioni",show_temperature:"Temperatura"},update:{show_buttons_control:"Pulsanti di controllo"},vacuum:{commands:"Comandi"},"media-player":{use_media_info:"Mostra le Informazioni Sorgente",use_media_artwork:"Usa la copertina della Sorgente",show_volume_level:"Mostra Volume",media_controls:"Controlli Media",media_controls_list:{on_off:"Accendi/Spegni",shuffle:"Riproduzione Casuale",previous:"Traccia Precedente",play_pause_stop:"Play/Pausa/Stop",next:"Traccia Successiva",repeat:"Loop"},volume_controls:"Controlli del Volume",volume_controls_list:{volume_buttons:"Bottoni del Volume",volume_set:"Livello del Volume",volume_mute:"Silenzia"}},lock:{lock:"Blocca",unlock:"Sblocca",open:"Aperto"},humidifier:{show_target_humidity_control:"Controllo umidità"},climate:{show_temperature_control:"Controllo della temperatura?",hvac_modes:"Modalità del termostato"}},chip:{sub_element_editor:{title:"Editor di chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Aggiungi chip",edit:"Modifica",clear:"Rimuovi",select:"Seleziona chip",types:{action:"Azione","alarm-control-panel":"Allarme",back:"Pulsante indietro",conditional:"Condizione",entity:"Entità",light:"Luce",menu:"Menù",template:"Template",weather:"Meteo"}}}},_i={editor:gi},vi={form:{color_picker:{values:{default:"Standard farge"}},info_picker:{values:{default:"Standard informasjon",name:"Navn",state:"Tilstand","last-changed":"Sist endret","last-updated":"Sist oppdatert",none:"Ingen"}},layout_picker:{values:{default:"Standardoppsett",vertical:"Vertikalt oppsett",horizontal:"Horisontalt oppsett"}},alignment_picker:{values:{default:"Standard justering",start:"Start",end:"Slutt",center:"Senter",justify:"Bekreft"}}},card:{generic:{icon_color:"Ikon farge",layout:"Oppsett",primary_info:"Primærinformasjon",secondary_info:"Sekundærinformasjon",content_info:"Innhold",use_entity_picture:"Bruk enhetsbilde?",icon_animation:"Animer ikon når aktivt?"},light:{show_brightness_control:"Lysstyrkekontroll?",use_light_color:"Bruk lys farge",show_color_temp_control:"Temperatur fargekontroll?",show_color_control:"Fargekontroll?",incompatible_controls:"Noen kontroller vises kanskje ikke hvis lyset ditt ikke støtter denne funksjonen."},fan:{show_percentage_control:"Prosentvis kontroll?",show_oscillate_control:"Oscillerende kontroll?"},cover:{show_buttons_control:"Kontollere med knapper?",show_position_control:"Posisjonskontroll?"},template:{primary:"Primærinformasjon",secondary:"Sekundærinformasjon",multiline_secondary:"Multiline sekundær?",entity_extra:"Brukes i maler og handlinger",content:"Inhold"},title:{title:"Tittel",subtitle:"Undertekst"},chips:{alignment:"Justering"},weather:{show_conditions:"Forhold?",show_temperature:"Temperatur?"},vacuum:{commands:"Kommandoer"}},chip:{sub_element_editor:{title:"Chip redaktør"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Legg til chip",edit:"Endre",clear:"Klare",select:"Velg chip",types:{action:"Handling","alarm-control-panel":"Alarm",back:"Tilbake",conditional:"Betinget",entity:"Entitet",light:"Lys",menu:"Meny",template:"Mal",weather:"Vær"}}}},bi={editor:vi},yi={form:{color_picker:{values:{default:"Standaard kleur"}},info_picker:{values:{default:"Standaard informatie",name:"Naam",state:"Staat","last-changed":"Laatst gewijzigd","last-updated":"Laatst bijgewerkt",none:"Geen"}},layout_picker:{values:{default:"Standaard lay-out",vertical:"Verticale lay-out",horizontal:"Horizontale lay-out"}},alignment_picker:{values:{default:"Standaard uitlijning",start:"Begin",end:"Einde",center:"Midden",justify:"Uitlijnen "}}},card:{generic:{icon_color:"Icoon kleur",layout:"Lay-out",primary_info:"Primaire informatie",secondary_info:"Secundaire informatie",content_info:"Inhoud",use_entity_picture:"Gebruik entiteit afbeelding",collapsible_controls:"Bedieningselementen verbergen wanneer uitgeschakeld",icon_animation:"Pictogram animeren indien actief"},light:{show_brightness_control:"Bediening helderheid",use_light_color:"Gebruik licht kleur",show_color_temp_control:"Bediening kleurtemperatuur",show_color_control:"Bediening kleur",incompatible_controls:"Sommige bedieningselementen worden mogelijk niet weergegeven als uw lamp deze functie niet ondersteunt."},fan:{show_percentage_control:"Bediening middels percentage",show_oscillate_control:"Bediening oscillatie"},cover:{show_buttons_control:"Toon knoppen",show_position_control:"Toon positie bediening"},alarm_control_panel:{show_keypad:"Toon toetsenbord"},template:{primary:"Primaire informatie",secondary:"Secundaire informatie",multiline_secondary:"Meerlijnig secundair?",entity_extra:"Gebruikt in sjablonen en acties",content:"Inhoud"},title:{title:"Titel",subtitle:"Ondertitel"},chips:{alignment:"Uitlijning"},weather:{show_conditions:"Weerbeeld",show_temperature:"Temperatuur"},update:{show_buttons_control:"Bedieningsknoppen?"},vacuum:{commands:"Commando's",commands_list:{on_off:"Zet aan/uit"}},"media-player":{use_media_info:"Gebruik media informatie",use_media_artwork:"Gebruik media omslag",show_volume_level:"Toon volumeniveau",media_controls:"Mediabediening",media_controls_list:{on_off:"zet aan/uit",shuffle:"Shuffle",previous:"Vorige nummer",play_pause_stop:"Speel/pauze/stop",next:"Volgende nummer",repeat:"Herhalen"},volume_controls:"Volumeregeling",volume_controls_list:{volume_buttons:"Volume knoppen",volume_set:"Volumeniveau",volume_mute:"Dempen"}},lock:{lock:"Vergrendel",unlock:"Ontgrendel",open:"Open"}},chip:{sub_element_editor:{title:"Chip-editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Toevoegen chip",edit:"Bewerk",clear:"Maak leeg",select:"Selecteer chip",types:{action:"Actie","alarm-control-panel":"Alarm",back:"Terug",conditional:"Voorwaardelijk",entity:"Entiteit",light:"Licht",menu:"Menu",template:"Sjabloon",weather:"Weer"}}}},xi={editor:yi},wi={form:{color_picker:{values:{default:"Domyślny kolor"}},info_picker:{values:{default:"Domyślne informacje",name:"Nazwa",state:"Stan","last-changed":"Ostatnia zmiana","last-updated":"Ostatnia aktualizacja",none:"Brak"}},icon_type_picker:{values:{default:"Domyślny typ",icon:"Ikona","entity-picture":"Obraz encji",none:"Brak"}},layout_picker:{values:{default:"Układ domyślny",vertical:"Układ pionowy",horizontal:"Układ poziomy"}},alignment_picker:{values:{default:"Wyrównanie domyślne",start:"Wyrównanie do lewej",end:"Wyrównanie do prawej",center:"Wyśrodkowanie",justify:"Justowanie"}}},card:{generic:{icon_color:"Kolor ikony",layout:"Układ",fill_container:"Wypełnij zawartością",primary_info:"Informacje główne",secondary_info:"Informacje drugorzędne",icon_type:"Typ ikony",content_info:"Zawartość",use_entity_picture:"Użyć obrazu encji?",collapsible_controls:"Zwiń sterowanie, jeśli wyłączone",icon_animation:"Animować, gdy aktywny?"},light:{show_brightness_control:"Sterowanie jasnością?",use_light_color:"Użyj koloru światła",show_color_temp_control:"Sterowanie temperaturą światła?",show_color_control:"Sterowanie kolorami?",incompatible_controls:"Niektóre funkcje są niewidoczne, jeśli światło ich nie obsługuje."},fan:{show_percentage_control:"Sterowanie procentowe?",show_oscillate_control:"Sterowanie oscylacją?"},cover:{show_buttons_control:"Przyciski sterujące?",show_position_control:"Sterowanie położeniem?",show_tilt_position_control:"Sterowanie poziomem otwarcia?"},alarm_control_panel:{show_keypad:"Wyświetl klawiaturę"},template:{primary:"Informacje główne",secondary:"Informacje drugorzędne",multiline_secondary:"Drugorzędne wielowierszowe?",entity_extra:"Używane w szablonach i akcjach",content:"Zawartość",badge_icon:"Ikona odznaki",badge_color:"Kolor odznaki",picture:"Obraz (zamiast ikony)"},title:{title:"Tytuł",subtitle:"Podtytuł"},chips:{alignment:"Wyrównanie"},weather:{show_conditions:"Warunki?",show_temperature:"Temperatura?"},update:{show_buttons_control:"Przyciski sterujące?"},vacuum:{commands:"Polecenia"},"media-player":{use_media_info:"Użyj informacji o multimediach",use_media_artwork:"Użyj okładek multimediów",show_volume_level:"Wyświetl poziom głośności",media_controls:"Sterowanie multimediami",media_controls_list:{on_off:"Włącz/wyłącz",shuffle:"Losowo",previous:"Poprzednie nagranie",play_pause_stop:"Odtwórz/Pauza/Zatrzymaj",next:"Następne nagranie",repeat:"Powtarzanie"},volume_controls:"Sterowanie głośnością",volume_controls_list:{volume_buttons:"Przyciski głośności",volume_set:"Poziom głośności",volume_mute:"Wycisz"}},lock:{lock:"Zablokuj",unlock:"Odblokuj",open:"Otwórz"},humidifier:{show_target_humidity_control:"Sterowanie wilgotnością?"},climate:{show_temperature_control:"Sterowanie temperaturą?",hvac_modes:"Tryby urządzenia"}},chip:{sub_element_editor:{title:"Edytor czipów"},conditional:{chip:"Czip"},"chip-picker":{chips:"Czipy",add:"Dodaj czip",edit:"Edytuj",clear:"Wyczyść",select:"Wybierz czip",types:{action:"Akcja","alarm-control-panel":"Alarm",back:"Wstecz",conditional:"Warunkowy",entity:"Encja",light:"Światło",menu:"Menu",template:"Szablon",weather:"Pogoda"}}}},ki={editor:wi},Ci={form:{color_picker:{values:{default:"Cor padrão"}},info_picker:{values:{default:"Informações padrão",name:"Nome",state:"Estado","last-changed":"Última alteração","last-updated":"Última atualização",none:"Nenhum"}},layout_picker:{values:{default:"Layout padrão",vertical:"Layout vertical",horizontal:"Layout horizontal"}},alignment_picker:{values:{default:"Padrão (inicio)",end:"Final",center:"Centro",justify:"Justificado"}}},card:{generic:{icon_color:"Cor do ícone?",layout:"Layout",primary_info:"Informações primárias",secondary_info:"Informações secundárias",use_entity_picture:"Usar imagem da entidade?",icon_animation:"Animar ícone quando ativo?"},light:{show_brightness_control:"Mostrar controle de brilho?",use_light_color:"Usar cor da luz?",show_color_temp_control:"Mostrar controle de temperatura?",show_color_control:"Mostrar controle de cor?",incompatible_controls:"Alguns controles podem não ser exibidos se sua luz não suportar o recurso."},fan:{show_percentage_control:"Mostrar controle de porcentagem?",show_oscillate_control:"Mostrar controle de oscilação?"},cover:{show_buttons_control:"Mostrar botões?",show_position_control:"Mostrar controle de posição?"},template:{primary:"Informações primárias",secondary:"Informações secundárias",multiline_secondary:"Multilinha secundária?",content:"Conteúdo"},title:{title:"Título",subtitle:"Subtítulo"},chips:{alignment:"Alinhamento"},weather:{show_conditions:"Condições?",show_temperature:"Temperatura?"}},chip:{sub_element_editor:{title:"Editor de fichas"},conditional:{chip:"Ficha"},"chip-picker":{chips:"Fichas",add:"Adicionar ficha",edit:"Editar",clear:"Limpar",select:"Selecionar ficha",types:{action:"Ação","alarm-control-panel":"Alarme",back:"Voltar",conditional:"Condicional",entity:"Entidade",light:"Iluminação",menu:"Menu",template:"Modelo",weather:"Clima"}}}},$i={editor:Ci},Ei={form:{color_picker:{values:{default:"Cor padrão"}},info_picker:{values:{default:"Informações padrão",name:"Nome",state:"Estado","last-changed":"Última alteração","last-updated":"Última atualização",none:"Nenhum"}},layout_picker:{values:{default:"Layout padrão",vertical:"Layout vertical",horizontal:"Layout horizontal"}},alignment_picker:{values:{default:"Padrão (inicio)",end:"Fim",center:"Centrado",justify:"Justificado"}}},card:{generic:{icon_color:"Cor do ícone?",layout:"Layout",primary_info:"Informações primárias",secondary_info:"Informações secundárias",use_entity_picture:"Usar imagem da entidade?",icon_animation:"Animar ícone quando ativo?"},light:{show_brightness_control:"Mostrar controle de brilho?",use_light_color:"Usar cor da luz?",show_color_temp_control:"Mostrar controle de temperatura?",show_color_control:"Mostrar controle de cor?",incompatible_controls:"Alguns controles podem não ser exibidos se a luz não suportar o recurso."},fan:{show_percentage_control:"Mostrar controle de porcentagem?",show_oscillate_control:"Mostrar controle de oscilação?"},cover:{show_buttons_control:"Mostrar botões?",show_position_control:"Mostrar controle de posição?"},template:{primary:"Informações primárias",secondary:"Informações secundárias",multiline_secondary:"Multilinha secundária?",content:"Conteúdo"},title:{title:"Título",subtitle:"Subtítulo"},chips:{alignment:"Alinhamento"},weather:{show_conditions:"Condições?",show_temperature:"Temperatura?"}},chip:{sub_element_editor:{title:"Editor de fichas"},conditional:{chip:"Ficha"},"chip-picker":{chips:"Fichas",add:"Adicionar ficha",edit:"Editar",clear:"Limpar",select:"Selecionar ficha",types:{action:"Ação","alarm-control-panel":"Alarme",back:"Voltar",conditional:"Condicional",entity:"Entidade",light:"Iluminação",menu:"Menu",template:"Modelo",weather:"Clima"}}}},Ai={editor:Ei},Si={form:{color_picker:{values:{default:"Culoare implicită"}},info_picker:{values:{default:"Informație implicită",name:"Nume",state:"Stare","last-changed":"Ultima modificare","last-updated":"Ultima actulizare",none:"Niciuna"}},icon_type_picker:{values:{default:"Tip implicit",icon:"Pictogramă","entity-picture":"Imagine",none:"Niciuna"}},layout_picker:{values:{default:"Aranjare implicită",vertical:"Verticală",horizontal:"Orizontală"}},alignment_picker:{values:{default:"Aliniere implicită",start:"Stânga",end:"Dreapta",center:"Centrat",justify:"Umplere"}}},card:{generic:{icon_color:"Culoare pictogramă",layout:"Aranjare",fill_container:"Umplere container",primary_info:"Informație principală",secondary_info:"Informație secundară",icon_type:"Tip pictogramă",content_info:"Conținut",use_entity_picture:"Imagine?",collapsible_controls:"Restrângere la dezactivare"},light:{show_brightness_control:"Comandă pentru strălucire?",use_light_color:"Folosește culoarea luminii",show_color_temp_control:"Comandă pentru temperatură de culoare?",show_color_control:"Comandă pentru culoare?",incompatible_controls:"Unele comenzi ar putea să nu fie afișate dacă lumina nu suportă această caracteristică."},fan:{icon_animation:"Animare pictograma la activare?",show_percentage_control:"Comandă procent?",show_oscillate_control:"Comandă oscilație?"},cover:{show_buttons_control:"Comenzi pentru control?",show_position_control:"Comandă pentru poziție?",show_tilt_position_control:"Comandă pentru înclinare?"},alarm_control_panel:{show_keypad:"Arată tastatura"},template:{primary:"Informație principală",secondary:"Informație secundară",multiline_secondary:"Informație secundară pe mai multe linii?",entity_extra:"Folosită în șabloane și acțiuni",content:"Conținut",badge_icon:"Pictogramă insignă",badge_color:"Culoare insignă",picture:"Imagine (inlocuiește pictograma)"},title:{title:"Titlu",subtitle:"Subtitlu"},chips:{alignment:"Aliniere"},weather:{show_conditions:"Condiții?",show_temperature:"Temperatură?"},update:{show_buttons_control:"Comenzi control?"},vacuum:{commands:"Comenzi"},"media-player":{use_media_info:"Informații media",use_media_artwork:"Grafică media",show_volume_level:"Nivel volum",media_controls:"Comenzi media",media_controls_list:{on_off:"Pornit/Oprit",shuffle:"Amestecare",previous:"Pista anterioară",play_pause_stop:"Redare/Pauză/Stop",next:"Pista următoare",repeat:"Mod repetare"},volume_controls:"Comenzi volum",volume_controls_list:{volume_buttons:"Comenzi volum",volume_set:"Nivel volum",volume_mute:"Dezactivare sunet"}},lock:{lock:"Încuie",unlock:"Descuie",open:"Deschide"},humidifier:{show_target_humidity_control:"Comenzi umiditate?"},climate:{show_temperature_control:"Comenzi temperatură?",hvac_modes:"Moduri HVAC"}},chip:{sub_element_editor:{title:"Editor jeton"},conditional:{chip:"Jeton"},"chip-picker":{chips:"Jetoane",add:"Adaugă jeton",edit:"Modifică",clear:"Șterge",select:"Alege jeton",types:{action:"Acțiune","alarm-control-panel":"Alarmă",back:"Înapoi",conditional:"Condițional",entity:"Entitate",light:"Lumină",menu:"Meniu",template:"Șablon",weather:"Vreme"}}}},Ii={editor:Si},Ti={form:{color_picker:{values:{default:"Predvolená farba"}},info_picker:{values:{default:"Predvolené informácie",name:"Názov",state:"Stav","last-changed":"Posledná zmena","last-updated":"Posledná aktualizácia",none:"Žiadna"}},icon_type_picker:{values:{default:"Predvolený typ",icon:"Ikona","entity-picture":"Obrázok entity",none:"Žiadny"}},layout_picker:{values:{default:"Predvolené rozloženie",vertical:"Zvislé rozloženie",horizontal:"Vodorovné rozloženie"}},alignment_picker:{values:{default:"Predvolené zarovnanie",start:"Začiatok",end:"Koniec",center:"Stred",justify:"Vyplniť"}}},card:{generic:{icon_color:"Farba ikony",layout:"Rozloženie",fill_container:"Vyplniť priestor",primary_info:"Základné info",secondary_info:"Doplnkové info",icon_type:"Typ ikony",content_info:"Obsah",use_entity_picture:"Použiť obrázok entity?",collapsible_controls:"Skryť ovládanie v stave VYP.",icon_animation:"Animovaná ikona v stave ZAP?"},light:{show_brightness_control:"Ovládanie jasu?",use_light_color:"Použiť farbu svetla",show_color_temp_control:"Ovládanie teploty?",show_color_control:"Ovládanie farby?",incompatible_controls:"Niektoré ovládacie prvky sa nemusia zobraziť, pokiaľ ich svetlo nepodporuje."},fan:{show_percentage_control:"Ovládanie rýchlosti v percentách?",show_oscillate_control:"Ovládanie oscilácie?"},cover:{show_buttons_control:"Zobraziť ovládacie tlačidlá?",show_position_control:"Ovládanie pozície?",show_tilt_position_control:"Ovládanie natočenia?"},alarm_control_panel:{show_keypad:"Zobraziť klávesnicu"},template:{primary:"Základné info",secondary:"Doplnkové info",multiline_secondary:"Viacriadkové doplnkové info?",entity_extra:"Použitá v šablónach a akciách",content:"Obsah",badge_icon:"Ikona odznaku",badge_color:"Farba odznaku",picture:"Obrázok (nahrádza ikonu)"},title:{title:"Nadpis",subtitle:"Podnadpis"},chips:{alignment:"Zarovnanie"},weather:{show_conditions:"Zobraziť podmienky?",show_temperature:"Zobraziť teplotu?"},update:{show_buttons_control:"Zobraziť ovládacie tlačidlá?"},vacuum:{commands:"Príkazy"},"media-player":{use_media_info:"Použiť info o médiu",use_media_artwork:"Použiť obrázok z média",show_volume_level:"Zobraziť úroveň hlasitosti",media_controls:"Ovládanie média",media_controls_list:{on_off:"Zap / Vyp",shuffle:"Premiešať",previous:"Predchádzajúca",play_pause_stop:"Spustiť/pauza/stop",next:"Ďalšia",repeat:"Opakovať"},volume_controls:"Ovládanie hlasitosti",volume_controls_list:{volume_buttons:"Tlačidlá hlasitosti",volume_set:"Úroveň hlasitosti",volume_mute:"Stlmiť"}},lock:{lock:"Zamknuté",unlock:"Odomknuté",open:"Otvorené"},humidifier:{show_target_humidity_control:"Ovládanie vlhkosti?"},climate:{show_temperature_control:"Ovládanie teploty?",hvac_modes:"HVAC mód"}},chip:{sub_element_editor:{title:"Editor štítkov"},conditional:{chip:"Štítok"},"chip-picker":{chips:"Štítky",add:"Pridať štítok",edit:"Editovať",clear:"Vymazať",select:"Vybrať štítok",types:{action:"Akcia","alarm-control-panel":"Alarm",back:"Späť",conditional:"Podmienka",entity:"Entita",light:"Svetlo",menu:"Menu",template:"Šablóna",weather:"Počasie"}}}},zi={editor:Ti},Oi={form:{color_picker:{values:{default:"Standardfärg"}},info_picker:{values:{default:"Förvald information",name:"Namn",state:"Status","last-changed":"Sist ändrad","last-updated":"Sist uppdaterad",none:"Ingen"}},layout_picker:{values:{default:"Standard",vertical:"Vertikal",horizontal:"Horisontell"}},alignment_picker:{values:{default:"Standard (början)",end:"Slutet",center:"Centrerad",justify:"Anpassa"}}},card:{generic:{icon_color:"Ikonens färg",layout:"Layout",primary_info:"Primär information",secondary_info:"Sekundär information",use_entity_picture:"Använd enheten bild?",icon_animation:"Animera ikonen när fläkten är på?"},light:{show_brightness_control:"Styr ljushet?",use_light_color:"Styr ljusets färg",show_color_temp_control:"Styr färgtemperatur?",show_color_control:"Styr färg?",incompatible_controls:"Kontroller som inte stöds av enheten kommer inte visas."},fan:{show_percentage_control:"Procentuell kontroll?",show_oscillate_control:"Kontroll för oscillera?"},cover:{show_buttons_control:"Visa kontrollknappar?",show_position_control:"Visa positionskontroll?"},template:{primary:"Primär information",secondary:"Sekundär information",multiline_secondary:"Sekundär med flera rader?",content:"Innehåll"},title:{title:"Rubrik",subtitle:"Underrubrik"},chips:{alignment:"Justering"},weather:{show_conditions:"Förhållanden?",show_temperature:"Temperatur?"}},chip:{sub_element_editor:{title:"Chipredigerare"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Lägg till chip",edit:"Redigera",clear:"Rensa",select:"Välj chip",types:{action:"Händelse","alarm-control-panel":"Alarm",back:"Bakåt",conditional:"Villkorad",entity:"Enhet",light:"Ljus",menu:"Meny",template:"Mall",weather:"Väder"}}}},Mi={editor:Oi},Li={form:{color_picker:{values:{default:"Varsayılan renk"}},info_picker:{values:{default:"Varsayılan bilgi",name:"İsim",state:"Durum","last-changed":"Son Değişim","last-updated":"Son Güncelleme",none:"None"}},layout_picker:{values:{default:"Varsayılan düzen",vertical:"Dikey düzen",horizontal:"Yatay düzen"}},alignment_picker:{values:{default:"Varsayılan hizalama",start:"Sola yasla",end:"Sağa yasla",center:"Ortala",justify:"İki yana yasla"}}},card:{generic:{icon_color:"Simge renki",layout:"Düzen",primary_info:"Birinci bilgi",secondary_info:"İkinci bilgi",content_info:"İçerik",use_entity_picture:"Varlık resmi kullanılsın",icon_animation:"Aktif olduğunda simgeyi hareket ettir"},light:{show_brightness_control:"Parlaklık kontrolü",use_light_color:"Işık rengini kullan",show_color_temp_control:"Renk ısısı kontrolü",show_color_control:"Renk kontrolü",incompatible_controls:"Kullandığınız lamba bu özellikleri desteklemiyorsa bazı kontroller görüntülenemeyebilir."},fan:{show_percentage_control:"Yüzde kontrolü",show_oscillate_control:"Salınım kontrolü"},cover:{show_buttons_control:"Düğme kontrolleri",show_position_control:"Pozisyon kontrolü"},template:{primary:"Birinci bilgi",secondary:"İkinci bilgi",multiline_secondary:"İkinci bilgi çok satır olsun",entity_extra:"Şablonlarda ve eylemlerde kullanılsın",content:"İçerik"},title:{title:"Başlık",subtitle:"Altbaşlık"},chips:{alignment:"Hizalama"},weather:{show_conditions:"Hava koşulu",show_temperature:"Sıcaklık"},update:{show_buttons_control:"Düğme kontrolü"},vacuum:{commands:"Komutlar"}},chip:{sub_element_editor:{title:"Chip düzenleyici"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Chip ekle",edit:"Düzenle",clear:"Temizle",select:"Chip seç",types:{action:"Eylem","alarm-control-panel":"Alarm",back:"Geri",conditional:"Koşullu",entity:"Varlık",light:"Işık",menu:"Menü",template:"Şablon",weather:"Hava Durumu"}}}},Di={editor:Li},ji={form:{color_picker:{values:{default:"Màu mặc định"}},info_picker:{values:{default:"Thông tin mặc định",name:"Tên",state:"Trạng thái","last-changed":"Lần cuối thay đổi","last-updated":"Lần cuối cập nhật",none:"Rỗng"}},layout_picker:{values:{default:"Bố cục mặc định",vertical:"Bố cục dọc",horizontal:"Bố cục ngang"}},alignment_picker:{values:{default:"Căn chỉnh mặc định",start:"Căn đầu",end:"Căn cuối",center:"Căn giữa",justify:"Căn hai bên"}}},card:{generic:{icon_color:"Màu biểu tượng",layout:"Bố cục",fill_container:"Làm đầy",primary_info:"Thông tin chính",secondary_info:"Thông tin phụ",content_info:"Nội dung",use_entity_picture:"Dùng ảnh của thực thể?",collapsible_controls:"Thu nhỏ điều kiển khi tắt",icon_animation:"Biểu tượng hoạt ảnh khi hoạt động?"},light:{show_brightness_control:"Điều khiển độ sáng?",use_light_color:"Dùng ánh sáng màu",show_color_temp_control:"Điều khiển nhiệt độ màu?",show_color_control:"Điều khiển màu sắc?",incompatible_controls:"Một số màu sẽ không được hiển thị nếu đèn của bạn không hỗ trợ tính năng này."},fan:{show_percentage_control:"Điều khiển dạng phần trăm?",show_oscillate_control:"Điều khiển xoay?"},cover:{show_buttons_control:"Nút điều khiển?",show_position_control:"Điều khiển vị trí?"},alarm_control_panel:{show_keypad:"Hiện bàn phím"},template:{primary:"Thông tin chính",secondary:"Thông tin phụ",multiline_secondary:"Nhiều dòng thông tin phụ?",entity_extra:"Được sử dụng trong mẫu và hành động",content:"Nội dung"},title:{title:"Tiêu đề",subtitle:"Phụ đề"},chips:{alignment:"Căn chỉnh"},weather:{show_conditions:"Điều kiện?",show_temperature:"Nhiệt độ?"},update:{show_buttons_control:"Nút điều khiển?"},vacuum:{commands:"Mệnh lệnh"},"media-player":{use_media_info:"Dùng thông tin đa phương tiện",use_media_artwork:"Dùng ảnh đa phương tiện",media_controls:"Điều khiển đa phương tiện",media_controls_list:{on_off:"Bật/Tắt",shuffle:"Xáo trộn",previous:"Bài trước",play_pause_stop:"Phát/Tạm dừng/Dừng",next:"Bài tiếp theo",repeat:"Chế độ lặp lại"},volume_controls:"Điều khiển âm lượng",volume_controls_list:{volume_buttons:"Nút âm lượng",volume_set:"Mức âm lượng",volume_mute:"Im lặng"}},lock:{lock:"Khóa",unlock:"Mở khóa",open:"Mở"}},chip:{sub_element_editor:{title:"Chỉnh sửa chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Các chip",add:"Thêm chip",edit:"Chỉnh sửa",clear:"Làm mới",select:"Chọn chip",types:{action:"Hành động","alarm-control-panel":"Báo động",back:"Quay về",conditional:"Điều kiện",entity:"Thực thể",light:"Đèn",menu:"Menu",template:"Mẫu",weather:"Thời tiết"}}}},Pi={editor:ji},Ni={form:{color_picker:{values:{default:"默认颜色"}},info_picker:{values:{default:"默认信息",name:"名称",state:"状态","last-changed":"变更时间","last-updated":"更新时间",none:"无"}},layout_picker:{values:{default:"默认布局",vertical:"垂直布局",horizontal:"水平布局"}},alignment_picker:{values:{default:"默认 (左对齐)",end:"右对齐",center:"居中对齐",justify:"两端对齐"}}},card:{generic:{icon_color:"图标颜色",primary_info:"首要信息",secondary_info:"次要信息",use_entity_picture:"使用实体图片?",icon_animation:"激活时使用动态图标?"},light:{show_brightness_control:"亮度控制?",use_light_color:"使用灯光颜色",show_color_temp_control:"色温控制?",show_color_control:"颜色控制?",incompatible_controls:"设备不支持的控制器将不会显示。"},fan:{show_percentage_control:"百分比控制?",show_oscillate_control:"摆动控制?"},cover:{show_buttons_control:"按钮控制?",show_position_control:"位置控制?"},template:{primary:"首要信息",secondary:"次要信息",multiline_secondary:"多行次要信息?",content:"内容"},title:{title:"标题",subtitle:"子标题"},chips:{alignment:"对齐"},weather:{show_conditions:"条件?",show_temperature:"温度?"}},chip:{sub_element_editor:{title:"Chip 编辑"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"添加 chip",edit:"编辑",clear:"清除",select:"选择 chip",types:{action:"动作","alarm-control-panel":"警戒控制台",back:"返回",conditional:"条件显示",entity:"实体",light:"灯光",menu:"菜单",template:"模板",weather:"天气"}}}},Vi={editor:Ni},Ri={form:{color_picker:{values:{default:"預設顏色"}},info_picker:{values:{default:"預設訊息",name:"名稱",state:"狀態","last-changed":"最近變動時間","last-updated":"最近更新時間",none:"無"}},icon_type_picker:{values:{default:"預設樣式",icon:"圖示","entity-picture":"實體圖片",none:"無"}},layout_picker:{values:{default:"預設佈局",vertical:"垂直佈局",horizontal:"水平佈局"}},alignment_picker:{values:{default:"預設對齊",start:"居左對齊",end:"居右對齊",center:"居中對齊",justify:"兩端對齊"}}},card:{generic:{icon_color:"圖示顏色",layout:"佈局",fill_container:"填滿容器",primary_info:"主要訊息",secondary_info:"次要訊息",icon_type:"圖示樣式",content_info:"內容",use_entity_picture:"使用實體圖片?",collapsible_controls:"關閉時隱藏控制項",icon_animation:"啟動時使用動態圖示?"},light:{show_brightness_control:"亮度控制?",use_light_color:"使用燈光顏色",show_color_temp_control:"色溫控制?",show_color_control:"色彩控制?",incompatible_controls:"裝置不支援的控制不會顯示。"},fan:{show_percentage_control:"百分比控制?",show_oscillate_control:"擺頭控制?"},cover:{show_buttons_control:"按鈕控制?",show_position_control:"位置控制?",show_tilt_position_control:"傾斜控制?"},alarm_control_panel:{show_keypad:"顯示鍵盤"},template:{primary:"主要訊息",secondary:"次要訊息",multiline_secondary:"多行次要訊息?",entity_extra:"用於模板與動作",content:"內容",badge_icon:"角標圖示",badge_color:"角標顏色",picture:"圖片(將會取代圖示)"},title:{title:"標題",subtitle:"副標題"},chips:{alignment:"對齊"},weather:{show_conditions:"狀況?",show_temperature:"溫度?"},update:{show_buttons_control:"按鈕控制?"},vacuum:{commands:"指令"},"media-player":{use_media_info:"使用媒體資訊",use_media_artwork:"使用媒體插圖",show_volume_level:"顯示音量大小",media_controls:"媒體控制",media_controls_list:{on_off:" 開啟、關閉",shuffle:"隨機播放",previous:"上一首",play_pause_stop:"播放、暫停、停止",next:"下一首",repeat:"重複播放"},volume_controls:"音量控制",volume_controls_list:{volume_buttons:"音量按鈕",volume_set:"音量等級",volume_mute:"靜音"}},lock:{lock:"上鎖",unlock:"解鎖",open:"打開"},humidifier:{show_target_humidity_control:"溼度控制?"},climate:{show_temperature_control:"溫度控制?",hvac_modes:"空調模式"}},chip:{sub_element_editor:{title:"Chip 編輯"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"新增 chip",edit:"編輯",clear:"清除",select:"選擇 chip",types:{action:"動作","alarm-control-panel":"警報器控制",back:"返回",conditional:"條件",entity:"實體",light:"燈光",menu:"選單",template:"模板",weather:"天氣"}}}},Fi={editor:Ri};const Bi={ar:Object.freeze({__proto__:null,editor:qe,default:Ke}),cs:Object.freeze({__proto__:null,editor:Ge,default:Ze}),da:Object.freeze({__proto__:null,editor:Je,default:Qe}),de:Object.freeze({__proto__:null,editor:ti,default:ei}),el:Object.freeze({__proto__:null,editor:ii,default:ni}),en:Object.freeze({__proto__:null,editor:oi,default:ri}),es:Object.freeze({__proto__:null,editor:ai,default:li}),fi:Object.freeze({__proto__:null,editor:si,default:ci}),fr:Object.freeze({__proto__:null,editor:di,default:ui}),he:Object.freeze({__proto__:null,editor:hi,default:mi}),hu:Object.freeze({__proto__:null,editor:pi,default:fi}),it:Object.freeze({__proto__:null,editor:gi,default:_i}),nb:Object.freeze({__proto__:null,editor:vi,default:bi}),nl:Object.freeze({__proto__:null,editor:yi,default:xi}),pl:Object.freeze({__proto__:null,editor:wi,default:ki}),"pt-BR":Object.freeze({__proto__:null,editor:Ci,default:$i}),"pt-PT":Object.freeze({__proto__:null,editor:Ei,default:Ai}),ro:Object.freeze({__proto__:null,editor:Si,default:Ii}),sk:Object.freeze({__proto__:null,editor:Ti,default:zi}),sv:Object.freeze({__proto__:null,editor:Oi,default:Mi}),tr:Object.freeze({__proto__:null,editor:Li,default:Di}),vi:Object.freeze({__proto__:null,editor:ji,default:Pi}),"zh-Hans":Object.freeze({__proto__:null,editor:Ni,default:Vi}),"zh-Hant":Object.freeze({__proto__:null,editor:Ri,default:Fi})};function Ui(t,e){try{return t.split(".").reduce(((t,e)=>t[e]),Bi[e])}catch(t){return}}function Hi(t){return function(e){var i;let n=Ui(e,null!==(i=null==t?void 0:t.locale.language)&&void 0!==i?i:"en");return n||(n=Ui(e,"en")),null!=n?n:e}} +`;var ii={form:{color_picker:{values:{default:"اللون الإفتراضي"}},info_picker:{values:{default:"المعلومات الافتراضية",name:"الإسم",state:"الحالة","last-changed":"آخر تغيير","last-updated":"آخر تحديث",none:"لا شئ"}},icon_type_picker:{values:{default:"النوع افتراضي",icon:"أيقونة","entity-picture":"صورة الكيان",none:"لا شئ"}},layout_picker:{values:{default:"تخطيط افتراضي",vertical:"تخطيط رأسي",horizontal:"تخطيط أفقي"}},alignment_picker:{values:{default:"المحاذاة الافتراضية",start:"بداية",end:"نهاية",center:"توسيط",justify:"مساواة"}}},card:{generic:{icon_color:"لون الأيقونة",layout:"التخطيط",fill_container:"ملئ الحاوية",primary_info:"المعلومات الأساسية",secondary_info:"المعلومات الفرعية",icon_type:"نوع الأيقونة",content_info:"المحتوى",use_entity_picture:"استخدم صورة الكيان؟",collapsible_controls:"تصغير عناصر التحكم عند الإيقاف",icon_animation:"تحريك الرمز عندما يكون نشطًا؟"},light:{show_brightness_control:"التحكم في السطوع؟",use_light_color:"استخدم لون فاتح",show_color_temp_control:"التحكم في حرارة اللون؟",show_color_control:"التحكم في اللون؟",incompatible_controls:"قد لا يتم عرض بعض عناصر التحكم إذا كان الضوء الخاص بك لا يدعم الميزة."},fan:{show_percentage_control:"التحكم في النسبة المئوية؟",show_oscillate_control:"التحكم في التذبذب؟"},cover:{show_buttons_control:"أزرار التحكم؟",show_position_control:"التحكم في الموقع؟"},alarm_control_panel:{show_keypad:"إظهار لوحة المفاتيح"},template:{primary:"المعلومات الأساسية",secondary:"المعلومات الثانوية",multiline_secondary:"متعدد الأسطر الثانوية؟",entity_extra:"تستخدم في القوالب والإجراءات",content:"المحتوى",badge_icon:"أيقونة الشارة",badge_color:"لون الشارة",picture:"صورة (ستحل محل الأيقونة)"},title:{title:"العنوان",subtitle:"العنوان الفرعي"},chips:{alignment:"محاذاة"},weather:{show_conditions:"الأحوال الجوية؟",show_temperature:"الطقس؟"},update:{show_buttons_control:"أزرار التحكم؟"},vacuum:{commands:"الاوامر"},"media-player":{use_media_info:"استخدم معلومات الوسائط",use_media_artwork:"استخدم صورة الوسائط",show_volume_level:"إظهار مستوى الصوت",media_controls:"التحكم في الوسائط",media_controls_list:{on_off:"تشغيل/إيقاف",shuffle:"خلط",previous:"السابق",play_pause_stop:"تشغيل/إيقاف مؤقت/إيقاف",next:"التالي",repeat:"وضع التكرار"},volume_controls:"التحكم في الصوت",volume_controls_list:{volume_buttons:"أزرار الصوت",volume_set:"مستوى الصوت",volume_mute:"كتم"}},lock:{lock:"مقفل",unlock:"إلغاء قفل",open:"مفتوح"},humidifier:{show_target_humidity_control:"التحكم في الرطوبة؟?"},climate:{show_temperature_control:"التحكم في درجة الحرارة؟",hvac_modes:"أوضاع HVAC"}},chip:{sub_element_editor:{title:"محرر الرقاقة"},conditional:{chip:"رقاقة"},"chip-picker":{chips:"رقاقات",add:"أضف رقاقة",edit:"تعديل",clear:"مسح",select:"اختر الرقاقة",types:{action:"إجراء","alarm-control-panel":"تنبيه",back:"رجوع",conditional:"مشروط",entity:"الكيان",light:"Light",menu:"القائمة",template:"قالب",weather:"الطقس"}}}},oi={editor:ii},ni={form:{color_picker:{values:{default:"Основен цвят"}},info_picker:{values:{default:"Основна информация",name:"Име",state:"Състояние","last-changed":"Последно Променен","last-updated":"Последно Актуализиран",none:"Липсва"}},icon_type_picker:{values:{default:"Основен тип",icon:"Икона","entity-picture":"Картина на обекта",none:"Липсва"}},layout_picker:{values:{default:"Основно оформление",vertical:"Вертикално оформление",horizontal:"Хоризонтално оформление"}},alignment_picker:{values:{default:"Основно подравняване",start:"Старт",end:"Край",center:"Център",justify:"Подравнен"}}},card:{generic:{icon_color:"Цвят на икона",layout:"Оформление",fill_container:"Изпълване на контейнера",primary_info:"Първостепенна информация",secondary_info:"Второстепенна информация",icon_type:"Тип на икона",content_info:"Съдържание",use_entity_picture:"Използвай снимката на обекта?",collapsible_controls:"Свий контролите при изключен",icon_animation:"Анимирай иконата при активен?"},light:{show_brightness_control:"Контрол на яркостта?",use_light_color:"Използвай цвета на светлината",show_color_temp_control:"Контрол на температурата?",show_color_control:"Контрол на цвета?",incompatible_controls:"Някои опции могат да бъдат скрити при условие че осветителното тяло не поддържа фунцията."},fan:{show_percentage_control:"Процентов контрол?",show_oscillate_control:"Контрол на трептенето?"},cover:{show_buttons_control:"Контролни бутони?",show_position_control:"Контрол на позицията?",show_tilt_position_control:"Контрол на наклона?"},alarm_control_panel:{show_keypad:"Покажи клавиатура"},template:{primary:"Първостепенна информация",secondary:"Второстепенна информация",multiline_secondary:"Много-редова второстепенна информация?",entity_extra:"Използван в шаблони и действия",content:"Съдържание",badge_icon:"Икона на значка",badge_color:"Цвят на значка",picture:"Картина (ще замени иконата)"},title:{title:"Заглавие",subtitle:"Подзаглавие"},chips:{alignment:"Подравняване"},weather:{show_conditions:"Условия?",show_temperature:"Температура?"},update:{show_buttons_control:"Контролни бутони?"},vacuum:{commands:"Конади",commands_list:{on_off:"Вкл./Изкл."}},"media-player":{use_media_info:"Използвай информация от медията",use_media_artwork:"Използвай визуалните детайли от медията",show_volume_level:"Покажи контрола за звук",media_controls:"Контрол на Медиата",media_controls_list:{on_off:"Вкл./Изкл.",shuffle:"Разбъркано",previous:"Предишен",play_pause_stop:"Пусни/пауза/стоп",next:"Следващ",repeat:"Повтаряне"},volume_controls:"Контрол на звука",volume_controls_list:{volume_buttons:"Бутони за звук",volume_set:"Ниво на звука",volume_mute:"Заглуши"}},lock:{lock:"Заключен",unlock:"Отключен",open:"Отворен"},humidifier:{show_target_humidity_control:"Контрол на влажността?"},climate:{show_temperature_control:"Контрол на температурата?",hvac_modes:"HVAC Режими"}},chip:{sub_element_editor:{title:"Чип редактор"},conditional:{chip:"Чип"},"chip-picker":{chips:"Чипове",add:"Добави чип",edit:"Редактирай",clear:"Изчисти",select:"Избери чип",types:{action:"Действия","alarm-control-panel":"Аларма",back:"Назад",conditional:"Условни",entity:"Обект",light:"Осветление",menu:"Меню",template:"Шаблон",weather:"Време"}}}},ri={editor:ni},ai={form:{color_picker:{values:{default:"Color per defecte"}},info_picker:{values:{default:"Informació per defecte",name:"Nom",state:"Estat","last-changed":"Últim Canvi","last-updated":"Última Actualització",none:"Cap"}},icon_type_picker:{values:{default:"Tipus per defecte",icon:"Icona","entity-picture":"Entitat d'imatge",none:"Cap"}},layout_picker:{values:{default:"Distribució per defecte",vertical:"Distribució vertical",horizontal:"Distribució horitzontal"}},alignment_picker:{values:{default:"Alineació per defecte",start:"Inici",end:"Final",center:"Centre",justify:"Justifica"}}},card:{generic:{icon_color:"Color d'icona",layout:"Distribució",fill_container:"Emplena el contenidor",primary_info:"Informació primaria",secondary_info:"Informació secundaria",icon_type:"Tipus d'icona",content_info:"Contingut",use_entity_picture:"Fer servir la imatge de l'entitat?",collapsible_controls:"Amaga els controls en desactivar",icon_animation:"Animar icona en activar?"},light:{show_brightness_control:"Control de brillantor?",use_light_color:"Fes servir el color del llum",show_color_temp_control:"Control de la temperatura del color?",show_color_control:"Control de color?",incompatible_controls:"Alguns controls no es mostraran si l'entitat no suporta eixa funció."},fan:{show_percentage_control:"Control de percentatge?",show_oscillate_control:"Control d'oscil·lació?"},cover:{show_buttons_control:"Botons de control?",show_position_control:"Control de posició?",show_tilt_position_control:"Control d'inclinació?"},alarm_control_panel:{show_keypad:"Mostra el teclat"},template:{primary:"Informació primaria",secondary:"Informació secundaria",multiline_secondary:"Secundaria en varies línies?",entity_extra:"Utilitzats en plantilles i accions",content:"Contingut",badge_icon:"Icona de la insígnia",badge_color:"Color de la insígnia",picture:"Imatge (reemplaçarà la icona)"},title:{title:"Títol",subtitle:"Subtítol"},chips:{alignment:"Alineació"},weather:{show_conditions:"Condicions?",show_temperature:"Temperatura?"},update:{show_buttons_control:"Botons de control?"},vacuum:{commands:"Comandaments",commands_list:{on_off:"Engegar/Apagar"}},"media-player":{use_media_info:"Empra la informació multimèdia",use_media_artwork:"Fes servir l'art multimèdia",show_volume_level:"Mostra el nivell de volum",media_controls:"Controls multimèdia",media_controls_list:{on_off:"Engegar/Apagar",shuffle:"Mesclar",previous:"Pista anterior",play_pause_stop:"Reproduïr/Pausar/Detindre",next:"Pista següent",repeat:"Mode de repetició"},volume_controls:"Controls de volum",volume_controls_list:{volume_buttons:"Botons de volum",volume_set:"Nivell de volum",volume_mute:"Silenci"}},lock:{lock:"Bloqueja",unlock:"Desbloqueja",open:"Obri"},humidifier:{show_target_humidity_control:"Control d'humitat?"},climate:{show_temperature_control:"Control de temperatura?",hvac_modes:"Modes HVAC"}},chip:{sub_element_editor:{title:"Editor de xips"},conditional:{chip:"Xip"},"chip-picker":{chips:"Xips",add:"Afegir xip",edit:"Editar",clear:"Buidar",select:"Seleccionar chip",types:{action:"Acció","alarm-control-panel":"Alarma",back:"Tornar",conditional:"Condicional",entity:"Entitat",light:"Llum",menu:"Menú",template:"Plantilla",weather:"Oratge"}}}},li={editor:ai},si={form:{color_picker:{values:{default:"Výchozí barva"}},info_picker:{values:{default:"Základní informace",name:"Název",state:"Stav","last-changed":"Poslední změna","last-updated":"Poslední update",none:"Nic"}},icon_type_picker:{values:{default:"Výchozí typ",icon:"Ikona","entity-picture":"Ikona entity",none:"Nic"}},layout_picker:{values:{default:"Výchozí rozložení",vertical:"Svislé rozložení",horizontal:"Vodorovné rozložení"}},alignment_picker:{values:{default:"Výchozí zarovnání",start:"Začátek",end:"Konec",center:"Na střed",justify:"Do bloku"}}},card:{generic:{icon_color:"Barva ikony",layout:"Rozložení",fill_container:"Vyplnit prostor",primary_info:"Základní informace",secondary_info:"Sekundární informace",icon_type:"Typ ikony",content_info:"Obsah",use_entity_picture:"Použít ikonu entity?",collapsible_controls:"Skrýt ovládací prvky pokud je VYP",icon_animation:"Animovaná ikona, pokud je aktivní?"},light:{show_brightness_control:"Ovládání jasu?",use_light_color:"Použít ovládání světla",show_color_temp_control:"Ovládání teploty světla?",show_color_control:"Ovládání barvy světla?",incompatible_controls:"Některé ovládací prvky se nemusí zobrazit, pokud vaše světlo tuto funkci nepodporuje."},fan:{show_percentage_control:"Ovládání v procentech?",show_oscillate_control:"Oscillate control?"},cover:{show_buttons_control:"Zobrazit ovládací tlačítka?",show_position_control:"Zobrazit ovládání polohy?"},alarm_control_panel:{show_keypad:"Zobrazit klávesnici"},template:{primary:"Základní informace",secondary:"Sekundární informace",multiline_secondary:"Víceřádková sekundární informace?",entity_extra:"Použito v šablonách a akcích",content:"Obsah",badge_icon:"Ikona odznaku",badge_color:"Barva odznaku",picture:"Obrázek (nahradí ikonu)"},title:{title:"Titulek",subtitle:"Popis"},chips:{alignment:"Zarovnání"},weather:{show_conditions:"Zobrazit podmínky?",show_temperature:"Zobrazit teplotu?"},update:{show_buttons_control:"Zobrazit ovládací tlačítka?"},vacuum:{commands:"Příkazy"},"media-player":{use_media_info:"Použít informace o médiích",use_media_artwork:"Použít ilustrace médií",show_volume_level:"Zobrazit úroveň hlasitosti",media_controls:"Ovládání médií",media_controls_list:{on_off:"Vyp / Zap",shuffle:"Zamíchat",previous:"Předchozí skladba",play_pause_stop:"hrát/pauza/zastavit",next:"Další skladba",repeat:"Opakovat"},volume_controls:"Ovládání hlasitosti",volume_controls_list:{volume_buttons:"Tlačítka hlasitosti",volume_set:"Úroveň hlasitosti",volume_mute:"Ztlumit"}},lock:{lock:"Zamčeno",unlock:"Odemčeno",open:"Otevřeno"},humidifier:{show_target_humidity_control:"Ovládání vlhkosti?"},climate:{show_temperature_control:"Ovládání teploty?",hvac_modes:"HVAC Mód"}},chip:{sub_element_editor:{title:"Editor tlačítek"},conditional:{chip:"Tlačítko"},"chip-picker":{chips:"Tlačítka",add:"Přidat tlačítko",edit:"Editovat",clear:"Vymazat",select:"Vybrat tlačítko",types:{action:"Akce","alarm-control-panel":"Alarm",back:"Zpět",conditional:"Podmínky",entity:"Entita",light:"Světlo",menu:"Menu",template:"Šablona",weather:"Počasí"}}}},ci={editor:si},di={form:{color_picker:{values:{default:"Standard farve"}},info_picker:{values:{default:"Standard information",name:"Navn",state:"Status","last-changed":"Sidst ændret","last-updated":"Sidst opdateret",none:"Ingen"}},icon_type_picker:{values:{default:"Standard type",icon:"Ikon","entity-picture":"Enheds billede",none:"Ingen"}},layout_picker:{values:{default:"Standard layout",vertical:"Vertikal layout",horizontal:"Horisontal layout"}},alignment_picker:{values:{default:"Standard justering",start:"Start",end:"Slut",center:"Centrer",justify:"Lige margener"}}},card:{generic:{icon_color:"Ikon farve",layout:"Layout",fill_container:"Fyld container",primary_info:"Primær information",secondary_info:"Sekundær information",icon_type:"Ikon type",content_info:"Indhold",use_entity_picture:"Brug enheds billede?",collapsible_controls:"Skjul kontroller når slukket",icon_animation:"Animér ikon når aktiv?"},light:{show_brightness_control:"Lysstyrkekontrol?",use_light_color:"Brug lysfarve",show_color_temp_control:"Temperatur farvekontrol?",show_color_control:"Farvekontrol?",incompatible_controls:"Nogle kontroller vises muligvis ikke, hvis dit lys ikke understøtter funktionen."},fan:{show_percentage_control:"Procentvis kontrol?",show_oscillate_control:"Oscillerende kontrol?"},cover:{show_buttons_control:"Betjeningsknapper?",show_position_control:"Positionskontrol?"},alarm_control_panel:{show_keypad:"Vis tastatur"},template:{primary:"Primær information",secondary:"Sekundær information",multiline_secondary:"Multi-linje skundær?",entity_extra:"Anvendes i skabelober og handlinger",content:"Indhold",badge_icon:"Badge ikon",badge_color:"Badge farve",picture:"Billede (erstatter ikonen)"},title:{title:"Titel",subtitle:"Undertitel"},chips:{alignment:"Justering"},weather:{show_conditions:"Forhold?",show_temperature:"Temperatur?"},update:{show_buttons_control:"Betjeningsknapper?"},vacuum:{commands:"Kommandoer"},"media-player":{use_media_info:"Brug medie info",use_media_artwork:"Brug mediebilleder",show_volume_level:"Vis volumen niveau",media_controls:"Medie kontrol",media_controls_list:{on_off:"Tænd/Sluk",shuffle:"Bland",previous:"Forrige nummer",play_pause_stop:"Afspil/Pause/Stop",next:"Næste nummer",repeat:"Gentagelsestilstand"},volume_controls:"Volumen kontrol",volume_controls_list:{volume_buttons:"Volumen knapper",volume_set:"Volumenniveau",volume_mute:"Lydløs"}},lock:{lock:"Lås",unlock:"Lås op",open:"Åben"},humidifier:{show_target_humidity_control:"Luftfugtigheds kontrol?"},climate:{show_temperature_control:"Temperatur kontrol?",hvac_modes:"HVAC-tilstande"}},chip:{sub_element_editor:{title:"Chip-editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Tilføj chip",edit:"Rediger",clear:"Nulstil",select:"Vælg chip",types:{action:"Handling","alarm-control-panel":"Alarm",back:"Tilbage",conditional:"Betinget",entity:"Enhed",light:"Lys",menu:"Menu",template:"Skabelon",weather:"Vejr"}}}},ui={editor:di},hi={form:{color_picker:{values:{default:"Standardfarbe"}},info_picker:{values:{default:"Standard-Information",name:"Name",state:"Zustand","last-changed":"Letzte Änderung","last-updated":"Letzte Aktualisierung",none:"Keine"}},icon_type_picker:{values:{default:"Standard-Typ",icon:"Icon","entity-picture":"Entitätsbild",none:"Keines"}},layout_picker:{values:{default:"Standard-Layout",vertical:"Vertikales Layout",horizontal:"Horizontales Layout"}},alignment_picker:{values:{default:"Standard",start:"Anfang",end:"Ende",center:"Mitte",justify:"Ausrichten"}}},card:{generic:{icon_color:"Icon-Farbe",layout:"Layout",fill_container:"Container ausfüllen",primary_info:"Primäre Information",secondary_info:"Sekundäre Information",icon_type:"Icon-Typ",content_info:"Inhalt",use_entity_picture:"Entitätsbild verwenden?",collapsible_controls:"Schieberegler einklappen, wenn aus",icon_animation:"Icon animieren, wenn aktiv?"},light:{show_brightness_control:"Helligkeitsregelung?",use_light_color:"Farbsteuerung verwenden",show_color_temp_control:"Farbtemperatursteuerung?",show_color_control:"Farbsteuerung?",incompatible_controls:"Einige Steuerelemente werden möglicherweise nicht angezeigt, wenn Ihr Licht diese Funktion nicht unterstützt."},fan:{show_percentage_control:"Prozentuale Kontrolle?",show_oscillate_control:"Oszillationssteuerung?"},cover:{show_buttons_control:"Schaltflächensteuerung?",show_position_control:"Positionssteuerung?",show_tilt_position_control:"Winkelsteuerung?"},alarm_control_panel:{show_keypad:"Keypad anzeigen"},template:{primary:"Primäre Information",secondary:"Sekundäre Information",multiline_secondary:"Mehrzeilig sekundär?",entity_extra:"Wird in Vorlagen und Aktionen verwendet",content:"Inhalt",badge_icon:"Badge-Icon",badge_color:"Badge-Farbe",picture:"Bild (ersetzt das Icon)"},title:{title:"Titel",subtitle:"Untertitel",title_tap_action:"Titel Tipp-Aktion",subtitle_tap_action:"Untertitel Tipp-Aktion"},chips:{alignment:"Ausrichtung"},weather:{show_conditions:"Bedingungen?",show_temperature:"Temperatur?"},update:{show_buttons_control:"Schaltflächensteuerung?"},vacuum:{commands:"Befehle",commands_list:{on_off:"An/Ausschalten"}},"media-player":{use_media_info:"Medieninfos verwenden",use_media_artwork:"Mediengrafik verwenden",show_volume_level:"Lautstärke-Level anzeigen",media_controls:"Mediensteuerung",media_controls_list:{on_off:"Ein/Aus",shuffle:"Zufällige Wiedergabe",previous:"Vorheriger Titel",play_pause_stop:"Play/Pause/Stop",next:"Nächster Titel",repeat:"Wiederholen"},volume_controls:"Lautstärkesteuerung",volume_controls_list:{volume_buttons:"Lautstärke-Buttons",volume_set:"Lautstärke-Level",volume_mute:"Stumm"}},lock:{lock:"Verriegeln",unlock:"Entriegeln",open:"Öffnen"},humidifier:{show_target_humidity_control:"Luftfeuchtigkeitssteuerung?"},climate:{show_temperature_control:"Temperatursteuerung?",hvac_modes:"HVAC-Modi"},number:{display_mode:"Anzeigemodus",display_mode_list:{default:"Standard (Schieberegler)",slider:"Schieberegler",buttons:"Buttons"}}},chip:{sub_element_editor:{title:"Chip Editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Chip hinzufügen",edit:"Editieren",clear:"Löschen",select:"Chip auswählen",types:{action:"Aktion","alarm-control-panel":"Alarm",back:"Zurück",conditional:"Bedingung",entity:"Entität",light:"Licht",menu:"Menü",template:"Vorlage",weather:"Wetter"}}}},mi={editor:hi},pi={form:{color_picker:{values:{default:"Προεπιλεγμένο χρώμα"}},info_picker:{values:{default:"Προεπιλεγμένες πληροφορίες",name:"Όνομα",state:"Κατάσταση","last-changed":"Τελευταία αλλαγή","last-updated":"Τελευταία ενημέρωση",none:"Τίποτα"}},layout_picker:{values:{default:"Προεπιλεγμένη διάταξη",vertical:"Κάθετη διάταξη",horizontal:"Οριζόντια διάταξη"}},alignment_picker:{values:{default:"Προεπιλεγμένη στοίχιση",start:"Στοίχιση αριστερά",end:"Στοίχιση δεξιά",center:"Στοίχιση στο κέντρο",justify:"Πλήρης στοίχιση"}}},card:{generic:{icon_color:"Χρώμα εικονιδίου",layout:"Διάταξη",primary_info:"Πρωτεύουσες πληροφορίες",secondary_info:"Δευτερεύουσες πληροφορίες",content_info:"Περιεχόμενο",use_entity_picture:"Χρήση εικόνας οντότητας;",icon_animation:"Κίνηση εικονιδίου όταν είναι ενεργό;"},light:{show_brightness_control:"Έλεγχος φωτεινότητας;",use_light_color:"Χρήση χρώματος φωτος",show_color_temp_control:"Έλεγχος χρώματος θερμοκρασίας;",show_color_control:"Έλεγχος χρώματος;",incompatible_controls:"Ορισμένα στοιχεία ελέγχου ενδέχεται να μην εμφανίζονται εάν το φωτιστικό σας δεν υποστηρίζει τη λειτουργία."},fan:{show_percentage_control:"Έλεγχος ποσοστού;",show_oscillate_control:"Έλεγχος ταλάντωσης;"},cover:{show_buttons_control:"Έλεγχος κουμπιών;",show_position_control:"Έλεγχος θέσης;"},template:{primary:"Πρωτεύουσες πληροφορίες",secondary:"Δευτερεύουσες πληροφορίες",multiline_secondary:"Δευτερεύουσες πολλαπλών γραμμών;",entity_extra:"Χρησιμοποιείται σε πρότυπα και ενέργειες",content:"Περιεχόμενο"},title:{title:"Τίτλος",subtitle:"Υπότιτλος"},chips:{alignment:"Ευθυγράμμιση"},weather:{show_conditions:"Συνθήκες;",show_temperature:"Θερμοκρασία;"},update:{show_buttons_control:"Έλεγχος κουμπιών;"},vacuum:{commands:"Εντολές"},"media-player":{use_media_info:"Χρήση πληροφοριών πολυμέσων",use_media_artwork:"Χρήση έργων τέχνης πολυμέσων",media_controls:"Έλεγχος πολυμέσων",media_controls_list:{on_off:"Ενεργοποίηση/απενεργοποίηση",shuffle:"Τυχαία σειρά",previous:"Προηγούμενο κομμάτι",play_pause_stop:"Αναπαραγωγή/παύση/διακοπή",next:"Επόμενο κομμάτι",repeat:"Λειτουργία επανάληψης"},volume_controls:"Χειριστήρια έντασης ήχου",volume_controls_list:{volume_buttons:"Κουμπιά έντασης ήχου",volume_set:"Επίπεδο έντασης ήχου",volume_mute:"Σίγαση"}}},chip:{sub_element_editor:{title:"Επεξεργαστής Chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Προσθήκη chip",edit:"Επεξεργασία",clear:"Καθαρισμός",select:"Επιλογή chip",types:{action:"Ενέργεια","alarm-control-panel":"Συναγερμός",back:"Πίσω",conditional:"Υπό προϋποθέσεις",entity:"Οντότητα",light:"Φως",menu:"Μενού",template:"Πρότυπο",weather:"Καιρός"}}}},fi={editor:pi},gi={form:{color_picker:{values:{default:"Default color"}},info_picker:{values:{default:"Default information",name:"Name",state:"State","last-changed":"Last Changed","last-updated":"Last Updated",none:"None"}},icon_type_picker:{values:{default:"Default type",icon:"Icon","entity-picture":"Entity picture",none:"None"}},layout_picker:{values:{default:"Default layout",vertical:"Vertical layout",horizontal:"Horizontal layout"}},alignment_picker:{values:{default:"Default alignment",start:"Start",end:"End",center:"Center",justify:"Justify"}}},card:{generic:{icon_color:"Icon color",layout:"Layout",fill_container:"Fill container",primary_info:"Primary information",secondary_info:"Secondary information",icon_type:"Icon type",content_info:"Content",use_entity_picture:"Use entity picture?",collapsible_controls:"Collapse controls when off",icon_animation:"Animate icon when active?"},light:{show_brightness_control:"Brightness control?",use_light_color:"Use light color",show_color_temp_control:"Temperature color control?",show_color_control:"Color control?",incompatible_controls:"Some controls may not be displayed if your light does not support the feature."},fan:{show_percentage_control:"Percentage control?",show_oscillate_control:"Oscillate control?"},cover:{show_buttons_control:"Control buttons?",show_position_control:"Position control?",show_tilt_position_control:"Tilt control?"},alarm_control_panel:{show_keypad:"Show keypad"},template:{primary:"Primary information",secondary:"Secondary information",multiline_secondary:"Multiline secondary?",entity_extra:"Used in templates and actions",content:"Content",badge_icon:"Badge icon",badge_color:"Badge color",picture:"Picture (will replace the icon)"},title:{title:"Title",subtitle:"Subtitle",title_tap_action:"Title tap action",subtitle_tap_action:"Subtitle tap action"},chips:{alignment:"Alignment"},weather:{show_conditions:"Conditions?",show_temperature:"Temperature?"},update:{show_buttons_control:"Control buttons?"},vacuum:{commands:"Commands",commands_list:{on_off:"Turn on/off"}},"media-player":{use_media_info:"Use media info",use_media_artwork:"Use media artwork",show_volume_level:"Show volume level",media_controls:"Media controls",media_controls_list:{on_off:"Turn on/off",shuffle:"Shuffle",previous:"Previous track",play_pause_stop:"Play/pause/stop",next:"Next track",repeat:"Repeat mode"},volume_controls:"Volume controls",volume_controls_list:{volume_buttons:"Volume buttons",volume_set:"Volume level",volume_mute:"Mute"}},lock:{lock:"Lock",unlock:"Unlock",open:"Open"},humidifier:{show_target_humidity_control:"Humidity control?"},climate:{show_temperature_control:"Temperature control?",hvac_modes:"HVAC Modes"},number:{display_mode:"Display Mode",display_mode_list:{default:"Default (slider)",slider:"Slider",buttons:"Buttons"}}},chip:{sub_element_editor:{title:"Chip editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Add chip",edit:"Edit",clear:"Clear",select:"Select chip",types:{action:"Action","alarm-control-panel":"Alarm",back:"Back",conditional:"Conditional",entity:"Entity",light:"Light",menu:"Menu",template:"Template",weather:"Weather"}}}},_i={editor:gi},vi={form:{color_picker:{values:{default:"Color predeterminado"}},info_picker:{values:{default:"Información predeterminada",name:"Nombre",state:"Estado","last-changed":"Último cambio","last-updated":"Última actualización",none:"Ninguno"}},icon_type_picker:{values:{default:"Por defecto",icon:"Icono","entity-picture":"Imagen de entidad",none:"Ninguno"}},layout_picker:{values:{default:"Diseño predeterminado",vertical:"Diseño vertical",horizontal:"Diseño horizontal"}},alignment_picker:{values:{default:"Alineación predeterminada",start:"Inicio",end:"Final",center:"Centrado",justify:"Justificado"}}},card:{generic:{icon_color:"Color de icono",layout:"Diseño",fill_container:"Rellenar",primary_info:"Información primaria",secondary_info:"Información secundaria",icon_type:"Icono",content_info:"Contenido",use_entity_picture:"¿Usar imagen de entidad?",collapsible_controls:"Contraer controles cuando está apagado",icon_animation:"¿Icono animado cuando está activo?"},light:{show_brightness_control:"¿Controlar brillo?",use_light_color:"Usar color de la luz",show_color_temp_control:"¿Controlar temperatura del color?",show_color_control:"¿Controlar color?",incompatible_controls:"Es posible que algunos controles no se muestren si la luz no es compatible con esta función."},fan:{show_percentage_control:"¿Controlar porcentaje?",show_oscillate_control:"¿Controlar oscilación?"},cover:{show_buttons_control:"¿Botones de control?",show_position_control:"¿Control de posición?",show_tilt_position_control:"¿Control de inclinación?"},alarm_control_panel:{show_keypad:"Mostrar teclado"},template:{primary:"Información primaria",secondary:"Información secundaria",multiline_secondary:"¿Secundaria multilínea?",entity_extra:"Utilizado en plantillas y acciones.",content:"Contenido",badge_icon:"Icono del distintivo",badge_color:"Color del distintivo",picture:"Imagen (sustituirá al icono)"},title:{title:"Título",subtitle:"Subtítulo",title_tap_action:"Acción al tocar el título",subtitle_tap_action:"Acción al tocar el subtítulo"},chips:{alignment:"Alineación"},weather:{show_conditions:"¿Condiciones?",show_temperature:"¿Temperatura?"},update:{show_buttons_control:"¿Botones de control?"},vacuum:{commands:"Comandos",commands_list:{on_off:"Activar/desactivar"}},"media-player":{use_media_info:"Usar información multimedia",use_media_artwork:"Usar ilustraciones multimedia",show_volume_level:"Mostrar nivel de volumen",media_controls:"Controles multimedia",media_controls_list:{on_off:"Activar/desactivar",shuffle:"Aleatoria",previous:"Pista anterior",play_pause_stop:"Reproducir/pausa/parar",next:"Pista siguiente",repeat:"Modo de repetición"},volume_controls:"Controles de volumen",volume_controls_list:{volume_buttons:"Botones de volumen",volume_set:"Nivel de volumen",volume_mute:"Silenciar"}},lock:{lock:"Bloquear",unlock:"Desbloquear",open:"Abrir"},humidifier:{show_target_humidity_control:"¿Controlar humedad?"},climate:{show_temperature_control:"¿Control de temperatura?",hvac_modes:"Modos de climatización"}},chip:{sub_element_editor:{title:"Editor de chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Añadir chip",edit:"Editar",clear:"Limpiar",select:"Seleccionar chip",types:{action:"Acción","alarm-control-panel":"Alarma",back:"Volver",conditional:"Condicional",entity:"Entidad",light:"Luz",menu:"Menú",template:"Plantilla",weather:"Clima"}}}},bi={editor:vi},yi={form:{color_picker:{values:{default:"Oletusväri"}},info_picker:{values:{default:"Oletustiedot",name:"Nimi",state:"Tila","last-changed":"Viimeksi muuttunut","last-updated":"Viimeksi päivittynyt",none:"Ei mitään"}},icon_type_picker:{values:{default:"Oletustyyppi",icon:"Kuvake","entity-picture":"Kohteen kuva",none:"Ei mitään"}},layout_picker:{values:{default:"Oletusasettelu",vertical:"Pystysuuntainen",horizontal:"Vaakasuuntainen"}},alignment_picker:{values:{default:"Keskitys",start:"Alku",end:"Loppu",center:"Keskitä",justify:"Sovita"}}},card:{generic:{icon_color:"Ikonin väri",layout:"Asettelu",fill_container:"Täytä alue",primary_info:"Ensisijaiset tiedot",secondary_info:"Toissijaiset tiedot",icon_type:"Kuvakkeen tyyppi",content_info:"Sisältö",use_entity_picture:"Käytä kohteen kuvaa?",collapsible_controls:"Piilota toiminnot off-tilassa",icon_animation:"Animoi kuvake, kun aktiivinen?"},light:{show_brightness_control:"Kirkkauden säätö?",use_light_color:"Käytä valaisimen väriä",show_color_temp_control:"Värilämpötilan säätö?",show_color_control:"Värin säätö?",incompatible_controls:"Jotkin toiminnot eivät näy, jos valaisimesi ei tue niitä."},fan:{show_percentage_control:"Prosentuaalinen säätö?",show_oscillate_control:"Oskillaation säätö?"},cover:{show_buttons_control:"Toimintopainikkeet?",show_position_control:"Sijainnin hallinta?"},alarm_control_panel:{show_keypad:"Näytä näppäimet"},template:{primary:"Ensisijaiset tiedot",secondary:"Toissijaiset tiedot",multiline_secondary:"Monirivinen toissijainen tieto?",entity_extra:"Käytetään malleissa ja toiminnoissa",content:"Sisältö",badge_icon:"Merkin kuvake",badge_color:"Merkin väri",picture:"Kuva (korvaa kuvakkeen)"},title:{title:"Otsikko",subtitle:"Tekstitys"},chips:{alignment:"Asettelu"},weather:{show_conditions:"Ehdot?",show_temperature:"Lämpötila?"},update:{show_buttons_control:"Toimintopainikkeet?"},vacuum:{commands:"Komennot"},"media-player":{use_media_info:"Käytä median tietoja",use_media_artwork:"Käytä median kuvituksia",show_volume_level:"Näytä äänenvoimakkuuden hallinta",media_controls:"Toiminnot",media_controls_list:{on_off:"Päälle/pois",shuffle:"Sekoita",previous:"Edellinen kappale",play_pause_stop:"Toista/keskeytä/pysäytä",next:"Seuraava kappale",repeat:"Jatkuva toisto"},volume_controls:"Äänenvoimakkuuden hallinta",volume_controls_list:{volume_buttons:"Äänenvoimakkuuspainikkeet",volume_set:"Äänenvoimakkuus",volume_mute:"Mykistä"}},lock:{lock:"Lukitse",unlock:"Poista lukitus",open:"Avaa"},humidifier:{show_target_humidity_control:"Kosteudenhallinta?"}},chip:{sub_element_editor:{title:"Merkkieditori"},conditional:{chip:"Merkki"},"chip-picker":{chips:"Merkit",add:"Lisää merkki",edit:"Muokkaa",clear:"Tyhjennä",select:"Valitse merkki",types:{action:"Toiminto","alarm-control-panel":"Hälytys",back:"Takaisin",conditional:"Ehdollinen",entity:"Kohde",light:"Valaisin",menu:"Valikko",template:"Malli",weather:"Sää"}}}},xi={editor:yi},wi={form:{color_picker:{values:{default:"Couleur par défaut"}},info_picker:{values:{default:"Information par défaut",name:"Nom",state:"État","last-changed":"Dernière modification","last-updated":"Dernière mise à jour",none:"Aucune"}},icon_type_picker:{values:{default:"Type par défaut",icon:"Icône","entity-picture":"Image de l'entité",none:"Aucune"}},layout_picker:{values:{default:"Disposition par défault",vertical:"Disposition verticale",horizontal:"Disposition horizontale"}},alignment_picker:{values:{default:"Alignement par défaut",start:"Début",end:"Fin",center:"Centré",justify:"Justifié"}}},card:{generic:{icon_color:"Couleur de l'icône",layout:"Disposition",fill_container:"Remplir le conteneur",primary_info:"Information principale",secondary_info:"Information secondaire",icon_type:"Type d'icône",content_info:"Contenu",use_entity_picture:"Utiliser l'image de l'entité ?",collapsible_controls:"Reduire les contrôles quand éteint",icon_animation:"Animation de l'icône ?"},light:{show_brightness_control:"Contrôle de luminosité ?",use_light_color:"Utiliser la couleur de la lumière",show_color_temp_control:"Contrôle de la température ?",show_color_control:"Contrôle de la couleur ?",incompatible_controls:"Certains contrôles peuvent ne pas être affichés si votre lumière ne supporte pas la fonctionnalité."},fan:{show_percentage_control:"Contrôle de la vitesse ?",show_oscillate_control:"Contrôle de l'oscillation ?"},cover:{show_buttons_control:"Contrôle avec boutons ?",show_position_control:"Contrôle de la position ?"},alarm_control_panel:{show_keypad:"Afficher le clavier"},template:{primary:"Information principale",secondary:"Information secondaire",multiline_secondary:"Information secondaire sur plusieurs lignes ?",entity_extra:"Utilisée pour les templates et les actions",content:"Contenu",badge_icon:"Icône du badge",badge_color:"Couleur du badge",picture:"Picture (remplacera l'icône)"},title:{title:"Titre",subtitle:"Sous-titre",title_tap_action:"Appui sur le titre",subtitle_tap_action:"Appui sur le sous-titre"},chips:{alignment:"Alignement"},weather:{show_conditons:"Conditions ?",show_temperature:"Température ?"},update:{show_buttons_control:"Contrôle avec boutons ?"},vacuum:{commands:"Commandes",commands_list:{on_off:"Allumer/Éteindre"}},"media-player":{use_media_info:"Utiliser les informations du media",use_media_artwork:"Utiliser l'illustration du media",show_volume_level:"Afficher le niveau de volume",media_controls:"Contrôles du media",media_controls_list:{on_off:"Allumer/Éteindre",shuffle:"Lecture aléatoire",previous:"Précédent",play_pause_stop:"Lecture/pause/stop",next:"Suivant",repeat:"Mode de répétition"},volume_controls:"Contrôles du volume",volume_controls_list:{volume_buttons:"Bouton de volume",volume_set:"Niveau de volume",volume_mute:"Muet"}},lock:{lock:"Verrouiller",unlock:"Déverrouiller",open:"Ouvrir"},humidifier:{show_target_humidity_control:"Contrôle d'humidité ?"},climate:{show_temperature_control:"Contrôle de la température?",hvac_modes:"Modes du thermostat"},number:{display_mode:"Mode d'affichage",display_mode_list:{default:"Par défaut (Curseur)",slider:"Curseur",buttons:"Boutons"}}},chip:{sub_element_editor:{title:'Éditeur de "chip"'},conditional:{chip:"Chip"},"chip-picker":{chips:'"Chips"',add:'Ajouter une "chip"',edit:"Modifier",clear:"Effacer",select:'Sélectionner une "chip"',types:{action:"Action","alarm-control-panel":"Alarme",back:"Retour",conditional:"Conditionnel",entity:"Entité",light:"Lumière",menu:"Menu",template:"Template",weather:"Météo"}}}},ki={editor:wi},Ci={form:{color_picker:{values:{default:"צבע ברירת מחדל"}},info_picker:{values:{default:"מידע ברירת מחדל",name:"שם",state:"מצב","last-changed":"שונה לאחרונה","last-updated":"עודכן לאחרונה",none:"ריק"}},layout_picker:{values:{default:"סידור ברירת מחדל",vertical:"סידור מאונך",horizontal:"סידור מאוזן"}},alignment_picker:{values:{default:"יישור ברירת מחדל",start:"התחלה",end:"סוף",center:"אמצע",justify:"מוצדק"}}},card:{generic:{icon_color:"צבע אייקון",layout:"סידור",fill_container:"מלא גבולות",primary_info:"מידע ראשי",secondary_info:"מידע מישני",content_info:"תוכן",use_entity_picture:"השתמש בתמונת ישות?",collapsible_controls:"הסתר שליטה כשאר מכובה?",icon_animation:"להנפיש אייקון כאשר דלוק?"},light:{show_brightness_control:"שליטה בבהירות?",use_light_color:"השתמש בצבע האור",show_color_temp_control:"שליטה בגוון האור?",show_color_control:"שליטה בצבע האור?",incompatible_controls:"יתכן וחלק מהכפתורים לא יופיעו אם התאורה אינה תומכת בתכונה."},fan:{show_percentage_control:"שליטה באחוז?",show_oscillate_control:"שליטה בהתנדנדות?"},cover:{show_buttons_control:"כפתורי שליטה?",show_position_control:"שליטה במיקום?"},alarm_control_panel:{show_keypad:"הצג מקלדת"},template:{primary:"מידע ראשי",secondary:"מידע מישני",multiline_secondary:"מידע מישני רו קווי?",entity_extra:"משמש בתבניות ופעולות",content:"תוכן"},title:{title:"כותרת",subtitle:"כתובית"},chips:{alignment:"יישור"},weather:{show_conditions:"הצג תנאים?",show_temperature:"הצג טמפרטורה?"},update:{show_buttons_control:"הצג כפתורי שליטה?"},vacuum:{commands:"פקודות",icon_animation:"להנפיש אייקון כאשר דלוק?"},"media-player":{use_media_info:"השתמש במידע מדיה",use_media_artwork:"השתמש באומנות מדיה",show_volume_level:"הצג שליטת ווליום",media_controls:"שליטה במדיה",media_controls_list:{on_off:"הדלק/כבה",shuffle:"ערבב",previous:"רצועה קודמת",play_pause_stop:"נגן/השהה/הפסק",next:"רצועה הבאה",repeat:"חזרה"},volume_controls:"שליטה בווליום",volume_controls_list:{volume_buttons:"כפתורי ווליום",volume_set:"רמת ווליום",volume_mute:"השתק"}},lock:{lock:"נעל",unlock:"בטל נעילה",open:"פתח"},humidifier:{show_target_humidity_control:"שליטה בלחות?"}},chip:{sub_element_editor:{title:"עורך שבב"},conditional:{chip:"שבב"},"chip-picker":{chips:"שבבים",add:"הוסף שבב",edit:"ערוך",clear:"נקה",select:"בחר שבב",types:{action:"פעולה","alarm-control-panel":"אזעקה",back:"חזור",conditional:"מותנה",entity:"ישות",light:"אור",menu:"תפריט",template:"תבנית",weather:"מזג אוויר"}}}},$i={editor:Ci},Ei={form:{color_picker:{values:{default:"Alapértelmezett szín"}},info_picker:{values:{default:"Alepértelmezett információ",name:"Név",state:"Állapot","last-changed":"Utoljára módosítva","last-updated":"Utoljára frissítve",none:"Egyik sem"}},icon_type_picker:{values:{default:"Alapértelmezett típus",icon:"Ikon","entity-picture":"Entitás kép",none:"Egyik sem"}},layout_picker:{values:{default:"Alapértelmezet elrendezés",vertical:"Függőleges elrendezés",horizontal:"Vízszintes elrendezés"}},alignment_picker:{values:{default:"Alapértelmezett rendezés",start:"Kezdete",end:"Vége",center:"Közepe",justify:"Sorkizárt"}}},card:{generic:{icon_color:"Ikon szín",layout:"Elrendezés",fill_container:"Tároló kitöltése",primary_info:"Elsődleges információ",secondary_info:"Másodlagos információ",icon_type:"Ikon típus",content_info:"Tartalom",use_entity_picture:"Entitás kép használata",collapsible_controls:"Vezérlők összezárása kikapcsolt állapotban",icon_animation:"Ikon animálása aktív állapotban"},light:{show_brightness_control:"Fényerő vezérlő",use_light_color:"Fény szín használata",show_color_temp_control:"Színhőmérséklet vezérlő",show_color_control:"Szín vezérlő",incompatible_controls:"Azok a vezérlők nem lesznek megjelenítve, amelyeket a fényforrás nem támogat."},fan:{show_percentage_control:"Százalékos vezérlő",show_oscillate_control:"Oszcilláció vezérlő"},cover:{show_buttons_control:"Vezérlő gombok",show_position_control:"Pozíció vezérlő",show_tilt_position_control:"Dőlésszög szabályzó"},alarm_control_panel:{show_keypad:"Billentyűzet mutatása"},template:{primary:"Elsődleges információ",secondary:"Másodlagos információ",multiline_secondary:"Másodlagost több sorba?",entity_extra:"Műveletek és sablonok használatakor",content:"Tartalom",badge_icon:"Jelvény ikon",badge_color:"Jelvény szín",picture:"Kép (lecseréli az ikont)"},title:{title:"Fejléc",subtitle:"Alcím",title_tap_action:"Fejlécre koppintáskor",subtitle_tap_action:"Alcímre koppintáskor"},chips:{alignment:"Rendezés"},weather:{show_conditions:"Állapotok",show_temperature:"Hőmérséklet"},update:{show_buttons_control:"Vezérlő gombok"},vacuum:{commands:"Utasítások",commands_list:{on_off:"Ki/Bekapcsolás"}},"media-player":{use_media_info:"Média infó használata",use_media_artwork:"Média borító használata",show_volume_level:"Hangerő mutatása",media_controls:"Média vezérlők",media_controls_list:{on_off:"Ki/bekapcsolás",shuffle:"Véletlen lejátszás",previous:"Előző szám",play_pause_stop:"Lejátszás/szünet/állj",next:"Következő szám",repeat:"Ismétlés módja"},volume_controls:"Hangerő vezérlők",volume_controls_list:{volume_buttons:"Hangerő gombok",volume_set:"Hangerő szint",volume_mute:"Némítás"}},lock:{lock:"Zár",unlock:"Nyit",open:"Nyitva"},humidifier:{show_target_humidity_control:"Páratartalom vezérlő"},climate:{show_temperature_control:"Hőmérséklet vezérlő",hvac_modes:"HVAC mód"},number:{display_mode:"Megjelenítési mód",display_mode_list:{default:"Alepértelmezett (csúszka)",slider:"Csúszka",buttons:"Gombok"}}},chip:{sub_element_editor:{title:"Chip szerkesztő"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chip-ek",add:"Chip hozzáadása",edit:"Szerkesztés",clear:"Ürítés",select:"Chip kiválasztása",types:{action:"Művelet","alarm-control-panel":"Riasztó",back:"Vissza",conditional:"Feltételes",entity:"Entitás",light:"Fényforrás",menu:"Menü",template:"Sablon",weather:"Időjárás"}}}},Ai={editor:Ei},Si={form:{color_picker:{values:{default:"Colore predefinito"}},info_picker:{values:{default:"Informazione predefinita",name:"Nome",state:"Stato","last-changed":"Ultimo Cambiamento","last-updated":"Ultimo Aggiornamento",none:"Nessuno"}},icon_type_picker:{values:{default:"Tipo predefinito",icon:"Icona","entity-picture":"Immagine dell'entità",none:"Nessuna"}},layout_picker:{values:{default:"Disposizione Predefinita",vertical:"Disposizione Verticale",horizontal:"Disposizione Orizzontale"}},alignment_picker:{values:{default:"Allineamento predefinito",start:"Inizio",end:"Fine",center:"Centro",justify:"Giustificato"}}},card:{generic:{icon_color:"Colore dell'icona",layout:"Disposizione",fill_container:"Riempi il contenitore",primary_info:"Informazione primaria",secondary_info:"Informazione secondaria",icon_type:"Tipo icona",content_info:"Contenuto",use_entity_picture:"Usa l'immagine dell'entità",collapsible_controls:"Nascondi i controlli quando spento",icon_animation:"Anima l'icona quando attiva"},light:{use_light_color:"Usa il colore della luce",show_brightness_control:"Controllo luminosità",show_color_temp_control:"Controllo temperatura",show_color_control:"Controllo colore",incompatible_controls:"Alcuni controlli potrebbero non essere mostrati se la tua luce non li supporta."},fan:{show_percentage_control:"Controllo potenza",show_oscillate_control:"Controllo oscillazione"},cover:{show_buttons_control:"Pulsanti di controllo",show_position_control:"Controllo percentuale apertura",show_tilt_position_control:"Controllo percentuale inclinazione"},alarm_control_panel:{show_keypad:"Mostra il tastierino numerico"},template:{primary:"Informazione primaria",secondary:"Informazione secondaria",multiline_secondary:"Abilita frasi multilinea",entity_extra:"Usato in templates ed azioni",content:"Contenuto",badge_icon:"Icona del badge",badge_color:"Colore del badge",picture:"Immagine (sostituirà l'icona)"},title:{title:"Titolo",subtitle:"Sottotitolo",title_tap_action:"Azione di tap sul titolo",subtitle_tap_action:"Azione di tap sul sottotitolo"},chips:{alignment:"Allineamento"},weather:{show_conditions:"Condizioni",show_temperature:"Temperatura"},update:{show_buttons_control:"Pulsanti di controllo"},vacuum:{commands:"Comandi",commands_list:{on_off:"Accendi/Spegni"}},"media-player":{use_media_info:"Mostra le Informazioni Sorgente",use_media_artwork:"Usa la copertina della Sorgente",show_volume_level:"Mostra Volume",media_controls:"Controlli Media",media_controls_list:{on_off:"Accendi/Spegni",shuffle:"Riproduzione Casuale",previous:"Traccia Precedente",play_pause_stop:"Play/Pausa/Stop",next:"Traccia Successiva",repeat:"Loop"},volume_controls:"Controlli del Volume",volume_controls_list:{volume_buttons:"Bottoni del Volume",volume_set:"Livello del Volume",volume_mute:"Silenzia"}},lock:{lock:"Blocca",unlock:"Sblocca",open:"Aperto"},humidifier:{show_target_humidity_control:"Controllo umidità"},climate:{show_temperature_control:"Controllo della temperatura?",hvac_modes:"Modalità del termostato"},number:{display_mode:"Modalità di visualizzazione",display_mode_list:{default:"Predefinito (slider)",slider:"Slider",buttons:"Pulsanti"}}},chip:{sub_element_editor:{title:"Editor di chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Aggiungi chip",edit:"Modifica",clear:"Rimuovi",select:"Seleziona chip",types:{action:"Azione","alarm-control-panel":"Allarme",back:"Pulsante indietro",conditional:"Condizione",entity:"Entità",light:"Luce",menu:"Menù",template:"Template",weather:"Meteo"}}}},Ii={editor:Si},Ti={form:{color_picker:{values:{default:"기본 색"}},info_picker:{values:{default:"기본 정보",name:"이름",state:"상태","last-changed":"마지막 변경","last-updated":"마지막 업데이트",none:"없음"}},icon_type_picker:{values:{default:"기본 타입",icon:"아이콘","entity-picture":"엔티티 사진",none:"없음"}},layout_picker:{values:{default:"기본 레이아웃",vertical:"수직 레이아웃",horizontal:"수평 레이아웃"}},alignment_picker:{values:{default:"기본 정렬",start:"시작",end:"끝",center:"중앙",justify:"행 정렬"}}},card:{generic:{icon_color:"아이콘 색",layout:"레이아웃",fill_container:"콘테이너 채우기",primary_info:"기본 정보",secondary_info:"보조 정보",icon_type:"아이콘 타입",content_info:"내용 정보",use_entity_picture:"엔티티 사진 사용",collapsible_controls:"꺼져있을 때 컨트롤 접기",icon_animation:"활성화 시 아이콘 애니메이션 사용"},light:{show_brightness_control:"밝기 컨트롤 표시",use_light_color:"조명 색 사용",show_color_temp_control:"색 온도 컨트롤 표시",show_color_control:"색 컨트롤 표시",incompatible_controls:"조명이 기능을 지원하지 않는 경우 일부 컨트롤이 표시되지 않을 수 있습니다."},fan:{show_percentage_control:"퍼센트 컨트롤",show_oscillate_control:"오실레이트 컨트롤"},cover:{show_buttons_control:"컨트롤 버튼 표시",show_position_control:"위치 컨트롤 표시",show_tilt_position_control:"기울기 컨트롤 표시"},alarm_control_panel:{show_keypad:"키패드 표시"},template:{primary:"기본 정보",secondary:"보조 정보",multiline_secondary:"Multiline secondary?",entity_extra:"템플릿 및 작업에 사용",content:"내용",badge_icon:"뱃지 아이콘",badge_color:"뱃지 색",picture:"그림 (아이콘 대체)"},title:{title:"제목",subtitle:"부제목",title_tap_action:"제목 탭 액션",subtitle_tap_action:"부제목 탭 액션"},chips:{alignment:"정렬"},weather:{show_conditions:"조건 표시",show_temperature:"온도 표시"},update:{show_buttons_control:"컨트롤 버튼 표시"},vacuum:{commands:"명령어",commands_list:{on_off:"켜기/끄기"}},"media-player":{use_media_info:"미디어 정보 사용",use_media_artwork:"미디어 아트워크 사용",show_volume_level:"볼륨 레벨 표시",media_controls:"미디어 컨트롤",media_controls_list:{on_off:"켜기/끄기",shuffle:"섞기",previous:"이전 트랙",play_pause_stop:"재생/일시 정지/정지",next:"다음 트랙",repeat:"반복 모드"},volume_controls:"볼륨 컨트롤",volume_controls_list:{volume_buttons:"볼륨 버튼",volume_set:"볼륨 레벨",volume_mute:"음소거"}},lock:{lock:"잠금",unlock:"잠금 해제",open:"열기"},humidifier:{show_target_humidity_control:"습도 조절 표시"},climate:{show_temperature_control:"온도 조절 표시",hvac_modes:"HVAC 모드"}},chip:{sub_element_editor:{title:"칩 에디터"},conditional:{chip:"칩"},"chip-picker":{chips:"칩",add:"칩 추가",edit:"수정",clear:"클리어",select:"칩 선택",types:{action:"액션","alarm-control-panel":"알람",back:"이전",conditional:"Conditional",entity:"엔티티",light:"조명",menu:"메뉴",template:"템플릿",weather:"날씨"}}}},zi={editor:Ti},Oi={form:{color_picker:{values:{default:"Standard farge"}},info_picker:{values:{default:"Standard informasjon",name:"Navn",state:"Tilstand","last-changed":"Sist endret","last-updated":"Sist oppdatert",none:"Ingen"}},layout_picker:{values:{default:"Standardoppsett",vertical:"Vertikalt oppsett",horizontal:"Horisontalt oppsett"}},alignment_picker:{values:{default:"Standard justering",start:"Start",end:"Slutt",center:"Senter",justify:"Bekreft"}}},card:{generic:{icon_color:"Ikon farge",layout:"Oppsett",primary_info:"Primærinformasjon",secondary_info:"Sekundærinformasjon",content_info:"Innhold",use_entity_picture:"Bruk enhetsbilde?",icon_animation:"Animer ikon når aktivt?"},light:{show_brightness_control:"Lysstyrkekontroll?",use_light_color:"Bruk lys farge",show_color_temp_control:"Temperatur fargekontroll?",show_color_control:"Fargekontroll?",incompatible_controls:"Noen kontroller vises kanskje ikke hvis lyset ditt ikke støtter denne funksjonen."},fan:{show_percentage_control:"Prosentvis kontroll?",show_oscillate_control:"Oscillerende kontroll?"},cover:{show_buttons_control:"Kontollere med knapper?",show_position_control:"Posisjonskontroll?"},template:{primary:"Primærinformasjon",secondary:"Sekundærinformasjon",multiline_secondary:"Multiline sekundær?",entity_extra:"Brukes i maler og handlinger",content:"Inhold"},title:{title:"Tittel",subtitle:"Undertekst"},chips:{alignment:"Justering"},weather:{show_conditions:"Forhold?",show_temperature:"Temperatur?"},vacuum:{commands:"Kommandoer"}},chip:{sub_element_editor:{title:"Chip redaktør"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Legg til chip",edit:"Endre",clear:"Klare",select:"Velg chip",types:{action:"Handling","alarm-control-panel":"Alarm",back:"Tilbake",conditional:"Betinget",entity:"Entitet",light:"Lys",menu:"Meny",template:"Mal",weather:"Vær"}}}},Mi={editor:Oi},Di={form:{color_picker:{values:{default:"Standaard kleur"}},info_picker:{values:{default:"Standaard informatie",name:"Naam",state:"Staat","last-changed":"Laatst gewijzigd","last-updated":"Laatst bijgewerkt",none:"Geen"}},icon_type_picker:{values:{default:"Standaard icoon type",icon:"Icoon","entity-picture":"Entiteit afbeelding",none:"Geen"}},layout_picker:{values:{default:"Standaard lay-out",vertical:"Verticale lay-out",horizontal:"Horizontale lay-out"}},alignment_picker:{values:{default:"Standaard uitlijning",start:"Begin",end:"Einde",center:"Midden",justify:"Uitlijnen "}}},card:{generic:{icon_color:"Icoon kleur",layout:"Lay-out",fill_container:"Vul container",primary_info:"Primaire informatie",secondary_info:"Secundaire informatie",icon_type:"Icoon type",content_info:"Inhoud",use_entity_picture:"Gebruik entiteit afbeelding",collapsible_controls:"Bedieningselementen verbergen wanneer uitgeschakeld",icon_animation:"Pictogram animeren indien actief"},light:{show_brightness_control:"Bediening helderheid",use_light_color:"Gebruik licht kleur",show_color_temp_control:"Bediening kleurtemperatuur",show_color_control:"Bediening kleur",incompatible_controls:"Sommige bedieningselementen worden mogelijk niet weergegeven als uw lamp deze functie niet ondersteunt."},fan:{show_percentage_control:"Bediening middels percentage",show_oscillate_control:"Bediening oscillatie"},cover:{show_buttons_control:"Toon knoppen",show_position_control:"Toon positie bediening",show_tilt_position_control:"Toon tilt control"},alarm_control_panel:{show_keypad:"Toon toetsenbord"},template:{primary:"Primaire informatie",secondary:"Secundaire informatie",multiline_secondary:"Secundaire informatie op meerdere lijnen weergeven",entity_extra:"Gebruikt in sjablonen en acties",content:"Inhoud",badge_icon:"Badge icoon",badge_color:"Badge kleur",picture:"Afbeelding (zal het icoon vervangen)"},title:{title:"Titel",subtitle:"Ondertitel",title_tap_action:"Titel tik actie",subtitle_tap_action:"Ondertitel tik actie"},chips:{alignment:"Uitlijning"},weather:{show_conditions:"Weerbeeld",show_temperature:"Temperatuur"},update:{show_buttons_control:"Bedieningsknoppen"},vacuum:{commands:"Commando's",commands_list:{on_off:"Zet aan/uit"}},"media-player":{use_media_info:"Gebruik media informatie",use_media_artwork:"Gebruik media omslag",show_volume_level:"Toon volumeniveau",media_controls:"Mediabediening",media_controls_list:{on_off:"zet aan/uit",shuffle:"Shuffle",previous:"Vorige nummer",play_pause_stop:"Speel/pauze/stop",next:"Volgende nummer",repeat:"Herhalen"},volume_controls:"Volumeregeling",volume_controls_list:{volume_buttons:"Volume knoppen",volume_set:"Volumeniveau",volume_mute:"Dempen"}},lock:{lock:"Vergrendel",unlock:"Ontgrendel",open:"Open"},humidifier:{show_target_humidity_control:"Vochtigheid controle?"},climate:{show_temperature_control:"Temperatuur controle",hvac_modes:"HVAC Modes"},number:{display_mode:"Weergave Modus",display_mode_list:{default:"Standaard (schuifbalk)",slider:"Schuifbalk",buttons:"Knoppen"}}},chip:{sub_element_editor:{title:"Chip editor"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Toevoegen chip",edit:"Bewerk",clear:"Maak leeg",select:"Selecteer chip",types:{action:"Actie","alarm-control-panel":"Alarm",back:"Terug",conditional:"Voorwaardelijk",entity:"Entiteit",light:"Licht",menu:"Menu",template:"Sjabloon",weather:"Weer"}}}},Li={editor:Di},ji={form:{color_picker:{values:{default:"Domyślny kolor"}},info_picker:{values:{default:"Domyślne informacje",name:"Nazwa",state:"Stan","last-changed":"Ostatnia zmiana","last-updated":"Ostatnia aktualizacja",none:"Brak"}},icon_type_picker:{values:{default:"Domyślny typ",icon:"Ikona","entity-picture":"Obraz encji",none:"Brak"}},layout_picker:{values:{default:"Układ domyślny",vertical:"Układ pionowy",horizontal:"Układ poziomy"}},alignment_picker:{values:{default:"Wyrównanie domyślne",start:"Wyrównanie do lewej",end:"Wyrównanie do prawej",center:"Wyśrodkowanie",justify:"Justowanie"}}},card:{generic:{icon_color:"Kolor ikony",layout:"Układ",fill_container:"Wypełnij zawartością",primary_info:"Informacje główne",secondary_info:"Informacje drugorzędne",icon_type:"Typ ikony",content_info:"Zawartość",use_entity_picture:"Użyć obrazu encji?",collapsible_controls:"Zwiń sterowanie, jeśli wyłączone",icon_animation:"Animować, gdy aktywny?"},light:{show_brightness_control:"Sterowanie jasnością?",use_light_color:"Użyj koloru światła",show_color_temp_control:"Sterowanie temperaturą światła?",show_color_control:"Sterowanie kolorami?",incompatible_controls:"Niektóre funkcje są niewidoczne, jeśli światło ich nie obsługuje."},fan:{show_percentage_control:"Sterowanie procentowe?",show_oscillate_control:"Sterowanie oscylacją?"},cover:{show_buttons_control:"Przyciski sterujące?",show_position_control:"Sterowanie położeniem?",show_tilt_position_control:"Sterowanie poziomem otwarcia?"},alarm_control_panel:{show_keypad:"Wyświetl klawiaturę"},template:{primary:"Informacje główne",secondary:"Informacje drugorzędne",multiline_secondary:"Drugorzędne wielowierszowe?",entity_extra:"Używane w szablonach i akcjach",content:"Zawartość",badge_icon:"Ikona odznaki",badge_color:"Kolor odznaki",picture:"Obraz (zamiast ikony)"},title:{title:"Tytuł",subtitle:"Podtytuł"},chips:{alignment:"Wyrównanie"},weather:{show_conditions:"Warunki?",show_temperature:"Temperatura?"},update:{show_buttons_control:"Przyciski sterujące?"},vacuum:{commands:"Polecenia"},"media-player":{use_media_info:"Użyj informacji o multimediach",use_media_artwork:"Użyj okładek multimediów",show_volume_level:"Wyświetl poziom głośności",media_controls:"Sterowanie multimediami",media_controls_list:{on_off:"Włącz/wyłącz",shuffle:"Losowo",previous:"Poprzednie nagranie",play_pause_stop:"Odtwórz/Pauza/Zatrzymaj",next:"Następne nagranie",repeat:"Powtarzanie"},volume_controls:"Sterowanie głośnością",volume_controls_list:{volume_buttons:"Przyciski głośności",volume_set:"Poziom głośności",volume_mute:"Wycisz"}},lock:{lock:"Zablokuj",unlock:"Odblokuj",open:"Otwórz"},humidifier:{show_target_humidity_control:"Sterowanie wilgotnością?"},climate:{show_temperature_control:"Sterowanie temperaturą?",hvac_modes:"Tryby urządzenia"}},chip:{sub_element_editor:{title:"Edytor czipów"},conditional:{chip:"Czip"},"chip-picker":{chips:"Czipy",add:"Dodaj czip",edit:"Edytuj",clear:"Wyczyść",select:"Wybierz czip",types:{action:"Akcja","alarm-control-panel":"Alarm",back:"Wstecz",conditional:"Warunkowy",entity:"Encja",light:"Światło",menu:"Menu",template:"Szablon",weather:"Pogoda"}}}},Pi={editor:ji},Ni={form:{color_picker:{values:{default:"Cor padrão"}},info_picker:{values:{default:"Informações padrão",name:"Nome",state:"Estado","last-changed":"Última alteração","last-updated":"Última atualização",none:"Nenhum"}},layout_picker:{values:{default:"Layout padrão",vertical:"Layout vertical",horizontal:"Layout horizontal"}},alignment_picker:{values:{default:"Padrão (inicio)",end:"Final",center:"Centro",justify:"Justificado"}}},card:{generic:{icon_color:"Cor do ícone?",layout:"Layout",primary_info:"Informações primárias",secondary_info:"Informações secundárias",use_entity_picture:"Usar imagem da entidade?",icon_animation:"Animar ícone quando ativo?"},light:{show_brightness_control:"Mostrar controle de brilho?",use_light_color:"Usar cor da luz?",show_color_temp_control:"Mostrar controle de temperatura?",show_color_control:"Mostrar controle de cor?",incompatible_controls:"Alguns controles podem não ser exibidos se sua luz não suportar o recurso."},fan:{show_percentage_control:"Mostrar controle de porcentagem?",show_oscillate_control:"Mostrar controle de oscilação?"},cover:{show_buttons_control:"Mostrar botões?",show_position_control:"Mostrar controle de posição?"},template:{primary:"Informações primárias",secondary:"Informações secundárias",multiline_secondary:"Multilinha secundária?",content:"Conteúdo"},title:{title:"Título",subtitle:"Subtítulo"},chips:{alignment:"Alinhamento"},weather:{show_conditions:"Condições?",show_temperature:"Temperatura?"}},chip:{sub_element_editor:{title:"Editor de fichas"},conditional:{chip:"Ficha"},"chip-picker":{chips:"Fichas",add:"Adicionar ficha",edit:"Editar",clear:"Limpar",select:"Selecionar ficha",types:{action:"Ação","alarm-control-panel":"Alarme",back:"Voltar",conditional:"Condicional",entity:"Entidade",light:"Iluminação",menu:"Menu",template:"Modelo",weather:"Clima"}}}},Ri={editor:Ni},Vi={form:{color_picker:{values:{default:"Cor padrão"}},info_picker:{values:{default:"Informações padrão",name:"Nome",state:"Estado","last-changed":"Última alteração","last-updated":"Última atualização",none:"Nenhum"}},layout_picker:{values:{default:"Layout padrão",vertical:"Layout vertical",horizontal:"Layout horizontal"}},alignment_picker:{values:{default:"Padrão (inicio)",end:"Fim",center:"Centrado",justify:"Justificado"}}},card:{generic:{icon_color:"Cor do ícone?",layout:"Layout",primary_info:"Informações primárias",secondary_info:"Informações secundárias",use_entity_picture:"Usar imagem da entidade?",icon_animation:"Animar ícone quando ativo?"},light:{show_brightness_control:"Mostrar controle de brilho?",use_light_color:"Usar cor da luz?",show_color_temp_control:"Mostrar controle de temperatura?",show_color_control:"Mostrar controle de cor?",incompatible_controls:"Alguns controles podem não ser exibidos se a luz não suportar o recurso."},fan:{show_percentage_control:"Mostrar controle de porcentagem?",show_oscillate_control:"Mostrar controle de oscilação?"},cover:{show_buttons_control:"Mostrar botões?",show_position_control:"Mostrar controle de posição?"},template:{primary:"Informações primárias",secondary:"Informações secundárias",multiline_secondary:"Multilinha secundária?",content:"Conteúdo"},title:{title:"Título",subtitle:"Subtítulo"},chips:{alignment:"Alinhamento"},weather:{show_conditions:"Condições?",show_temperature:"Temperatura?"}},chip:{sub_element_editor:{title:"Editor de fichas"},conditional:{chip:"Ficha"},"chip-picker":{chips:"Fichas",add:"Adicionar ficha",edit:"Editar",clear:"Limpar",select:"Selecionar ficha",types:{action:"Ação","alarm-control-panel":"Alarme",back:"Voltar",conditional:"Condicional",entity:"Entidade",light:"Iluminação",menu:"Menu",template:"Modelo",weather:"Clima"}}}},Fi={editor:Vi},Bi={form:{color_picker:{values:{default:"Culoare implicită"}},info_picker:{values:{default:"Informație implicită",name:"Nume",state:"Stare","last-changed":"Ultima modificare","last-updated":"Ultima actulizare",none:"Niciuna"}},icon_type_picker:{values:{default:"Tip implicit",icon:"Pictogramă","entity-picture":"Imagine",none:"Niciuna"}},layout_picker:{values:{default:"Aranjare implicită",vertical:"Verticală",horizontal:"Orizontală"}},alignment_picker:{values:{default:"Aliniere implicită",start:"Stânga",end:"Dreapta",center:"Centrat",justify:"Umplere"}}},card:{generic:{icon_color:"Culoare pictogramă",layout:"Aranjare",fill_container:"Umplere container",primary_info:"Informație principală",secondary_info:"Informație secundară",icon_type:"Tip pictogramă",content_info:"Conținut",use_entity_picture:"Imagine?",collapsible_controls:"Restrângere la dezactivare"},light:{show_brightness_control:"Comandă pentru strălucire?",use_light_color:"Folosește culoarea luminii",show_color_temp_control:"Comandă pentru temperatură de culoare?",show_color_control:"Comandă pentru culoare?",incompatible_controls:"Unele comenzi ar putea să nu fie afișate dacă lumina nu suportă această caracteristică."},fan:{icon_animation:"Animare pictograma la activare?",show_percentage_control:"Comandă procent?",show_oscillate_control:"Comandă oscilație?"},cover:{show_buttons_control:"Comenzi pentru control?",show_position_control:"Comandă pentru poziție?",show_tilt_position_control:"Comandă pentru înclinare?"},alarm_control_panel:{show_keypad:"Arată tastatura"},template:{primary:"Informație principală",secondary:"Informație secundară",multiline_secondary:"Informație secundară pe mai multe linii?",entity_extra:"Folosită în șabloane și acțiuni",content:"Conținut",badge_icon:"Pictogramă insignă",badge_color:"Culoare insignă",picture:"Imagine (inlocuiește pictograma)"},title:{title:"Titlu",subtitle:"Subtitlu"},chips:{alignment:"Aliniere"},weather:{show_conditions:"Condiții?",show_temperature:"Temperatură?"},update:{show_buttons_control:"Comenzi control?"},vacuum:{commands:"Comenzi"},"media-player":{use_media_info:"Informații media",use_media_artwork:"Grafică media",show_volume_level:"Nivel volum",media_controls:"Comenzi media",media_controls_list:{on_off:"Pornit/Oprit",shuffle:"Amestecare",previous:"Pista anterioară",play_pause_stop:"Redare/Pauză/Stop",next:"Pista următoare",repeat:"Mod repetare"},volume_controls:"Comenzi volum",volume_controls_list:{volume_buttons:"Comenzi volum",volume_set:"Nivel volum",volume_mute:"Dezactivare sunet"}},lock:{lock:"Încuie",unlock:"Descuie",open:"Deschide"},humidifier:{show_target_humidity_control:"Comenzi umiditate?"},climate:{show_temperature_control:"Comenzi temperatură?",hvac_modes:"Moduri HVAC"}},chip:{sub_element_editor:{title:"Editor jeton"},conditional:{chip:"Jeton"},"chip-picker":{chips:"Jetoane",add:"Adaugă jeton",edit:"Modifică",clear:"Șterge",select:"Alege jeton",types:{action:"Acțiune","alarm-control-panel":"Alarmă",back:"Înapoi",conditional:"Condițional",entity:"Entitate",light:"Lumină",menu:"Meniu",template:"Șablon",weather:"Vreme"}}}},Ui={editor:Bi},Hi={form:{color_picker:{values:{default:"Цвет по-умолчанию"}},info_picker:{values:{default:"По-умолчанию",name:"Имя",state:"Статус","last-changed":"Последнее изменение","last-updated":"Последнее обновление",none:"Нет"}},icon_type_picker:{values:{default:"По-умолчанию",icon:"Иконка","entity-picture":"Изображение",none:"Нет"}},layout_picker:{values:{default:"Расположение по-умолчанию",vertical:"Вертикальное расположение",horizontal:"Горизонтальное расположение"}},alignment_picker:{values:{default:"Выравнивание по-умолчанию",start:"По правому краю",end:"По левому краю",center:"По-центру",justify:"На всю ширину"}}},card:{generic:{icon_color:"Цвет иконки",layout:"Расположение",fill_container:"Заполнение",primary_info:"Основная информация",secondary_info:"Второстепенная информация",icon_type:"Тип иконки",content_info:"Содержимое",use_entity_picture:"Использовать изображение объекта?",collapsible_controls:"Сворачивать элементы управления при выключении"},light:{show_brightness_control:"Управлять яркостью?",use_light_color:"Использовать текущий цвет света",show_color_temp_control:"Управлять цветовой температурой?",show_color_control:"Управлять цветом?",incompatible_controls:"Некоторые элементы управления могут не отображаться, если ваш светильник не поддерживает эти функции."},fan:{icon_animation:"Анимировать иконку когда включено?",show_percentage_control:"Управлять процентами?",show_oscillate_control:"Oscillate control?"},cover:{show_buttons_control:"Добавить кнопки управления?",show_position_control:"Управлять позицией?",show_tilt_position_control:"Управлять наклоном?"},alarm_control_panel:{show_keypad:"Показ клавиатуры"},template:{primary:"Основная информация",secondary:"Второстепенная информация",multiline_secondary:"Многострочная Второстепенная информация?",entity_extra:"Используется в шаблонах и действиях",content:"Содержимое",badge_icon:"Иконка значка",badge_color:"Цвет значка",picture:"Изображение (заменить иконку)"},title:{title:"Заголовок",subtitle:"Подзаголовок"},chips:{alignment:"Выравнивание"},weather:{show_conditions:"Условия?",show_temperature:"Температура?"},update:{show_buttons_control:"Кнопки управления?"},vacuum:{commands:"Команды"},"media-player":{use_media_info:"Использовать информацию с медиа-устройства",use_media_artwork:"Использовать обложку с медиа-устройства",show_volume_level:"Показать уровень громкости",media_controls:"Управление медиа-устройством",media_controls_list:{on_off:"Включение/выключение",shuffle:"Перемешивание",previous:"Предыдущий трек",play_pause_stop:"Воспроизведение/пауза/остановка",next:"Следующий трек",repeat:"Режим повтора"},volume_controls:"Регулятор громкости",volume_controls_list:{volume_buttons:"Кнопки громкости",volume_set:"Уровень громкости",volume_mute:"Без звука"}},lock:{lock:"Закрыто",unlock:"Разблокировано",open:"Открыто"},humidifier:{show_target_humidity_control:"Управлять целевым уровенем влажности?"},climate:{show_temperature_control:"Управлять целевой температурой?",hvac_modes:"Режимы работы"}},chip:{sub_element_editor:{title:"Редактор мини-карточек"},conditional:{chip:"Мини-карточка"},"chip-picker":{chips:"Мини-карточки",add:"Добавить мини-карточку",edit:"Изменить",clear:"Очистить",select:"Выбрать мини-карточку",types:{action:"Действие","alarm-control-panel":"Тревога",back:"Назад",conditional:"Условия",entity:"Объект",light:"Освещение",menu:"Меню",template:"Шаблон",weather:"Погода"}}}},Yi={editor:Hi},Xi={form:{color_picker:{values:{default:"Predvolená farba"}},info_picker:{values:{default:"Predvolené informácie",name:"Názov",state:"Stav","last-changed":"Posledná zmena","last-updated":"Posledná aktualizácia",none:"Žiadna"}},icon_type_picker:{values:{default:"Predvolený typ",icon:"Ikona","entity-picture":"Obrázok entity",none:"Žiadny"}},layout_picker:{values:{default:"Predvolené rozloženie",vertical:"Zvislé rozloženie",horizontal:"Vodorovné rozloženie"}},alignment_picker:{values:{default:"Predvolené zarovnanie",start:"Začiatok",end:"Koniec",center:"Stred",justify:"Vyplniť"}}},card:{generic:{icon_color:"Farba ikony",layout:"Rozloženie",fill_container:"Vyplniť priestor",primary_info:"Základné info",secondary_info:"Doplnkové info",icon_type:"Typ ikony",content_info:"Obsah",use_entity_picture:"Použiť obrázok entity?",collapsible_controls:"Skryť ovládanie v stave VYP.",icon_animation:"Animovaná ikona v stave ZAP?"},light:{show_brightness_control:"Ovládanie jasu?",use_light_color:"Použiť farbu svetla",show_color_temp_control:"Ovládanie teploty?",show_color_control:"Ovládanie farby?",incompatible_controls:"Niektoré ovládacie prvky sa nemusia zobraziť, pokiaľ ich svetlo nepodporuje."},fan:{show_percentage_control:"Ovládanie rýchlosti v percentách?",show_oscillate_control:"Ovládanie oscilácie?"},cover:{show_buttons_control:"Zobraziť ovládacie tlačidlá?",show_position_control:"Ovládanie pozície?",show_tilt_position_control:"Ovládanie natočenia?"},alarm_control_panel:{show_keypad:"Zobraziť klávesnicu"},template:{primary:"Základné info",secondary:"Doplnkové info",multiline_secondary:"Viacriadkové doplnkové info?",entity_extra:"Použitá v šablónach a akciách",content:"Obsah",badge_icon:"Ikona odznaku",badge_color:"Farba odznaku",picture:"Obrázok (nahrádza ikonu)"},title:{title:"Nadpis",subtitle:"Podnadpis"},chips:{alignment:"Zarovnanie"},weather:{show_conditions:"Zobraziť podmienky?",show_temperature:"Zobraziť teplotu?"},update:{show_buttons_control:"Zobraziť ovládacie tlačidlá?"},vacuum:{commands:"Príkazy"},"media-player":{use_media_info:"Použiť info o médiu",use_media_artwork:"Použiť obrázok z média",show_volume_level:"Zobraziť úroveň hlasitosti",media_controls:"Ovládanie média",media_controls_list:{on_off:"Zap / Vyp",shuffle:"Premiešať",previous:"Predchádzajúca",play_pause_stop:"Spustiť/pauza/stop",next:"Ďalšia",repeat:"Opakovať"},volume_controls:"Ovládanie hlasitosti",volume_controls_list:{volume_buttons:"Tlačidlá hlasitosti",volume_set:"Úroveň hlasitosti",volume_mute:"Stlmiť"}},lock:{lock:"Zamknuté",unlock:"Odomknuté",open:"Otvorené"},humidifier:{show_target_humidity_control:"Ovládanie vlhkosti?"},climate:{show_temperature_control:"Ovládanie teploty?",hvac_modes:"HVAC mód"}},chip:{sub_element_editor:{title:"Editor štítkov"},conditional:{chip:"Štítok"},"chip-picker":{chips:"Štítky",add:"Pridať štítok",edit:"Editovať",clear:"Vymazať",select:"Vybrať štítok",types:{action:"Akcia","alarm-control-panel":"Alarm",back:"Späť",conditional:"Podmienka",entity:"Entita",light:"Svetlo",menu:"Menu",template:"Šablóna",weather:"Počasie"}}}},Wi={editor:Xi},qi={form:{color_picker:{values:{default:"Standardfärg"}},info_picker:{values:{default:"Förvald information",name:"Namn",state:"Status","last-changed":"Sist ändrad","last-updated":"Sist uppdaterad",none:"Ingen"}},layout_picker:{values:{default:"Standard",vertical:"Vertikal",horizontal:"Horisontell"}},alignment_picker:{values:{default:"Standard (början)",end:"Slutet",center:"Centrerad",justify:"Anpassa"}}},card:{generic:{icon_color:"Ikonens färg",layout:"Layout",primary_info:"Primär information",secondary_info:"Sekundär information",use_entity_picture:"Använd enheten bild?",icon_animation:"Animera ikonen när fläkten är på?"},light:{show_brightness_control:"Styr ljushet?",use_light_color:"Styr ljusets färg",show_color_temp_control:"Styr färgtemperatur?",show_color_control:"Styr färg?",incompatible_controls:"Kontroller som inte stöds av enheten kommer inte visas."},fan:{show_percentage_control:"Procentuell kontroll?",show_oscillate_control:"Kontroll för oscillera?"},cover:{show_buttons_control:"Visa kontrollknappar?",show_position_control:"Visa positionskontroll?"},template:{primary:"Primär information",secondary:"Sekundär information",multiline_secondary:"Sekundär med flera rader?",content:"Innehåll"},title:{title:"Rubrik",subtitle:"Underrubrik"},chips:{alignment:"Justering"},weather:{show_conditions:"Förhållanden?",show_temperature:"Temperatur?"}},chip:{sub_element_editor:{title:"Chipredigerare"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Lägg till chip",edit:"Redigera",clear:"Rensa",select:"Välj chip",types:{action:"Händelse","alarm-control-panel":"Alarm",back:"Bakåt",conditional:"Villkorad",entity:"Enhet",light:"Ljus",menu:"Meny",template:"Mall",weather:"Väder"}}}},Ki={editor:qi},Gi={form:{color_picker:{values:{default:"Varsayılan renk"}},info_picker:{values:{default:"Varsayılan bilgi",name:"İsim",state:"Durum","last-changed":"Son Değişim","last-updated":"Son Güncelleme",none:"None"}},layout_picker:{values:{default:"Varsayılan düzen",vertical:"Dikey düzen",horizontal:"Yatay düzen"}},alignment_picker:{values:{default:"Varsayılan hizalama",start:"Sola yasla",end:"Sağa yasla",center:"Ortala",justify:"İki yana yasla"}}},card:{generic:{icon_color:"Simge renki",layout:"Düzen",primary_info:"Birinci bilgi",secondary_info:"İkinci bilgi",content_info:"İçerik",use_entity_picture:"Varlık resmi kullanılsın",icon_animation:"Aktif olduğunda simgeyi hareket ettir"},light:{show_brightness_control:"Parlaklık kontrolü",use_light_color:"Işık rengini kullan",show_color_temp_control:"Renk ısısı kontrolü",show_color_control:"Renk kontrolü",incompatible_controls:"Kullandığınız lamba bu özellikleri desteklemiyorsa bazı kontroller görüntülenemeyebilir."},fan:{show_percentage_control:"Yüzde kontrolü",show_oscillate_control:"Salınım kontrolü"},cover:{show_buttons_control:"Düğme kontrolleri",show_position_control:"Pozisyon kontrolü"},template:{primary:"Birinci bilgi",secondary:"İkinci bilgi",multiline_secondary:"İkinci bilgi çok satır olsun",entity_extra:"Şablonlarda ve eylemlerde kullanılsın",content:"İçerik"},title:{title:"Başlık",subtitle:"Altbaşlık"},chips:{alignment:"Hizalama"},weather:{show_conditions:"Hava koşulu",show_temperature:"Sıcaklık"},update:{show_buttons_control:"Düğme kontrolü"},vacuum:{commands:"Komutlar"}},chip:{sub_element_editor:{title:"Chip düzenleyici"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"Chip ekle",edit:"Düzenle",clear:"Temizle",select:"Chip seç",types:{action:"Eylem","alarm-control-panel":"Alarm",back:"Geri",conditional:"Koşullu",entity:"Varlık",light:"Işık",menu:"Menü",template:"Şablon",weather:"Hava Durumu"}}}},Zi={editor:Gi},Ji={form:{color_picker:{values:{default:"Màu mặc định"}},info_picker:{values:{default:"Thông tin mặc định",name:"Tên",state:"Trạng thái","last-changed":"Lần cuối thay đổi","last-updated":"Lần cuối cập nhật",none:"Rỗng"}},layout_picker:{values:{default:"Bố cục mặc định",vertical:"Bố cục dọc",horizontal:"Bố cục ngang"}},alignment_picker:{values:{default:"Căn chỉnh mặc định",start:"Căn đầu",end:"Căn cuối",center:"Căn giữa",justify:"Căn hai bên"}}},card:{generic:{icon_color:"Màu biểu tượng",layout:"Bố cục",fill_container:"Làm đầy",primary_info:"Thông tin chính",secondary_info:"Thông tin phụ",content_info:"Nội dung",use_entity_picture:"Dùng ảnh của thực thể?",collapsible_controls:"Thu nhỏ điều kiển khi tắt",icon_animation:"Biểu tượng hoạt ảnh khi hoạt động?"},light:{show_brightness_control:"Điều khiển độ sáng?",use_light_color:"Dùng ánh sáng màu",show_color_temp_control:"Điều khiển nhiệt độ màu?",show_color_control:"Điều khiển màu sắc?",incompatible_controls:"Một số màu sẽ không được hiển thị nếu đèn của bạn không hỗ trợ tính năng này."},fan:{show_percentage_control:"Điều khiển dạng phần trăm?",show_oscillate_control:"Điều khiển xoay?"},cover:{show_buttons_control:"Nút điều khiển?",show_position_control:"Điều khiển vị trí?"},alarm_control_panel:{show_keypad:"Hiện bàn phím"},template:{primary:"Thông tin chính",secondary:"Thông tin phụ",multiline_secondary:"Nhiều dòng thông tin phụ?",entity_extra:"Được sử dụng trong mẫu và hành động",content:"Nội dung"},title:{title:"Tiêu đề",subtitle:"Phụ đề"},chips:{alignment:"Căn chỉnh"},weather:{show_conditions:"Điều kiện?",show_temperature:"Nhiệt độ?"},update:{show_buttons_control:"Nút điều khiển?"},vacuum:{commands:"Mệnh lệnh"},"media-player":{use_media_info:"Dùng thông tin đa phương tiện",use_media_artwork:"Dùng ảnh đa phương tiện",media_controls:"Điều khiển đa phương tiện",media_controls_list:{on_off:"Bật/Tắt",shuffle:"Xáo trộn",previous:"Bài trước",play_pause_stop:"Phát/Tạm dừng/Dừng",next:"Bài tiếp theo",repeat:"Chế độ lặp lại"},volume_controls:"Điều khiển âm lượng",volume_controls_list:{volume_buttons:"Nút âm lượng",volume_set:"Mức âm lượng",volume_mute:"Im lặng"}},lock:{lock:"Khóa",unlock:"Mở khóa",open:"Mở"}},chip:{sub_element_editor:{title:"Chỉnh sửa chip"},conditional:{chip:"Chip"},"chip-picker":{chips:"Các chip",add:"Thêm chip",edit:"Chỉnh sửa",clear:"Làm mới",select:"Chọn chip",types:{action:"Hành động","alarm-control-panel":"Báo động",back:"Quay về",conditional:"Điều kiện",entity:"Thực thể",light:"Đèn",menu:"Menu",template:"Mẫu",weather:"Thời tiết"}}}},Qi={editor:Ji},to={form:{color_picker:{values:{default:"默认颜色"}},info_picker:{values:{default:"默认信息",name:"名称",state:"状态","last-changed":"变更时间","last-updated":"更新时间",none:"无"}},layout_picker:{values:{default:"默认布局",vertical:"垂直布局",horizontal:"水平布局"}},alignment_picker:{values:{default:"默认 (左对齐)",end:"右对齐",center:"居中对齐",justify:"两端对齐"}}},card:{generic:{icon_color:"图标颜色",primary_info:"首要信息",secondary_info:"次要信息",use_entity_picture:"使用实体图片?",icon_animation:"激活时使用动态图标?"},light:{show_brightness_control:"亮度控制?",use_light_color:"使用灯光颜色",show_color_temp_control:"色温控制?",show_color_control:"颜色控制?",incompatible_controls:"设备不支持的控制器将不会显示。"},fan:{show_percentage_control:"百分比控制?",show_oscillate_control:"摆动控制?"},cover:{show_buttons_control:"按钮控制?",show_position_control:"位置控制?"},template:{primary:"首要信息",secondary:"次要信息",multiline_secondary:"多行次要信息?",content:"内容"},title:{title:"标题",subtitle:"子标题"},chips:{alignment:"对齐"},weather:{show_conditions:"条件?",show_temperature:"温度?"}},chip:{sub_element_editor:{title:"Chip 编辑"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"添加 chip",edit:"编辑",clear:"清除",select:"选择 chip",types:{action:"动作","alarm-control-panel":"警戒控制台",back:"返回",conditional:"条件显示",entity:"实体",light:"灯光",menu:"菜单",template:"模板",weather:"天气"}}}},eo={editor:to},io={form:{color_picker:{values:{default:"預設顏色"}},info_picker:{values:{default:"預設訊息",name:"名稱",state:"狀態","last-changed":"最近變動時間","last-updated":"最近更新時間",none:"無"}},icon_type_picker:{values:{default:"預設樣式",icon:"圖示","entity-picture":"實體圖片",none:"無"}},layout_picker:{values:{default:"預設佈局",vertical:"垂直佈局",horizontal:"水平佈局"}},alignment_picker:{values:{default:"預設對齊",start:"居左對齊",end:"居右對齊",center:"居中對齊",justify:"兩端對齊"}}},card:{generic:{icon_color:"圖示顏色",layout:"佈局",fill_container:"填滿容器",primary_info:"主要訊息",secondary_info:"次要訊息",icon_type:"圖示樣式",content_info:"內容",use_entity_picture:"使用實體圖片?",collapsible_controls:"關閉時隱藏控制項",icon_animation:"啟動時使用動態圖示?"},light:{show_brightness_control:"亮度控制?",use_light_color:"使用燈光顏色",show_color_temp_control:"色溫控制?",show_color_control:"色彩控制?",incompatible_controls:"裝置不支援的控制不會顯示。"},fan:{show_percentage_control:"百分比控制?",show_oscillate_control:"擺頭控制?"},cover:{show_buttons_control:"按鈕控制?",show_position_control:"位置控制?",show_tilt_position_control:"傾斜控制?"},alarm_control_panel:{show_keypad:"顯示鍵盤"},template:{primary:"主要訊息",secondary:"次要訊息",multiline_secondary:"多行次要訊息?",entity_extra:"用於模板與動作",content:"內容",badge_icon:"角標圖示",badge_color:"角標顏色",picture:"圖片(將會取代圖示)"},title:{title:"標題",subtitle:"副標題",title_tap_action:"標題點擊動作",subtitle_tap_action:"副標題點擊動作"},chips:{alignment:"對齊"},weather:{show_conditions:"狀況?",show_temperature:"溫度?"},update:{show_buttons_control:"按鈕控制?"},vacuum:{commands:"指令",commands_list:{on_off:"開啟、關閉"}},"media-player":{use_media_info:"使用媒體資訊",use_media_artwork:"使用媒體插圖",show_volume_level:"顯示音量大小",media_controls:"媒體控制",media_controls_list:{on_off:"開啟、關閉",shuffle:"隨機播放",previous:"上一首",play_pause_stop:"播放、暫停、停止",next:"下一首",repeat:"重複播放"},volume_controls:"音量控制",volume_controls_list:{volume_buttons:"音量按鈕",volume_set:"音量等級",volume_mute:"靜音"}},lock:{lock:"上鎖",unlock:"解鎖",open:"打開"},humidifier:{show_target_humidity_control:"溼度控制?"},climate:{show_temperature_control:"溫度控制?",hvac_modes:"空調模式"}},chip:{sub_element_editor:{title:"Chip 編輯"},conditional:{chip:"Chip"},"chip-picker":{chips:"Chips",add:"新增 chip",edit:"編輯",clear:"清除",select:"選擇 chip",types:{action:"動作","alarm-control-panel":"警報器控制",back:"返回",conditional:"條件",entity:"實體",light:"燈光",menu:"選單",template:"模板",weather:"天氣"}}}},oo={editor:io};const no={ar:Object.freeze({__proto__:null,default:oi,editor:ii}),bg:Object.freeze({__proto__:null,default:ri,editor:ni}),ca:Object.freeze({__proto__:null,default:li,editor:ai}),cs:Object.freeze({__proto__:null,default:ci,editor:si}),da:Object.freeze({__proto__:null,default:ui,editor:di}),de:Object.freeze({__proto__:null,default:mi,editor:hi}),el:Object.freeze({__proto__:null,default:fi,editor:pi}),en:Object.freeze({__proto__:null,default:_i,editor:gi}),es:Object.freeze({__proto__:null,default:bi,editor:vi}),fi:Object.freeze({__proto__:null,default:xi,editor:yi}),fr:Object.freeze({__proto__:null,default:ki,editor:wi}),he:Object.freeze({__proto__:null,default:$i,editor:Ci}),hu:Object.freeze({__proto__:null,default:Ai,editor:Ei}),it:Object.freeze({__proto__:null,default:Ii,editor:Si}),"ko-KR":Object.freeze({__proto__:null,default:zi,editor:Ti}),nb:Object.freeze({__proto__:null,default:Mi,editor:Oi}),nl:Object.freeze({__proto__:null,default:Li,editor:Di}),pl:Object.freeze({__proto__:null,default:Pi,editor:ji}),"pt-BR":Object.freeze({__proto__:null,default:Ri,editor:Ni}),"pt-PT":Object.freeze({__proto__:null,default:Fi,editor:Vi}),ro:Object.freeze({__proto__:null,default:Ui,editor:Bi}),ru:Object.freeze({__proto__:null,default:Yi,editor:Hi}),sk:Object.freeze({__proto__:null,default:Wi,editor:Xi}),sv:Object.freeze({__proto__:null,default:Ki,editor:qi}),tr:Object.freeze({__proto__:null,default:Zi,editor:Gi}),vi:Object.freeze({__proto__:null,default:Qi,editor:Ji}),"zh-Hans":Object.freeze({__proto__:null,default:eo,editor:to}),"zh-Hant":Object.freeze({__proto__:null,default:oo,editor:io})};function ro(t,e){try{return t.split(".").reduce(((t,e)=>t[e]),no[e])}catch(t){return}}function ao(t){return function(e){var i;let o=ro(e,null!==(i=null==t?void 0:t.locale.language)&&void 0!==i?i:"en");return o||(o=ro(e,"en")),null!=o?o:e}} /** * @license * Copyright 2020 Google Inc. @@ -220,7 +189,7 @@ const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */var Yi="Unknown",Xi="Backspace",Wi="Enter",qi="Spacebar",Ki="PageUp",Gi="PageDown",Zi="End",Ji="Home",Qi="ArrowLeft",tn="ArrowUp",en="ArrowRight",nn="ArrowDown",on="Delete",rn="Escape",an="Tab",ln=new Set;ln.add(Xi),ln.add(Wi),ln.add(qi),ln.add(Ki),ln.add(Gi),ln.add(Zi),ln.add(Ji),ln.add(Qi),ln.add(tn),ln.add(en),ln.add(nn),ln.add(on),ln.add(rn),ln.add(an);var sn=8,cn=13,dn=32,un=33,hn=34,mn=35,pn=36,fn=37,gn=38,_n=39,vn=40,bn=46,yn=27,xn=9,wn=new Map;wn.set(sn,Xi),wn.set(cn,Wi),wn.set(dn,qi),wn.set(un,Ki),wn.set(hn,Gi),wn.set(mn,Zi),wn.set(pn,Ji),wn.set(fn,Qi),wn.set(gn,tn),wn.set(_n,en),wn.set(vn,nn),wn.set(bn,on),wn.set(yn,rn),wn.set(xn,an);var kn=new Set;function Cn(t){var e=t.key;if(ln.has(e))return e;var i=wn.get(t.keyCode);return i||Yi} + */var lo="Unknown",so="Backspace",co="Enter",uo="Spacebar",ho="PageUp",mo="PageDown",po="End",fo="Home",go="ArrowLeft",_o="ArrowUp",vo="ArrowRight",bo="ArrowDown",yo="Delete",xo="Escape",wo="Tab",ko=new Set;ko.add(so),ko.add(co),ko.add(uo),ko.add(ho),ko.add(mo),ko.add(po),ko.add(fo),ko.add(go),ko.add(_o),ko.add(vo),ko.add(bo),ko.add(yo),ko.add(xo),ko.add(wo);var Co=8,$o=13,Eo=32,Ao=33,So=34,Io=35,To=36,zo=37,Oo=38,Mo=39,Do=40,Lo=46,jo=27,Po=9,No=new Map;No.set(Co,so),No.set($o,co),No.set(Eo,uo),No.set(Ao,ho),No.set(So,mo),No.set(Io,po),No.set(To,fo),No.set(zo,go),No.set(Oo,_o),No.set(Mo,vo),No.set(Do,bo),No.set(Lo,yo),No.set(jo,xo),No.set(Po,wo);var Ro=new Set;function Vo(t){var e=t.key;if(ko.has(e))return e;var i=No.get(t.keyCode);return i||lo} /** * @license * Copyright 2020 Google Inc. @@ -242,7 +211,7 @@ const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e * 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. - */kn.add(Ki),kn.add(Gi),kn.add(Zi),kn.add(Ji),kn.add(Qi),kn.add(tn),kn.add(en),kn.add(nn);var $n="Unknown",En="Backspace",An="Enter",Sn="Spacebar",In="PageUp",Tn="PageDown",zn="End",On="Home",Mn="ArrowLeft",Ln="ArrowUp",Dn="ArrowRight",jn="ArrowDown",Pn="Delete",Nn="Escape",Vn="Tab",Rn=new Set;Rn.add(En),Rn.add(An),Rn.add(Sn),Rn.add(In),Rn.add(Tn),Rn.add(zn),Rn.add(On),Rn.add(Mn),Rn.add(Ln),Rn.add(Dn),Rn.add(jn),Rn.add(Pn),Rn.add(Nn),Rn.add(Vn);var Fn=8,Bn=13,Un=32,Hn=33,Yn=34,Xn=35,Wn=36,qn=37,Kn=38,Gn=39,Zn=40,Jn=46,Qn=27,to=9,eo=new Map;eo.set(Fn,En),eo.set(Bn,An),eo.set(Un,Sn),eo.set(Hn,In),eo.set(Yn,Tn),eo.set(Xn,zn),eo.set(Wn,On),eo.set(qn,Mn),eo.set(Kn,Ln),eo.set(Gn,Dn),eo.set(Zn,jn),eo.set(Jn,Pn),eo.set(Qn,Nn),eo.set(to,Vn);var io,no,oo=new Set;function ro(t){var e=t.key;if(Rn.has(e))return e;var i=eo.get(t.keyCode);return i||$n} + */Ro.add(ho),Ro.add(mo),Ro.add(po),Ro.add(fo),Ro.add(go),Ro.add(_o),Ro.add(vo),Ro.add(bo);var Fo="Unknown",Bo="Backspace",Uo="Enter",Ho="Spacebar",Yo="PageUp",Xo="PageDown",Wo="End",qo="Home",Ko="ArrowLeft",Go="ArrowUp",Zo="ArrowRight",Jo="ArrowDown",Qo="Delete",tn="Escape",en="Tab",on=new Set;on.add(Bo),on.add(Uo),on.add(Ho),on.add(Yo),on.add(Xo),on.add(Wo),on.add(qo),on.add(Ko),on.add(Go),on.add(Zo),on.add(Jo),on.add(Qo),on.add(tn),on.add(en);var nn=8,rn=13,an=32,ln=33,sn=34,cn=35,dn=36,un=37,hn=38,mn=39,pn=40,fn=46,gn=27,_n=9,vn=new Map;vn.set(nn,Bo),vn.set(rn,Uo),vn.set(an,Ho),vn.set(ln,Yo),vn.set(sn,Xo),vn.set(cn,Wo),vn.set(dn,qo),vn.set(un,Ko),vn.set(hn,Go),vn.set(mn,Zo),vn.set(pn,Jo),vn.set(fn,Qo),vn.set(gn,tn),vn.set(_n,en);var bn,yn,xn=new Set;function wn(t){var e=t.key;if(on.has(e))return e;var i=vn.get(t.keyCode);return i||Fo} /** * @license * Copyright 2018 Google Inc. @@ -264,7 +233,7 @@ const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e * 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. - */oo.add(In),oo.add(Tn),oo.add(zn),oo.add(On),oo.add(Mn),oo.add(Ln),oo.add(Dn),oo.add(jn);var ao="mdc-list-item--activated",lo="mdc-list-item",so="mdc-list-item--disabled",co="mdc-list-item--selected",uo="mdc-list-item__text",ho="mdc-list-item__primary-text",mo="mdc-list";(io={})[""+ao]="mdc-list-item--activated",io[""+lo]="mdc-list-item",io[""+so]="mdc-list-item--disabled",io[""+co]="mdc-list-item--selected",io[""+ho]="mdc-list-item__primary-text",io[""+mo]="mdc-list";var po=((no={})[""+ao]="mdc-deprecated-list-item--activated",no[""+lo]="mdc-deprecated-list-item",no[""+so]="mdc-deprecated-list-item--disabled",no[""+co]="mdc-deprecated-list-item--selected",no[""+uo]="mdc-deprecated-list-item__text",no[""+ho]="mdc-deprecated-list-item__primary-text",no[""+mo]="mdc-deprecated-list",no);po[lo],po[lo],po[lo],po[lo],po[lo],po[lo];var fo=300,go=["input","button","textarea","select"],_o=function(t){var e=t.target;if(e){var i=(""+e.tagName).toLowerCase();-1===go.indexOf(i)&&t.preventDefault()}}; + */xn.add(Yo),xn.add(Xo),xn.add(Wo),xn.add(qo),xn.add(Ko),xn.add(Go),xn.add(Zo),xn.add(Jo);var kn="mdc-list-item--activated",Cn="mdc-list-item",$n="mdc-list-item--disabled",En="mdc-list-item--selected",An="mdc-list-item__text",Sn="mdc-list-item__primary-text",In="mdc-list";(bn={})[""+kn]="mdc-list-item--activated",bn[""+Cn]="mdc-list-item",bn[""+$n]="mdc-list-item--disabled",bn[""+En]="mdc-list-item--selected",bn[""+Sn]="mdc-list-item__primary-text",bn[""+In]="mdc-list";var Tn=((yn={})[""+kn]="mdc-deprecated-list-item--activated",yn[""+Cn]="mdc-deprecated-list-item",yn[""+$n]="mdc-deprecated-list-item--disabled",yn[""+En]="mdc-deprecated-list-item--selected",yn[""+An]="mdc-deprecated-list-item__text",yn[""+Sn]="mdc-deprecated-list-item__primary-text",yn[""+In]="mdc-deprecated-list",yn);Tn[Cn],Tn[Cn],Tn[Cn],Tn[Cn],Tn[Cn],Tn[Cn];var zn=300,On=["input","button","textarea","select"],Mn=function(t){var e=t.target;if(e){var i=(""+e.tagName).toLowerCase();-1===On.indexOf(i)&&t.preventDefault()}}; /** * @license * Copyright 2020 Google Inc. @@ -286,24 +255,24 @@ const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */function vo(t,e){for(var i=new Map,n=0;ne&&!i(r[l].index)){s=l;break}if(-1!==s)return n.sortedIndexCursor=s,r[n.sortedIndexCursor].index;return-1}(r,a,s,e):function(t,e,i){var n=i.typeaheadBuffer[0],o=t.get(n);if(!o)return-1;var r=o[i.sortedIndexCursor];if(0===r.text.lastIndexOf(i.typeaheadBuffer,0)&&!e(r.index))return r.index;var a=(i.sortedIndexCursor+1)%o.length,l=-1;for(;a!==i.sortedIndexCursor;){var s=o[a],c=0===s.text.lastIndexOf(i.typeaheadBuffer,0),d=!e(s.index);if(c&&d){l=a;break}a=(a+1)%o.length}if(-1!==l)return i.sortedIndexCursor=l,o[i.sortedIndexCursor].index;return-1}(r,s,e),-1===i||l||o(i),i}function yo(t){return t.typeaheadBuffer.length>0}function xo(t){return{addClass:e=>{t.classList.add(e)},removeClass:e=>{t.classList.remove(e)},hasClass:e=>t.classList.contains(e)}}const wo=()=>{},ko={get passive(){return!1}};document.addEventListener("x",wo,ko),document.removeEventListener("x",wo); + */function Dn(t,e){for(var i=new Map,o=0;oe&&!i(r[l].index)){s=l;break}if(-1!==s)return o.sortedIndexCursor=s,r[o.sortedIndexCursor].index;return-1}(r,a,s,e):function(t,e,i){var o=i.typeaheadBuffer[0],n=t.get(o);if(!n)return-1;var r=n[i.sortedIndexCursor];if(0===r.text.lastIndexOf(i.typeaheadBuffer,0)&&!e(r.index))return r.index;var a=(i.sortedIndexCursor+1)%n.length,l=-1;for(;a!==i.sortedIndexCursor;){var s=n[a],c=0===s.text.lastIndexOf(i.typeaheadBuffer,0),d=!e(s.index);if(c&&d){l=a;break}a=(a+1)%n.length}if(-1!==l)return i.sortedIndexCursor=l,n[i.sortedIndexCursor].index;return-1}(r,s,e),-1===i||l||n(i),i}function jn(t){return t.typeaheadBuffer.length>0}function Pn(t){return{addClass:e=>{t.classList.add(e)},removeClass:e=>{t.classList.remove(e)},hasClass:e=>t.classList.contains(e)}}const Nn=()=>{},Rn={get passive(){return!1}};document.addEventListener("x",Nn,Rn),document.removeEventListener("x",Nn); /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -class Co extends ot{click(){if(this.mdcRoot)return this.mdcRoot.focus(),void this.mdcRoot.click();super.click()}createFoundation(){void 0!==this.mdcFoundation&&this.mdcFoundation.destroy(),this.mdcFoundationClass&&(this.mdcFoundation=new this.mdcFoundationClass(this.createAdapter()),this.mdcFoundation.init())}firstUpdated(){this.createFoundation()}} +class Vn extends st{click(){if(this.mdcRoot)return this.mdcRoot.focus(),void this.mdcRoot.click();super.click()}createFoundation(){void 0!==this.mdcFoundation&&this.mdcFoundation.destroy(),this.mdcFoundationClass&&(this.mdcFoundation=new this.mdcFoundationClass(this.createAdapter()),this.mdcFoundation.init())}firstUpdated(){this.createFoundation()}} /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 - */var $o,Eo;const Ao=null!==(Eo=null===($o=window.ShadyDOM)||void 0===$o?void 0:$o.inUse)&&void 0!==Eo&&Eo;class So extends Co{constructor(){super(...arguments),this.disabled=!1,this.containingForm=null,this.formDataListener=t=>{this.disabled||this.setFormData(t.formData)}}findFormElement(){if(!this.shadowRoot||Ao)return null;const t=this.getRootNode().querySelectorAll("form");for(const e of Array.from(t))if(e.contains(this))return e;return null}connectedCallback(){var t;super.connectedCallback(),this.containingForm=this.findFormElement(),null===(t=this.containingForm)||void 0===t||t.addEventListener("formdata",this.formDataListener)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.containingForm)||void 0===t||t.removeEventListener("formdata",this.formDataListener),this.containingForm=null}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(t=>{this.dispatchEvent(new Event("change",t))}))}}So.shadowRootOptions={mode:"open",delegatesFocus:!0},n([st({type:Boolean})],So.prototype,"disabled",void 0); + */var Fn,Bn;const Un=null!==(Bn=null===(Fn=window.ShadyDOM)||void 0===Fn?void 0:Fn.inUse)&&void 0!==Bn&&Bn;class Hn extends Vn{constructor(){super(...arguments),this.disabled=!1,this.containingForm=null,this.formDataListener=t=>{this.disabled||this.setFormData(t.formData)}}findFormElement(){if(!this.shadowRoot||Un)return null;const t=this.getRootNode().querySelectorAll("form");for(const e of Array.from(t))if(e.contains(this))return e;return null}connectedCallback(){var t;super.connectedCallback(),this.containingForm=this.findFormElement(),null===(t=this.containingForm)||void 0===t||t.addEventListener("formdata",this.formDataListener)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.containingForm)||void 0===t||t.removeEventListener("formdata",this.formDataListener),this.containingForm=null}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(t=>{this.dispatchEvent(new Event("change",t))}))}}Hn.shadowRootOptions={mode:"open",delegatesFocus:!0},n([ht({type:Boolean})],Hn.prototype,"disabled",void 0); /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnProperty("_observers")){const t=e.constructor._observers;e.constructor._observers=new Map,t.forEach(((t,i)=>e.constructor._observers.set(i,t)))}}else{e.constructor._observers=new Map;const t=e.updated;e.updated=function(e){t.call(this,e),e.forEach(((t,e)=>{const i=this.constructor._observers.get(e);void 0!==i&&i.call(this,this[e],t)}))}}e.constructor._observers.set(i,t)} +const Yn=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnProperty("_observers")){const t=e.constructor._observers;e.constructor._observers=new Map,t.forEach(((t,i)=>e.constructor._observers.set(i,t)))}}else{e.constructor._observers=new Map;const t=e.updated;e.updated=function(e){t.call(this,e),e.forEach(((t,e)=>{const i=this.constructor._observers.get(e);void 0!==i&&i.call(this,this[e],t)}))}}e.constructor._observers.set(i,t)} /** * @license * Copyright 2016 Google Inc. @@ -325,7 +294,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */;var To=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),zo={LABEL_FLOAT_ABOVE:"mdc-floating-label--float-above",LABEL_REQUIRED:"mdc-floating-label--required",LABEL_SHAKE:"mdc-floating-label--shake",ROOT:"mdc-floating-label"},Oo=function(t){function n(e){var o=t.call(this,i(i({},n.defaultAdapter),e))||this;return o.shakeAnimationEndHandler=function(){o.handleShakeAnimationEnd()},o}return e(n,t),Object.defineProperty(n,"cssClasses",{get:function(){return zo},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},getWidth:function(){return 0},registerInteractionHandler:function(){},deregisterInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){this.adapter.registerInteractionHandler("animationend",this.shakeAnimationEndHandler)},n.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("animationend",this.shakeAnimationEndHandler)},n.prototype.getWidth=function(){return this.adapter.getWidth()},n.prototype.shake=function(t){var e=n.cssClasses.LABEL_SHAKE;t?this.adapter.addClass(e):this.adapter.removeClass(e)},n.prototype.float=function(t){var e=n.cssClasses,i=e.LABEL_FLOAT_ABOVE,o=e.LABEL_SHAKE;t?this.adapter.addClass(i):(this.adapter.removeClass(i),this.adapter.removeClass(o))},n.prototype.setRequired=function(t){var e=n.cssClasses.LABEL_REQUIRED;t?this.adapter.addClass(e):this.adapter.removeClass(e)},n.prototype.handleShakeAnimationEnd=function(){var t=n.cssClasses.LABEL_SHAKE;this.adapter.removeClass(t)},n}(To); + */;var Xn=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),Wn={LABEL_FLOAT_ABOVE:"mdc-floating-label--float-above",LABEL_REQUIRED:"mdc-floating-label--required",LABEL_SHAKE:"mdc-floating-label--shake",ROOT:"mdc-floating-label"},qn=function(t){function e(i){var n=t.call(this,o(o({},e.defaultAdapter),i))||this;return n.shakeAnimationEndHandler=function(){n.handleShakeAnimationEnd()},n}return i(e,t),Object.defineProperty(e,"cssClasses",{get:function(){return Wn},enumerable:!1,configurable:!0}),Object.defineProperty(e,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},getWidth:function(){return 0},registerInteractionHandler:function(){},deregisterInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),e.prototype.init=function(){this.adapter.registerInteractionHandler("animationend",this.shakeAnimationEndHandler)},e.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("animationend",this.shakeAnimationEndHandler)},e.prototype.getWidth=function(){return this.adapter.getWidth()},e.prototype.shake=function(t){var i=e.cssClasses.LABEL_SHAKE;t?this.adapter.addClass(i):this.adapter.removeClass(i)},e.prototype.float=function(t){var i=e.cssClasses,o=i.LABEL_FLOAT_ABOVE,n=i.LABEL_SHAKE;t?this.adapter.addClass(o):(this.adapter.removeClass(o),this.adapter.removeClass(n))},e.prototype.setRequired=function(t){var i=e.cssClasses.LABEL_REQUIRED;t?this.adapter.addClass(i):this.adapter.removeClass(i)},e.prototype.handleShakeAnimationEnd=function(){var t=e.cssClasses.LABEL_SHAKE;this.adapter.removeClass(t)},e}(Xn); /** * @license * Copyright 2016 Google Inc. @@ -347,7 +316,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */const Mo=Ae(class extends Se{constructor(t){switch(super(t),this.foundation=null,this.previousPart=null,t.type){case Ce:case $e:break;default:throw new Error("FloatingLabel directive only support attribute and property parts")}}update(t,[e]){if(t!==this.previousPart){this.foundation&&this.foundation.destroy(),this.previousPart=t;const e=t.element;e.classList.add("mdc-floating-label");const i=(t=>({addClass:e=>t.classList.add(e),removeClass:e=>t.classList.remove(e),getWidth:()=>t.scrollWidth,registerInteractionHandler:(e,i)=>{t.addEventListener(e,i)},deregisterInteractionHandler:(e,i)=>{t.removeEventListener(e,i)}}))(e);this.foundation=new Oo(i),this.foundation.init()}return this.render(e)}render(t){return this.foundation}}); + */const Kn=Re(class extends Ve{constructor(t){switch(super(t),this.foundation=null,this.previousPart=null,t.type){case je:case Pe:break;default:throw new Error("FloatingLabel directive only support attribute and property parts")}}update(t,[e]){if(t!==this.previousPart){this.foundation&&this.foundation.destroy(),this.previousPart=t;const e=t.element;e.classList.add("mdc-floating-label");const i=(t=>({addClass:e=>t.classList.add(e),removeClass:e=>t.classList.remove(e),getWidth:()=>t.scrollWidth,registerInteractionHandler:(e,i)=>{t.addEventListener(e,i)},deregisterInteractionHandler:(e,i)=>{t.removeEventListener(e,i)}}))(e);this.foundation=new qn(i),this.foundation.init()}return this.render(e)}render(t){return this.foundation}}); /** * @license * Copyright 2016 Google Inc. @@ -369,7 +338,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */var Lo=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),Do={LINE_RIPPLE_ACTIVE:"mdc-line-ripple--active",LINE_RIPPLE_DEACTIVATING:"mdc-line-ripple--deactivating"},jo=function(t){function n(e){var o=t.call(this,i(i({},n.defaultAdapter),e))||this;return o.transitionEndHandler=function(t){o.handleTransitionEnd(t)},o}return e(n,t),Object.defineProperty(n,"cssClasses",{get:function(){return Do},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},setStyle:function(){},registerEventHandler:function(){},deregisterEventHandler:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){this.adapter.registerEventHandler("transitionend",this.transitionEndHandler)},n.prototype.destroy=function(){this.adapter.deregisterEventHandler("transitionend",this.transitionEndHandler)},n.prototype.activate=function(){this.adapter.removeClass(Do.LINE_RIPPLE_DEACTIVATING),this.adapter.addClass(Do.LINE_RIPPLE_ACTIVE)},n.prototype.setRippleCenter=function(t){this.adapter.setStyle("transform-origin",t+"px center")},n.prototype.deactivate=function(){this.adapter.addClass(Do.LINE_RIPPLE_DEACTIVATING)},n.prototype.handleTransitionEnd=function(t){var e=this.adapter.hasClass(Do.LINE_RIPPLE_DEACTIVATING);"opacity"===t.propertyName&&e&&(this.adapter.removeClass(Do.LINE_RIPPLE_ACTIVE),this.adapter.removeClass(Do.LINE_RIPPLE_DEACTIVATING))},n}(Lo); + */var Gn=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),Zn={LINE_RIPPLE_ACTIVE:"mdc-line-ripple--active",LINE_RIPPLE_DEACTIVATING:"mdc-line-ripple--deactivating"},Jn=function(t){function e(i){var n=t.call(this,o(o({},e.defaultAdapter),i))||this;return n.transitionEndHandler=function(t){n.handleTransitionEnd(t)},n}return i(e,t),Object.defineProperty(e,"cssClasses",{get:function(){return Zn},enumerable:!1,configurable:!0}),Object.defineProperty(e,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},setStyle:function(){},registerEventHandler:function(){},deregisterEventHandler:function(){}}},enumerable:!1,configurable:!0}),e.prototype.init=function(){this.adapter.registerEventHandler("transitionend",this.transitionEndHandler)},e.prototype.destroy=function(){this.adapter.deregisterEventHandler("transitionend",this.transitionEndHandler)},e.prototype.activate=function(){this.adapter.removeClass(Zn.LINE_RIPPLE_DEACTIVATING),this.adapter.addClass(Zn.LINE_RIPPLE_ACTIVE)},e.prototype.setRippleCenter=function(t){this.adapter.setStyle("transform-origin",t+"px center")},e.prototype.deactivate=function(){this.adapter.addClass(Zn.LINE_RIPPLE_DEACTIVATING)},e.prototype.handleTransitionEnd=function(t){var e=this.adapter.hasClass(Zn.LINE_RIPPLE_DEACTIVATING);"opacity"===t.propertyName&&e&&(this.adapter.removeClass(Zn.LINE_RIPPLE_ACTIVE),this.adapter.removeClass(Zn.LINE_RIPPLE_DEACTIVATING))},e}(Gn); /** * @license * Copyright 2018 Google Inc. @@ -391,7 +360,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */const Po=Ae(class extends Se{constructor(t){switch(super(t),this.previousPart=null,this.foundation=null,t.type){case Ce:case $e:return;default:throw new Error("LineRipple only support attribute and property parts.")}}update(t,e){if(this.previousPart!==t){this.foundation&&this.foundation.destroy(),this.previousPart=t;const e=t.element;e.classList.add("mdc-line-ripple");const i=(t=>({addClass:e=>t.classList.add(e),removeClass:e=>t.classList.remove(e),hasClass:e=>t.classList.contains(e),setStyle:(e,i)=>t.style.setProperty(e,i),registerEventHandler:(e,i)=>{t.addEventListener(e,i)},deregisterEventHandler:(e,i)=>{t.removeEventListener(e,i)}}))(e);this.foundation=new jo(i),this.foundation.init()}return this.render()}render(){return this.foundation}}); + */const Qn=Re(class extends Ve{constructor(t){switch(super(t),this.previousPart=null,this.foundation=null,t.type){case je:case Pe:return;default:throw new Error("LineRipple only support attribute and property parts.")}}update(t,e){if(this.previousPart!==t){this.foundation&&this.foundation.destroy(),this.previousPart=t;const e=t.element;e.classList.add("mdc-line-ripple");const i=(t=>({addClass:e=>t.classList.add(e),removeClass:e=>t.classList.remove(e),hasClass:e=>t.classList.contains(e),setStyle:(e,i)=>t.style.setProperty(e,i),registerEventHandler:(e,i)=>{t.addEventListener(e,i)},deregisterEventHandler:(e,i)=>{t.removeEventListener(e,i)}}))(e);this.foundation=new Jn(i),this.foundation.init()}return this.render()}render(){return this.foundation}}); /** * @license * Copyright 2016 Google Inc. @@ -413,7 +382,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */var No=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),Vo="Unknown",Ro="Backspace",Fo="Enter",Bo="Spacebar",Uo="PageUp",Ho="PageDown",Yo="End",Xo="Home",Wo="ArrowLeft",qo="ArrowUp",Ko="ArrowRight",Go="ArrowDown",Zo="Delete",Jo="Escape",Qo="Tab",tr=new Set; + */var tr=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),er="Unknown",ir="Backspace",or="Enter",nr="Spacebar",rr="PageUp",ar="PageDown",lr="End",sr="Home",cr="ArrowLeft",dr="ArrowUp",ur="ArrowRight",hr="ArrowDown",mr="Delete",pr="Escape",fr="Tab",gr=new Set; /** * @license * Copyright 2020 Google Inc. @@ -435,7 +404,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * 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. - */tr.add(Ro),tr.add(Fo),tr.add(Bo),tr.add(Uo),tr.add(Ho),tr.add(Yo),tr.add(Xo),tr.add(Wo),tr.add(qo),tr.add(Ko),tr.add(Go),tr.add(Zo),tr.add(Jo),tr.add(Qo);var er=8,ir=13,nr=32,or=33,rr=34,ar=35,lr=36,sr=37,cr=38,dr=39,ur=40,hr=46,mr=27,pr=9,fr=new Map;fr.set(er,Ro),fr.set(ir,Fo),fr.set(nr,Bo),fr.set(or,Uo),fr.set(rr,Ho),fr.set(ar,Yo),fr.set(lr,Xo),fr.set(sr,Wo),fr.set(cr,qo),fr.set(dr,Ko),fr.set(ur,Go),fr.set(hr,Zo),fr.set(mr,Jo),fr.set(pr,Qo);var gr,_r,vr=new Set;function br(t){var e=t.key;if(tr.has(e))return e;var i=fr.get(t.keyCode);return i||Vo} + */gr.add(ir),gr.add(or),gr.add(nr),gr.add(rr),gr.add(ar),gr.add(lr),gr.add(sr),gr.add(cr),gr.add(dr),gr.add(ur),gr.add(hr),gr.add(mr),gr.add(pr),gr.add(fr);var _r=8,vr=13,br=32,yr=33,xr=34,wr=35,kr=36,Cr=37,$r=38,Er=39,Ar=40,Sr=46,Ir=27,Tr=9,zr=new Map;zr.set(_r,ir),zr.set(vr,or),zr.set(br,nr),zr.set(yr,rr),zr.set(xr,ar),zr.set(wr,lr),zr.set(kr,sr),zr.set(Cr,cr),zr.set($r,dr),zr.set(Er,ur),zr.set(Ar,hr),zr.set(Sr,mr),zr.set(Ir,pr),zr.set(Tr,fr);var Or,Mr,Dr=new Set;function Lr(t){var e=t.key;if(gr.has(e))return e;var i=zr.get(t.keyCode);return i||er} /** * @license * Copyright 2018 Google Inc. @@ -457,7 +426,7 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * 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. - */vr.add(Uo),vr.add(Ho),vr.add(Yo),vr.add(Xo),vr.add(Wo),vr.add(qo),vr.add(Ko),vr.add(Go),function(t){t[t.BOTTOM=1]="BOTTOM",t[t.CENTER=2]="CENTER",t[t.RIGHT=4]="RIGHT",t[t.FLIP_RTL=8]="FLIP_RTL"}(gr||(gr={})),function(t){t[t.TOP_LEFT=0]="TOP_LEFT",t[t.TOP_RIGHT=4]="TOP_RIGHT",t[t.BOTTOM_LEFT=1]="BOTTOM_LEFT",t[t.BOTTOM_RIGHT=5]="BOTTOM_RIGHT",t[t.TOP_START=8]="TOP_START",t[t.TOP_END=12]="TOP_END",t[t.BOTTOM_START=9]="BOTTOM_START",t[t.BOTTOM_END=13]="BOTTOM_END"}(_r||(_r={})); + */Dr.add(rr),Dr.add(ar),Dr.add(lr),Dr.add(sr),Dr.add(cr),Dr.add(dr),Dr.add(ur),Dr.add(hr),function(t){t[t.BOTTOM=1]="BOTTOM",t[t.CENTER=2]="CENTER",t[t.RIGHT=4]="RIGHT",t[t.FLIP_RTL=8]="FLIP_RTL"}(Or||(Or={})),function(t){t[t.TOP_LEFT=0]="TOP_LEFT",t[t.TOP_RIGHT=4]="TOP_RIGHT",t[t.BOTTOM_LEFT=1]="BOTTOM_LEFT",t[t.BOTTOM_RIGHT=5]="BOTTOM_RIGHT",t[t.TOP_START=8]="TOP_START",t[t.TOP_END=12]="TOP_END",t[t.BOTTOM_START=9]="BOTTOM_START",t[t.BOTTOM_END=13]="BOTTOM_END"}(Mr||(Mr={})); /** * @license * Copyright 2016 Google Inc. @@ -480,25 +449,25 @@ const Io=t=>(e,i)=>{if(e.constructor._observers){if(!e.constructor.hasOwnPropert * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -var yr={ACTIVATED:"mdc-select--activated",DISABLED:"mdc-select--disabled",FOCUSED:"mdc-select--focused",INVALID:"mdc-select--invalid",MENU_INVALID:"mdc-select__menu--invalid",OUTLINED:"mdc-select--outlined",REQUIRED:"mdc-select--required",ROOT:"mdc-select",WITH_LEADING_ICON:"mdc-select--with-leading-icon"},xr={ARIA_CONTROLS:"aria-controls",ARIA_DESCRIBEDBY:"aria-describedby",ARIA_SELECTED_ATTR:"aria-selected",CHANGE_EVENT:"MDCSelect:change",HIDDEN_INPUT_SELECTOR:'input[type="hidden"]',LABEL_SELECTOR:".mdc-floating-label",LEADING_ICON_SELECTOR:".mdc-select__icon",LINE_RIPPLE_SELECTOR:".mdc-line-ripple",MENU_SELECTOR:".mdc-select__menu",OUTLINE_SELECTOR:".mdc-notched-outline",SELECTED_TEXT_SELECTOR:".mdc-select__selected-text",SELECT_ANCHOR_SELECTOR:".mdc-select__anchor",VALUE_ATTR:"data-value"},wr={LABEL_SCALE:.75,UNSET_INDEX:-1,CLICK_DEBOUNCE_TIMEOUT_MS:330},kr=function(t){function n(e,o){void 0===o&&(o={});var r=t.call(this,i(i({},n.defaultAdapter),e))||this;return r.disabled=!1,r.isMenuOpen=!1,r.useDefaultValidation=!0,r.customValidity=!0,r.lastSelectedIndex=wr.UNSET_INDEX,r.clickDebounceTimeout=0,r.recentlyClicked=!1,r.leadingIcon=o.leadingIcon,r.helperText=o.helperText,r}return e(n,t),Object.defineProperty(n,"cssClasses",{get:function(){return yr},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return wr},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return xr},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},activateBottomLine:function(){},deactivateBottomLine:function(){},getSelectedIndex:function(){return-1},setSelectedIndex:function(){},hasLabel:function(){return!1},floatLabel:function(){},getLabelWidth:function(){return 0},setLabelRequired:function(){},hasOutline:function(){return!1},notchOutline:function(){},closeOutline:function(){},setRippleCenter:function(){},notifyChange:function(){},setSelectedText:function(){},isSelectAnchorFocused:function(){return!1},getSelectAnchorAttr:function(){return""},setSelectAnchorAttr:function(){},removeSelectAnchorAttr:function(){},addMenuClass:function(){},removeMenuClass:function(){},openMenu:function(){},closeMenu:function(){},getAnchorElement:function(){return null},setMenuAnchorElement:function(){},setMenuAnchorCorner:function(){},setMenuWrapFocus:function(){},focusMenuItemAtIndex:function(){},getMenuItemCount:function(){return 0},getMenuItemValues:function(){return[]},getMenuItemTextAtIndex:function(){return""},isTypeaheadInProgress:function(){return!1},typeaheadMatchItem:function(){return-1}}},enumerable:!1,configurable:!0}),n.prototype.getSelectedIndex=function(){return this.adapter.getSelectedIndex()},n.prototype.setSelectedIndex=function(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1),t>=this.adapter.getMenuItemCount()||(t===wr.UNSET_INDEX?this.adapter.setSelectedText(""):this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(t).trim()),this.adapter.setSelectedIndex(t),e&&this.adapter.closeMenu(),i||this.lastSelectedIndex===t||this.handleChange(),this.lastSelectedIndex=t)},n.prototype.setValue=function(t,e){void 0===e&&(e=!1);var i=this.adapter.getMenuItemValues().indexOf(t);this.setSelectedIndex(i,!1,e)},n.prototype.getValue=function(){var t=this.adapter.getSelectedIndex(),e=this.adapter.getMenuItemValues();return t!==wr.UNSET_INDEX?e[t]:""},n.prototype.getDisabled=function(){return this.disabled},n.prototype.setDisabled=function(t){this.disabled=t,this.disabled?(this.adapter.addClass(yr.DISABLED),this.adapter.closeMenu()):this.adapter.removeClass(yr.DISABLED),this.leadingIcon&&this.leadingIcon.setDisabled(this.disabled),this.disabled?this.adapter.removeSelectAnchorAttr("tabindex"):this.adapter.setSelectAnchorAttr("tabindex","0"),this.adapter.setSelectAnchorAttr("aria-disabled",this.disabled.toString())},n.prototype.openMenu=function(){this.adapter.addClass(yr.ACTIVATED),this.adapter.openMenu(),this.isMenuOpen=!0,this.adapter.setSelectAnchorAttr("aria-expanded","true")},n.prototype.setHelperTextContent=function(t){this.helperText&&this.helperText.setContent(t)},n.prototype.layout=function(){if(this.adapter.hasLabel()){var t=this.getValue().length>0,e=this.adapter.hasClass(yr.FOCUSED),i=t||e,n=this.adapter.hasClass(yr.REQUIRED);this.notchOutline(i),this.adapter.floatLabel(i),this.adapter.setLabelRequired(n)}},n.prototype.layoutOptions=function(){var t=this.adapter.getMenuItemValues().indexOf(this.getValue());this.setSelectedIndex(t,!1,!0)},n.prototype.handleMenuOpened=function(){if(0!==this.adapter.getMenuItemValues().length){var t=this.getSelectedIndex(),e=t>=0?t:0;this.adapter.focusMenuItemAtIndex(e)}},n.prototype.handleMenuClosing=function(){this.adapter.setSelectAnchorAttr("aria-expanded","false")},n.prototype.handleMenuClosed=function(){this.adapter.removeClass(yr.ACTIVATED),this.isMenuOpen=!1,this.adapter.isSelectAnchorFocused()||this.blur()},n.prototype.handleChange=function(){this.layout(),this.adapter.notifyChange(this.getValue()),this.adapter.hasClass(yr.REQUIRED)&&this.useDefaultValidation&&this.setValid(this.isValid())},n.prototype.handleMenuItemAction=function(t){this.setSelectedIndex(t,!0)},n.prototype.handleFocus=function(){this.adapter.addClass(yr.FOCUSED),this.layout(),this.adapter.activateBottomLine()},n.prototype.handleBlur=function(){this.isMenuOpen||this.blur()},n.prototype.handleClick=function(t){this.disabled||this.recentlyClicked||(this.setClickDebounceTimeout(),this.isMenuOpen?this.adapter.closeMenu():(this.adapter.setRippleCenter(t),this.openMenu()))},n.prototype.handleKeydown=function(t){if(!this.isMenuOpen&&this.adapter.hasClass(yr.FOCUSED)){var e=br(t)===Fo,i=br(t)===Bo,n=br(t)===qo,o=br(t)===Go;if(!(t.ctrlKey||t.metaKey)&&(!i&&t.key&&1===t.key.length||i&&this.adapter.isTypeaheadInProgress())){var r=i?" ":t.key,a=this.adapter.typeaheadMatchItem(r,this.getSelectedIndex());return a>=0&&this.setSelectedIndex(a),void t.preventDefault()}(e||i||n||o)&&(n&&this.getSelectedIndex()>0?this.setSelectedIndex(this.getSelectedIndex()-1):o&&this.getSelectedIndex()=this.adapter.getMenuItemCount()||(t===Nr.UNSET_INDEX?this.adapter.setSelectedText(""):this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(t).trim()),this.adapter.setSelectedIndex(t),e&&this.adapter.closeMenu(),i||this.lastSelectedIndex===t||this.handleChange(),this.lastSelectedIndex=t)},e.prototype.setValue=function(t,e){void 0===e&&(e=!1);var i=this.adapter.getMenuItemValues().indexOf(t);this.setSelectedIndex(i,!1,e)},e.prototype.getValue=function(){var t=this.adapter.getSelectedIndex(),e=this.adapter.getMenuItemValues();return t!==Nr.UNSET_INDEX?e[t]:""},e.prototype.getDisabled=function(){return this.disabled},e.prototype.setDisabled=function(t){this.disabled=t,this.disabled?(this.adapter.addClass(jr.DISABLED),this.adapter.closeMenu()):this.adapter.removeClass(jr.DISABLED),this.leadingIcon&&this.leadingIcon.setDisabled(this.disabled),this.disabled?this.adapter.removeSelectAnchorAttr("tabindex"):this.adapter.setSelectAnchorAttr("tabindex","0"),this.adapter.setSelectAnchorAttr("aria-disabled",this.disabled.toString())},e.prototype.openMenu=function(){this.adapter.addClass(jr.ACTIVATED),this.adapter.openMenu(),this.isMenuOpen=!0,this.adapter.setSelectAnchorAttr("aria-expanded","true")},e.prototype.setHelperTextContent=function(t){this.helperText&&this.helperText.setContent(t)},e.prototype.layout=function(){if(this.adapter.hasLabel()){var t=this.getValue().length>0,e=this.adapter.hasClass(jr.FOCUSED),i=t||e,o=this.adapter.hasClass(jr.REQUIRED);this.notchOutline(i),this.adapter.floatLabel(i),this.adapter.setLabelRequired(o)}},e.prototype.layoutOptions=function(){var t=this.adapter.getMenuItemValues().indexOf(this.getValue());this.setSelectedIndex(t,!1,!0)},e.prototype.handleMenuOpened=function(){if(0!==this.adapter.getMenuItemValues().length){var t=this.getSelectedIndex(),e=t>=0?t:0;this.adapter.focusMenuItemAtIndex(e)}},e.prototype.handleMenuClosing=function(){this.adapter.setSelectAnchorAttr("aria-expanded","false")},e.prototype.handleMenuClosed=function(){this.adapter.removeClass(jr.ACTIVATED),this.isMenuOpen=!1,this.adapter.isSelectAnchorFocused()||this.blur()},e.prototype.handleChange=function(){this.layout(),this.adapter.notifyChange(this.getValue()),this.adapter.hasClass(jr.REQUIRED)&&this.useDefaultValidation&&this.setValid(this.isValid())},e.prototype.handleMenuItemAction=function(t){this.setSelectedIndex(t,!0)},e.prototype.handleFocus=function(){this.adapter.addClass(jr.FOCUSED),this.layout(),this.adapter.activateBottomLine()},e.prototype.handleBlur=function(){this.isMenuOpen||this.blur()},e.prototype.handleClick=function(t){this.disabled||this.recentlyClicked||(this.setClickDebounceTimeout(),this.isMenuOpen?this.adapter.closeMenu():(this.adapter.setRippleCenter(t),this.openMenu()))},e.prototype.handleKeydown=function(t){if(!this.isMenuOpen&&this.adapter.hasClass(jr.FOCUSED)){var e=Lr(t)===or,i=Lr(t)===nr,o=Lr(t)===dr,n=Lr(t)===hr;if(!(t.ctrlKey||t.metaKey)&&(!i&&t.key&&1===t.key.length||i&&this.adapter.isTypeaheadInProgress())){var r=i?" ":t.key,a=this.adapter.typeaheadMatchItem(r,this.getSelectedIndex());return a>=0&&this.setSelectedIndex(a),void t.preventDefault()}(e||i||o||n)&&(this.openMenu(),t.preventDefault())}},e.prototype.notchOutline=function(t){if(this.adapter.hasOutline()){var e=this.adapter.hasClass(jr.FOCUSED);if(t){var i=Nr.LABEL_SCALE,o=this.adapter.getLabelWidth()*i;this.adapter.notchOutline(o)}else e||this.adapter.closeOutline()}},e.prototype.setLeadingIconAriaLabel=function(t){this.leadingIcon&&this.leadingIcon.setAriaLabel(t)},e.prototype.setLeadingIconContent=function(t){this.leadingIcon&&this.leadingIcon.setContent(t)},e.prototype.getUseDefaultValidation=function(){return this.useDefaultValidation},e.prototype.setUseDefaultValidation=function(t){this.useDefaultValidation=t},e.prototype.setValid=function(t){this.useDefaultValidation||(this.customValidity=t),this.adapter.setSelectAnchorAttr("aria-invalid",(!t).toString()),t?(this.adapter.removeClass(jr.INVALID),this.adapter.removeMenuClass(jr.MENU_INVALID)):(this.adapter.addClass(jr.INVALID),this.adapter.addMenuClass(jr.MENU_INVALID)),this.syncHelperTextValidity(t)},e.prototype.isValid=function(){return this.useDefaultValidation&&this.adapter.hasClass(jr.REQUIRED)&&!this.adapter.hasClass(jr.DISABLED)?this.getSelectedIndex()!==Nr.UNSET_INDEX&&(0!==this.getSelectedIndex()||Boolean(this.getValue())):this.customValidity},e.prototype.setRequired=function(t){t?this.adapter.addClass(jr.REQUIRED):this.adapter.removeClass(jr.REQUIRED),this.adapter.setSelectAnchorAttr("aria-required",t.toString()),this.adapter.setLabelRequired(t)},e.prototype.getRequired=function(){return"true"===this.adapter.getSelectAnchorAttr("aria-required")},e.prototype.init=function(){var t=this.adapter.getAnchorElement();t&&(this.adapter.setMenuAnchorElement(t),this.adapter.setMenuAnchorCorner(Mr.BOTTOM_START)),this.adapter.setMenuWrapFocus(!1),this.setDisabled(this.adapter.hasClass(jr.DISABLED)),this.syncHelperTextValidity(!this.adapter.hasClass(jr.INVALID)),this.layout(),this.layoutOptions()},e.prototype.blur=function(){this.adapter.removeClass(jr.FOCUSED),this.layout(),this.adapter.deactivateBottomLine(),this.adapter.hasClass(jr.REQUIRED)&&this.useDefaultValidation&&this.setValid(this.isValid())},e.prototype.syncHelperTextValidity=function(t){if(this.helperText){this.helperText.setValidity(t);var e=this.helperText.isVisible(),i=this.helperText.getId();e&&i?this.adapter.setSelectAnchorAttr(Pr.ARIA_DESCRIBEDBY,i):this.adapter.removeSelectAnchorAttr(Pr.ARIA_DESCRIBEDBY)}},e.prototype.setClickDebounceTimeout=function(){var t=this;clearTimeout(this.clickDebounceTimeout),this.clickDebounceTimeout=setTimeout((function(){t.recentlyClicked=!1}),Nr.CLICK_DEBOUNCE_TIMEOUT_MS),this.recentlyClicked=!0},e}(tr),Vr=Rr; /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const Cr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"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 i,n;if(void 0===this.et){this.et=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===(i=this.st)||void 0===i?void 0:i.has(t))&&this.et.add(t);return this.render(e)}const o=t.element.classList;this.et.forEach((t=>{t in e||(o.remove(t),this.et.delete(t))}));for(const t in e){const i=!!e[t];i===this.et.has(t)||(null===(n=this.st)||void 0===n?void 0:n.has(t))||(i?(o.add(t),this.et.add(t)):(o.remove(t),this.et.delete(t)))}return R}}),$r=t=>null!=t?t:F +const Fr=Re(class extends Ve{constructor(t){var e;if(super(t),t.type!==je||"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 i,o;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===(i=this.st)||void 0===i?void 0:i.has(t))&&this.nt.add(t);return this.render(e)}const n=t.element.classList;this.nt.forEach((t=>{t in e||(n.remove(t),this.nt.delete(t))}));for(const t in e){const i=!!e[t];i===this.nt.has(t)||(null===(o=this.st)||void 0===o?void 0:o.has(t))||(i?(n.add(t),this.nt.add(t)):(n.remove(t),this.nt.delete(t)))}return H}}),Br=t=>null!=t?t:Y /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 - */,Er=(t={})=>{const e={};for(const i in t)e[i]=t[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},e)}; + */,Ur=(t={})=>{const e={};for(const i in t)e[i]=t[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},e)}; /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */class Ar extends So{constructor(){super(...arguments),this.mdcFoundationClass=kr,this.disabled=!1,this.outlined=!1,this.label="",this.outlineOpen=!1,this.outlineWidth=0,this.value="",this.name="",this.selectedText="",this.icon="",this.menuOpen=!1,this.helper="",this.validateOnInitialRender=!1,this.validationMessage="",this.required=!1,this.naturalMenuWidth=!1,this.isUiValid=!0,this.fixedMenuPosition=!1,this.typeaheadState={bufferClearTimeout:0,currentFirstChar:"",sortedIndexCursor:0,typeaheadBuffer:""},this.sortedIndexByFirstChar=new Map,this.menuElement_=null,this.listeners=[],this.onBodyClickBound=()=>{},this._menuUpdateComplete=null,this.valueSetDirectly=!1,this.validityTransform=null,this._validity=Er()}get items(){return this.menuElement_||(this.menuElement_=this.menuElement),this.menuElement_?this.menuElement_.items:[]}get selected(){const t=this.menuElement;return t?t.selected:null}get index(){const t=this.menuElement;return t?t.index:-1}get shouldRenderHelperText(){return!!this.helper||!!this.validationMessage}get validity(){return this._checkValidity(this.value),this._validity}render(){const t={"mdc-select--disabled":this.disabled,"mdc-select--no-label":!this.label,"mdc-select--filled":!this.outlined,"mdc-select--outlined":this.outlined,"mdc-select--with-leading-icon":!!this.icon,"mdc-select--required":this.required,"mdc-select--invalid":!this.isUiValid},e={"mdc-select__menu--invalid":!this.isUiValid},i=this.label?"label":void 0,n=this.shouldRenderHelperText?"helper-text":void 0;return N` + */class Hr extends Hn{constructor(){super(...arguments),this.mdcFoundationClass=Vr,this.disabled=!1,this.outlined=!1,this.label="",this.outlineOpen=!1,this.outlineWidth=0,this.value="",this.name="",this.selectedText="",this.icon="",this.menuOpen=!1,this.helper="",this.validateOnInitialRender=!1,this.validationMessage="",this.required=!1,this.naturalMenuWidth=!1,this.isUiValid=!0,this.fixedMenuPosition=!1,this.typeaheadState={bufferClearTimeout:0,currentFirstChar:"",sortedIndexCursor:0,typeaheadBuffer:""},this.sortedIndexByFirstChar=new Map,this.menuElement_=null,this.listeners=[],this.onBodyClickBound=()=>{},this._menuUpdateComplete=null,this.valueSetDirectly=!1,this.validityTransform=null,this._validity=Ur()}get items(){return this.menuElement_||(this.menuElement_=this.menuElement),this.menuElement_?this.menuElement_.items:[]}get selected(){const t=this.menuElement;return t?t.selected:null}get index(){const t=this.menuElement;return t?t.index:-1}get shouldRenderHelperText(){return!!this.helper||!!this.validationMessage}get validity(){return this._checkValidity(this.value),this._validity}render(){const t={"mdc-select--disabled":this.disabled,"mdc-select--no-label":!this.label,"mdc-select--filled":!this.outlined,"mdc-select--outlined":this.outlined,"mdc-select--with-leading-icon":!!this.icon,"mdc-select--required":this.required,"mdc-select--invalid":!this.isUiValid},e=this.label?"label":void 0,i=this.shouldRenderHelperText?"helper-text":void 0;return B`
+ class="mdc-select ${Fr(t)}"> ${this.renderLineRipple()}
- - - + ${this.renderMenu()}
- ${this.renderHelperText()}`}renderRipple(){return this.outlined?F:N` + ${this.renderHelperText()}`}renderMenu(){const t=this.getMenuClasses();return B` + + ${this.renderMenuContent()} + `}getMenuClasses(){return{"mdc-select__menu":!0,"mdc-menu":!0,"mdc-menu-surface":!0,"mdc-select__menu--invalid":!this.isUiValid}}renderMenuContent(){return B``}renderRipple(){return this.outlined?Y:B` - `}renderOutline(){return this.outlined?N` + `}renderOutline(){return this.outlined?B` ${this.renderLabel()} - `:F}renderLabel(){return this.label?N` + `:Y}renderLabel(){return this.label?B` ${this.label} - `:F}renderLeadingIcon(){return this.icon?N`
${this.icon}
`:F}renderLineRipple(){return this.outlined?F:N` - - `}renderHelperText(){if(!this.shouldRenderHelperText)return F;const t=this.validationMessage&&!this.isUiValid;return N` + `:Y}renderLeadingIcon(){return this.icon?B`
${this.icon}
`:Y}renderLineRipple(){return this.outlined?Y:B` + + `}renderHelperText(){if(!this.shouldRenderHelperText)return Y;const t=this.validationMessage&&!this.isUiValid;return B`

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

`}createAdapter(){return Object.assign(Object.assign({},xo(this.mdcRoot)),{activateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},hasLabel:()=>!!this.label,floatLabel:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.float(t)},getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,setLabelRequired:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(t)},hasOutline:()=>this.outlined,notchOutline:t=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=t,this.outlineOpen=!0)},closeOutline:()=>{this.outlineElement&&(this.outlineOpen=!1)},setRippleCenter:t=>{if(this.lineRippleElement){this.lineRippleElement.lineRippleFoundation.setRippleCenter(t)}},notifyChange:async t=>{if(!this.valueSetDirectly&&t===this.value)return;this.valueSetDirectly=!1,this.value=t,await this.updateComplete;const e=new Event("change",{bubbles:!0});this.dispatchEvent(e)},setSelectedText:t=>this.selectedText=t,isSelectAnchorFocused:()=>{const t=this.anchorElement;if(!t)return!1;return t.getRootNode().activeElement===t},getSelectAnchorAttr:t=>{const e=this.anchorElement;return e?e.getAttribute(t):null},setSelectAnchorAttr:(t,e)=>{const i=this.anchorElement;i&&i.setAttribute(t,e)},removeSelectAnchorAttr:t=>{const e=this.anchorElement;e&&e.removeAttribute(t)},openMenu:()=>{this.menuOpen=!0},closeMenu:()=>{this.menuOpen=!1},addMenuClass:()=>{},removeMenuClass:()=>{},getAnchorElement:()=>this.anchorElement,setMenuAnchorElement:()=>{},setMenuAnchorCorner:()=>{const t=this.menuElement;t&&(t.corner="BOTTOM_START")},setMenuWrapFocus:t=>{const e=this.menuElement;e&&(e.wrapFocus=t)},focusMenuItemAtIndex:t=>{const e=this.menuElement;if(!e)return;const i=e.items[t];i&&i.focus()},getMenuItemCount:()=>{const t=this.menuElement;return t?t.items.length:0},getMenuItemValues:()=>{const t=this.menuElement;if(!t)return[];return t.items.map((t=>t.value))},getMenuItemTextAtIndex:t=>{const e=this.menuElement;if(!e)return"";const i=e.items[t];return i?i.text:""},getSelectedIndex:()=>this.index,setSelectedIndex:()=>{},isTypeaheadInProgress:()=>yo(this.typeaheadState),typeaheadMatchItem:(t,e)=>{if(!this.menuElement)return-1;const i={focusItemAtIndex:t=>{this.menuElement.focusItemAtIndex(t)},focusedItemIndex:e||this.menuElement.getFocusedItemIndex(),nextChar:t,sortedIndexByFirstChar:this.sortedIndexByFirstChar,skipFocus:!1,isItemAtIndexDisabled:t=>this.items[t].disabled},n=bo(i,this.typeaheadState);return-1!==n&&this.select(n),n}})}checkValidity(){const t=this._checkValidity(this.value);if(!t){const t=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(t)}return t}reportValidity(){const t=this.checkValidity();return this.isUiValid=t,t}_checkValidity(t){const e=this.formElement.validity;let i=Er(e);if(this.validityTransform){const e=this.validityTransform(t,i);i=Object.assign(Object.assign({},i),e)}return this._validity=i,this._validity.valid}setCustomValidity(t){this.validationMessage=t,this.formElement.setCustomValidity(t)}async getUpdateComplete(){await this._menuUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){const t=this.menuElement;if(t&&(this._menuUpdateComplete=t.updateComplete,await this._menuUpdateComplete),super.firstUpdated(),this.mdcFoundation.isValid=()=>!0,this.mdcFoundation.setValid=()=>{},this.mdcFoundation.setDisabled(this.disabled),this.validateOnInitialRender&&this.reportValidity(),!this.selected){!this.items.length&&this.slotElement&&this.slotElement.assignedNodes({flatten:!0}).length&&(await new Promise((t=>requestAnimationFrame(t))),await this.layout());const t=this.items.length&&""===this.items[0].value;if(!this.value&&t)return void this.select(0);this.selectByValue(this.value)}this.sortedIndexByFirstChar=vo(this.items.length,(t=>this.items[t].text))}onItemsUpdated(){this.sortedIndexByFirstChar=vo(this.items.length,(t=>this.items[t].text))}select(t){const e=this.menuElement;e&&e.select(t)}selectByValue(t){let e=-1;for(let i=0;i0,o=i&&this.index{this.menuElement.focusItemAtIndex(t)},focusedItemIndex:e,isTargetListItem:!!i&&i.hasAttribute("mwc-list-item"),sortedIndexByFirstChar:this.sortedIndexByFirstChar,isItemAtIndexDisabled:t=>this.items[t].disabled};!function(t,e){var i=t.event,n=t.isTargetListItem,o=t.focusedItemIndex,r=t.focusItemAtIndex,a=t.sortedIndexByFirstChar,l=t.isItemAtIndexDisabled,s="ArrowLeft"===ro(i),c="ArrowUp"===ro(i),d="ArrowRight"===ro(i),u="ArrowDown"===ro(i),h="Home"===ro(i),m="End"===ro(i),p="Enter"===ro(i),f="Spacebar"===ro(i);i.ctrlKey||i.metaKey||s||c||d||u||h||m||p||(f||1!==i.key.length?f&&(n&&_o(i),n&&yo(e)&&bo({focusItemAtIndex:r,focusedItemIndex:o,nextChar:" ",sortedIndexByFirstChar:a,skipFocus:!1,isItemAtIndexDisabled:l},e)):(_o(i),bo({focusItemAtIndex:r,focusedItemIndex:o,nextChar:i.key.toLowerCase(),sortedIndexByFirstChar:a,skipFocus:!1,isItemAtIndexDisabled:l},e)))} + class="mdc-select-helper-text ${Fr({"mdc-select-helper-text--validation-msg":t})}" + id="helper-text">${t?this.validationMessage:this.helper}

`}createAdapter(){return Object.assign(Object.assign({},Pn(this.mdcRoot)),{activateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},hasLabel:()=>!!this.label,floatLabel:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.float(t)},getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,setLabelRequired:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(t)},hasOutline:()=>this.outlined,notchOutline:t=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=t,this.outlineOpen=!0)},closeOutline:()=>{this.outlineElement&&(this.outlineOpen=!1)},setRippleCenter:t=>{if(this.lineRippleElement){this.lineRippleElement.lineRippleFoundation.setRippleCenter(t)}},notifyChange:async t=>{if(!this.valueSetDirectly&&t===this.value)return;this.valueSetDirectly=!1,this.value=t,await this.updateComplete;const e=new Event("change",{bubbles:!0});this.dispatchEvent(e)},setSelectedText:t=>this.selectedText=t,isSelectAnchorFocused:()=>{const t=this.anchorElement;if(!t)return!1;return t.getRootNode().activeElement===t},getSelectAnchorAttr:t=>{const e=this.anchorElement;return e?e.getAttribute(t):null},setSelectAnchorAttr:(t,e)=>{const i=this.anchorElement;i&&i.setAttribute(t,e)},removeSelectAnchorAttr:t=>{const e=this.anchorElement;e&&e.removeAttribute(t)},openMenu:()=>{this.menuOpen=!0},closeMenu:()=>{this.menuOpen=!1},addMenuClass:()=>{},removeMenuClass:()=>{},getAnchorElement:()=>this.anchorElement,setMenuAnchorElement:()=>{},setMenuAnchorCorner:()=>{const t=this.menuElement;t&&(t.corner="BOTTOM_START")},setMenuWrapFocus:t=>{const e=this.menuElement;e&&(e.wrapFocus=t)},focusMenuItemAtIndex:t=>{const e=this.menuElement;if(!e)return;const i=e.items[t];i&&i.focus()},getMenuItemCount:()=>{const t=this.menuElement;return t?t.items.length:0},getMenuItemValues:()=>{const t=this.menuElement;if(!t)return[];return t.items.map((t=>t.value))},getMenuItemTextAtIndex:t=>{const e=this.menuElement;if(!e)return"";const i=e.items[t];return i?i.text:""},getSelectedIndex:()=>this.index,setSelectedIndex:()=>{},isTypeaheadInProgress:()=>jn(this.typeaheadState),typeaheadMatchItem:(t,e)=>{if(!this.menuElement)return-1;const i={focusItemAtIndex:t=>{this.menuElement.focusItemAtIndex(t)},focusedItemIndex:e||this.menuElement.getFocusedItemIndex(),nextChar:t,sortedIndexByFirstChar:this.sortedIndexByFirstChar,skipFocus:!1,isItemAtIndexDisabled:t=>this.items[t].disabled},o=Ln(i,this.typeaheadState);return-1!==o&&this.select(o),o}})}checkValidity(){const t=this._checkValidity(this.value);if(!t){const t=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(t)}return t}reportValidity(){const t=this.checkValidity();return this.isUiValid=t,t}_checkValidity(t){const e=this.formElement.validity;let i=Ur(e);if(this.validityTransform){const e=this.validityTransform(t,i);i=Object.assign(Object.assign({},i),e)}return this._validity=i,this._validity.valid}setCustomValidity(t){this.validationMessage=t,this.formElement.setCustomValidity(t)}async getUpdateComplete(){await this._menuUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){const t=this.menuElement;if(t&&(this._menuUpdateComplete=t.updateComplete,await this._menuUpdateComplete),super.firstUpdated(),this.mdcFoundation.isValid=()=>!0,this.mdcFoundation.setValid=()=>{},this.mdcFoundation.setDisabled(this.disabled),this.validateOnInitialRender&&this.reportValidity(),!this.selected){!this.items.length&&this.slotElement&&this.slotElement.assignedNodes({flatten:!0}).length&&(await new Promise((t=>requestAnimationFrame(t))),await this.layout());const t=this.items.length&&""===this.items[0].value;if(!this.value&&t)return void this.select(0);this.selectByValue(this.value)}this.sortedIndexByFirstChar=Dn(this.items.length,(t=>this.items[t].text))}onItemsUpdated(){this.sortedIndexByFirstChar=Dn(this.items.length,(t=>this.items[t].text))}select(t){const e=this.menuElement;e&&e.select(t)}selectByValue(t){let e=-1;for(let i=0;i0,n=i&&this.index{this.menuElement.focusItemAtIndex(t)},focusedItemIndex:e,isTargetListItem:!!i&&i.hasAttribute("mwc-list-item"),sortedIndexByFirstChar:this.sortedIndexByFirstChar,isItemAtIndexDisabled:t=>this.items[t].disabled};!function(t,e){var i=t.event,o=t.isTargetListItem,n=t.focusedItemIndex,r=t.focusItemAtIndex,a=t.sortedIndexByFirstChar,l=t.isItemAtIndexDisabled,s="ArrowLeft"===wn(i),c="ArrowUp"===wn(i),d="ArrowRight"===wn(i),u="ArrowDown"===wn(i),h="Home"===wn(i),m="End"===wn(i),p="Enter"===wn(i),f="Spacebar"===wn(i);i.altKey||i.ctrlKey||i.metaKey||s||c||d||u||h||m||p||(f||1!==i.key.length?f&&(o&&Mn(i),o&&jn(e)&&Ln({focusItemAtIndex:r,focusedItemIndex:n,nextChar:" ",sortedIndexByFirstChar:a,skipFocus:!1,isItemAtIndexDisabled:l},e)):(Mn(i),Ln({focusItemAtIndex:r,focusedItemIndex:n,nextChar:i.key.toLowerCase(),sortedIndexByFirstChar:a,skipFocus:!1,isItemAtIndexDisabled:l},e)))} /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 - */(n,this.typeaheadState)}async onSelected(t){this.mdcFoundation||await this.updateComplete,this.mdcFoundation.handleMenuItemAction(t.detail.index);const e=this.items[t.detail.index];e&&(this.value=e.value)}onOpened(){this.mdcFoundation&&(this.menuOpen=!0,this.mdcFoundation.handleMenuOpened())}onClosed(){this.mdcFoundation&&(this.menuOpen=!1,this.mdcFoundation.handleMenuClosed())}setFormData(t){this.name&&null!==this.selected&&t.append(this.name,this.value)}async layout(t=!0){this.mdcFoundation&&this.mdcFoundation.layout(),await this.updateComplete;const e=this.menuElement;e&&e.layout(t);const i=this.labelElement;if(!i)return void(this.outlineOpen=!1);const n=!!this.label&&!!this.value;if(i.floatingLabelFoundation.float(n),!this.outlined)return;this.outlineOpen=n,await this.updateComplete;const o=i.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=o)}async layoutOptions(){this.mdcFoundation&&this.mdcFoundation.layoutOptions()}}n([ht(".mdc-select")],Ar.prototype,"mdcRoot",void 0),n([ht(".formElement")],Ar.prototype,"formElement",void 0),n([ht("slot")],Ar.prototype,"slotElement",void 0),n([ht("select")],Ar.prototype,"nativeSelectElement",void 0),n([ht("input")],Ar.prototype,"nativeInputElement",void 0),n([ht(".mdc-line-ripple")],Ar.prototype,"lineRippleElement",void 0),n([ht(".mdc-floating-label")],Ar.prototype,"labelElement",void 0),n([ht("mwc-notched-outline")],Ar.prototype,"outlineElement",void 0),n([ht(".mdc-menu")],Ar.prototype,"menuElement",void 0),n([ht(".mdc-select__anchor")],Ar.prototype,"anchorElement",void 0),n([st({type:Boolean,attribute:"disabled",reflect:!0}),Io((function(t){this.mdcFoundation&&this.mdcFoundation.setDisabled(t)}))],Ar.prototype,"disabled",void 0),n([st({type:Boolean}),Io((function(t,e){void 0!==e&&this.outlined!==e&&this.layout(!1)}))],Ar.prototype,"outlined",void 0),n([st({type:String}),Io((function(t,e){void 0!==e&&this.label!==e&&this.layout(!1)}))],Ar.prototype,"label",void 0),n([ct()],Ar.prototype,"outlineOpen",void 0),n([ct()],Ar.prototype,"outlineWidth",void 0),n([st({type:String}),Io((function(t){if(this.mdcFoundation){const e=null===this.selected&&!!t,i=this.selected&&this.selected.value!==t;(e||i)&&this.selectByValue(t),this.reportValidity()}}))],Ar.prototype,"value",void 0),n([st()],Ar.prototype,"name",void 0),n([ct()],Ar.prototype,"selectedText",void 0),n([st({type:String})],Ar.prototype,"icon",void 0),n([ct()],Ar.prototype,"menuOpen",void 0),n([st({type:String})],Ar.prototype,"helper",void 0),n([st({type:Boolean})],Ar.prototype,"validateOnInitialRender",void 0),n([st({type:String})],Ar.prototype,"validationMessage",void 0),n([st({type:Boolean})],Ar.prototype,"required",void 0),n([st({type:Boolean})],Ar.prototype,"naturalMenuWidth",void 0),n([ct()],Ar.prototype,"isUiValid",void 0),n([st({type:Boolean})],Ar.prototype,"fixedMenuPosition",void 0),n([ut({capture:!0})],Ar.prototype,"handleTypeahead",null); + */(o,this.typeaheadState)}async onSelected(t){this.mdcFoundation||await this.updateComplete,this.mdcFoundation.handleMenuItemAction(t.detail.index);const e=this.items[t.detail.index];e&&(this.value=e.value)}onOpened(){this.mdcFoundation&&(this.menuOpen=!0,this.mdcFoundation.handleMenuOpened())}onClosed(){this.mdcFoundation&&(this.menuOpen=!1,this.mdcFoundation.handleMenuClosed())}setFormData(t){this.name&&null!==this.selected&&t.append(this.name,this.value)}async layout(t=!0){this.mdcFoundation&&this.mdcFoundation.layout(),await this.updateComplete;const e=this.menuElement;e&&e.layout(t);const i=this.labelElement;if(!i)return void(this.outlineOpen=!1);const o=!!this.label&&!!this.value;if(i.floatingLabelFoundation.float(o),!this.outlined)return;this.outlineOpen=o,await this.updateComplete;const n=i.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=n)}async layoutOptions(){this.mdcFoundation&&this.mdcFoundation.layoutOptions()}}n([gt(".mdc-select")],Hr.prototype,"mdcRoot",void 0),n([gt(".formElement")],Hr.prototype,"formElement",void 0),n([gt("slot")],Hr.prototype,"slotElement",void 0),n([gt("select")],Hr.prototype,"nativeSelectElement",void 0),n([gt("input")],Hr.prototype,"nativeInputElement",void 0),n([gt(".mdc-line-ripple")],Hr.prototype,"lineRippleElement",void 0),n([gt(".mdc-floating-label")],Hr.prototype,"labelElement",void 0),n([gt("mwc-notched-outline")],Hr.prototype,"outlineElement",void 0),n([gt(".mdc-menu")],Hr.prototype,"menuElement",void 0),n([gt(".mdc-select__anchor")],Hr.prototype,"anchorElement",void 0),n([ht({type:Boolean,attribute:"disabled",reflect:!0}),Yn((function(t){this.mdcFoundation&&this.mdcFoundation.setDisabled(t)}))],Hr.prototype,"disabled",void 0),n([ht({type:Boolean}),Yn((function(t,e){void 0!==e&&this.outlined!==e&&this.layout(!1)}))],Hr.prototype,"outlined",void 0),n([ht({type:String}),Yn((function(t,e){void 0!==e&&this.label!==e&&this.layout(!1)}))],Hr.prototype,"label",void 0),n([mt()],Hr.prototype,"outlineOpen",void 0),n([mt()],Hr.prototype,"outlineWidth",void 0),n([ht({type:String}),Yn((function(t){if(this.mdcFoundation){const e=null===this.selected&&!!t,i=this.selected&&this.selected.value!==t;(e||i)&&this.selectByValue(t),this.reportValidity()}}))],Hr.prototype,"value",void 0),n([ht()],Hr.prototype,"name",void 0),n([mt()],Hr.prototype,"selectedText",void 0),n([ht({type:String})],Hr.prototype,"icon",void 0),n([mt()],Hr.prototype,"menuOpen",void 0),n([ht({type:String})],Hr.prototype,"helper",void 0),n([ht({type:Boolean})],Hr.prototype,"validateOnInitialRender",void 0),n([ht({type:String})],Hr.prototype,"validationMessage",void 0),n([ht({type:Boolean})],Hr.prototype,"required",void 0),n([ht({type:Boolean})],Hr.prototype,"naturalMenuWidth",void 0),n([mt()],Hr.prototype,"isUiValid",void 0),n([ht({type:Boolean})],Hr.prototype,"fixedMenuPosition",void 0),n([ft({capture:!0})],Hr.prototype,"handleTypeahead",null); /** * @license * Copyright 2021 Google LLC * SPDX-LIcense-Identifier: Apache-2.0 */ -const Sr=d`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px;z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}.mdc-select{display:inline-flex;position:relative}.mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87)}.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-select.mdc-select--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#6200ee;fill:var(--mdc-theme-primary, #6200ee)}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled)+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__icon{color:rgba(0, 0, 0, 0.54)}.mdc-select.mdc-select--disabled .mdc-select__icon{color:rgba(0, 0, 0, 0.38)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:red}.mdc-select.mdc-select--disabled .mdc-floating-label{color:GrayText}.mdc-select.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}.mdc-select.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select.mdc-select--disabled .mdc-notched-outline__trailing{border-color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__icon{color:GrayText}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:GrayText}}.mdc-select .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-select .mdc-select__anchor{padding-left:16px;padding-right:0}[dir=rtl] .mdc-select .mdc-select__anchor,.mdc-select .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:16px}.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor{padding-left:0;padding-right:0}[dir=rtl] .mdc-select.mdc-select--with-leading-icon .mdc-select__anchor,.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:0}.mdc-select .mdc-select__icon{width:24px;height:24px;font-size:24px}.mdc-select .mdc-select__dropdown-icon{width:24px;height:24px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item,.mdc-select .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:12px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic,.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:12px;margin-right:0}.mdc-select__dropdown-icon{margin-left:12px;margin-right:12px;display:inline-flex;position:relative;align-self:center;align-items:center;justify-content:center;flex-shrink:0;pointer-events:none}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active,.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{position:absolute;top:0;left:0}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-graphic{width:41.6666666667%;height:20.8333333333%}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:1;transition:opacity 75ms linear 75ms}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:0;transition:opacity 75ms linear}[dir=rtl] .mdc-select__dropdown-icon,.mdc-select__dropdown-icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:0;transition:opacity 49.5ms linear}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:1;transition:opacity 100.5ms linear 49.5ms}.mdc-select__anchor{width:200px;min-width:0;flex:1 1 auto;position:relative;box-sizing:border-box;overflow:hidden;outline:none;cursor:pointer}.mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-select__selected-text-container{display:flex;appearance:none;pointer-events:none;box-sizing:border-box;width:auto;min-width:0;flex-grow:1;height:28px;border:none;outline:none;padding:0;background-color:transparent;color:inherit}.mdc-select__selected-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;width:100%;text-align:left}[dir=rtl] .mdc-select__selected-text,.mdc-select__selected-text[dir=rtl]{text-align:right}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--invalid+.mdc-select-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--disabled{cursor:default;pointer-events:none}.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item{padding-left:12px;padding-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item,.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:12px;padding-right:12px}.mdc-select__menu .mdc-deprecated-list .mdc-select__icon,.mdc-select__menu .mdc-list .mdc-select__icon{margin-left:0;margin-right:0}[dir=rtl] .mdc-select__menu .mdc-deprecated-list .mdc-select__icon,[dir=rtl] .mdc-select__menu .mdc-list .mdc-select__icon,.mdc-select__menu .mdc-deprecated-list .mdc-select__icon[dir=rtl],.mdc-select__menu .mdc-list .mdc-select__icon[dir=rtl]{margin-left:0;margin-right:0}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-list-item__start{display:inline-flex;align-items:center}.mdc-select__option{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select__option,.mdc-select__option[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select__one-line-option.mdc-list-item--with-one-line{height:48px}.mdc-select__two-line-option.mdc-list-item--with-two-lines{height:64px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__start{margin-top:20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end{display:block;margin-top:0;line-height:normal}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before{display:inline-block;width:0;height:36px;content:"";vertical-align:0}.mdc-select__option-with-leading-content{padding-left:0;padding-right:12px}.mdc-select__option-with-leading-content.mdc-list-item{padding-left:0;padding-right:auto}[dir=rtl] .mdc-select__option-with-leading-content.mdc-list-item,.mdc-select__option-with-leading-content.mdc-list-item[dir=rtl]{padding-left:auto;padding-right:0}.mdc-select__option-with-leading-content .mdc-list-item__start{margin-left:12px;margin-right:0}[dir=rtl] .mdc-select__option-with-leading-content .mdc-list-item__start,.mdc-select__option-with-leading-content .mdc-list-item__start[dir=rtl]{margin-left:0;margin-right:12px}.mdc-select__option-with-leading-content .mdc-list-item__start{width:36px;height:24px}[dir=rtl] .mdc-select__option-with-leading-content,.mdc-select__option-with-leading-content[dir=rtl]{padding-left:12px;padding-right:0}.mdc-select__option-with-meta.mdc-list-item{padding-left:auto;padding-right:0}[dir=rtl] .mdc-select__option-with-meta.mdc-list-item,.mdc-select__option-with-meta.mdc-list-item[dir=rtl]{padding-left:0;padding-right:auto}.mdc-select__option-with-meta .mdc-list-item__end{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select__option-with-meta .mdc-list-item__end,.mdc-select__option-with-meta .mdc-list-item__end[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--filled .mdc-select__anchor{height:56px;display:flex;align-items:baseline}.mdc-select--filled .mdc-select__anchor::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor::before{display:none}.mdc-select--filled .mdc-select__anchor{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0}.mdc-select--filled:not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke}.mdc-select--filled.mdc-select--disabled .mdc-select__anchor{background-color:#fafafa}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-select--filled:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--filled.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-select--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-select--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-select--filled .mdc-menu-surface--is-open-below{border-top-left-radius:0px;border-top-right-radius:0px}.mdc-select--filled.mdc-select--focused.mdc-line-ripple::after{transform:scale(1, 2);opacity:1}.mdc-select--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-select--filled .mdc-floating-label,.mdc-select--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{left:48px;right:initial}[dir=rtl] .mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined{border:none}.mdc-select--outlined .mdc-select__anchor{height:56px}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-56px{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-select--outlined .mdc-select__anchor{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-select--outlined+.mdc-select-helper-text{margin-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined.mdc-select--disabled .mdc-select__anchor{background-color:transparent}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}.mdc-select--outlined .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-select--outlined .mdc-select__anchor{display:flex;align-items:baseline;overflow:visible}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--outlined .mdc-select__anchor::before{display:none}.mdc-select--outlined .mdc-select__selected-text-container{display:flex;border:none;z-index:1;background-color:transparent}.mdc-select--outlined .mdc-select__icon{z-index:2}.mdc-select--outlined .mdc-floating-label{line-height:1.15rem;left:4px;right:initial}[dir=rtl] .mdc-select--outlined .mdc-floating-label,.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-select--outlined.mdc-select--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake,.mdc-select--outlined.mdc-select--with-leading-icon[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 96px)}.mdc-select--outlined .mdc-menu-surface{margin-bottom:8px}.mdc-select--outlined.mdc-select--no-label .mdc-menu-surface,.mdc-select--outlined .mdc-menu-surface--is-open-below{margin-bottom:0}.mdc-select__anchor{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-select__anchor .mdc-select__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-select__anchor .mdc-select__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-select__anchor.mdc-ripple-upgraded--unbounded .mdc-select__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-select__anchor.mdc-ripple-upgraded--foreground-activation .mdc-select__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-select__anchor.mdc-ripple-upgraded--foreground-deactivation .mdc-select__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-select__anchor:hover .mdc-select__ripple::before,.mdc-select__anchor.mdc-ripple-surface--hover .mdc-select__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__anchor.mdc-ripple-upgraded--background-focused .mdc-select__ripple::before,.mdc-select__anchor:not(.mdc-ripple-upgraded):focus .mdc-select__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__anchor .mdc-select__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-deprecated-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-deprecated-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-deprecated-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-deprecated-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select-helper-text{margin:0;margin-left:16px;margin-right:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal}[dir=rtl] .mdc-select-helper-text,.mdc-select-helper-text[dir=rtl]{margin-left:16px;margin-right:16px}.mdc-select-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-select-helper-text--validation-msg{opacity:0;transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select--invalid+.mdc-select-helper-text--validation-msg,.mdc-select-helper-text--validation-msg-persistent{opacity:1}.mdc-select--with-leading-icon .mdc-select__icon{display:inline-block;box-sizing:border-box;border:none;text-decoration:none;cursor:pointer;user-select:none;flex-shrink:0;align-self:center;background-color:transparent;fill:currentColor}.mdc-select--with-leading-icon .mdc-select__icon{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__icon,.mdc-select--with-leading-icon .mdc-select__icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select__icon:not([tabindex]),.mdc-select__icon[tabindex="-1"]{cursor:default;pointer-events:none}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-block;vertical-align:top;outline:none}.mdc-select{width:100%}[hidden]{display:none}.mdc-select__icon{z-index:2}.mdc-select--with-leading-icon{--mdc-list-item-graphic-margin: calc( 48px - var(--mdc-list-item-graphic-size, 24px) - var(--mdc-list-side-padding, 16px) )}.mdc-select .mdc-select__anchor .mdc-select__selected-text{overflow:hidden}.mdc-select .mdc-select__anchor *{display:inline-flex}.mdc-select .mdc-select__anchor .mdc-floating-label{display:inline-block}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-idle-border-color, rgba(0, 0, 0, 0.38) );--mdc-notched-outline-notch-offset: 1px}:host(:not([disabled]):hover) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87);color:var(--mdc-select-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-select-idle-line-color, rgba(0, 0, 0, 0.42))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-select-hover-line-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--outlined):not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke;background-color:var(--mdc-select-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-select__dropdown-icon{fill:var(--mdc-select-error-dropdown-icon-color, var(--mdc-select-error-color, var(--mdc-theme-error, #b00020)))}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label,:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label::after{color:var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select.mdc-select--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}.mdc-select__menu--invalid{--mdc-theme-primary: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.6);color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54);fill:var(--mdc-select-dropdown-icon-color, rgba(0, 0, 0, 0.54))}:host(:not([disabled])) .mdc-select.mdc-select--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px;--mdc-notched-outline-notch-offset: 2px}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-select__dropdown-icon{fill:rgba(98,0,238,.87);fill:var(--mdc-select-focused-dropdown-icon-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)))}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label::after{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select-helper-text:not(.mdc-select-helper-text--validation-msg){color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]){pointer-events:none}:host([disabled]) .mdc-select:not(.mdc-select--outlined).mdc-select--disabled .mdc-select__anchor{background-color:#fafafa;background-color:var(--mdc-select-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-select.mdc-select--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-select .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38);fill:var(--mdc-select-disabled-dropdown-icon-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select-helper-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}`;let Ir=class extends Ar{constructor(){super(...arguments),this._translationsUpdated=fe((async()=>{await _e(),this.layoutOptions()}),500)}renderLeadingIcon(){return this.icon?N``:F}connectedCallback(){super.connectedCallback(),window.addEventListener("translations-updated",this._translationsUpdated)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("translations-updated",this._translationsUpdated)}};Ir.styles=[Sr],n([st({type:Boolean})],Ir.prototype,"icon",void 0),Ir=n([at("mushroom-select")],Ir);const Tr=["default","start","center","end","justify"],zr={default:"mdi:format-align-left",start:"mdi:format-align-left",center:"mdi:format-align-center",end:"mdi:format-align-right",justify:"mdi:format-align-justify"};let Or=class extends ot{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=Hi(this.hass),e=this.value||"default";return N` +const Yr=h`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px}.mdc-line-ripple::before{z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}.mdc-select{display:inline-flex;position:relative}.mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87)}.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-select.mdc-select--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#6200ee;fill:var(--mdc-theme-primary, #6200ee)}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled)+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__icon{color:rgba(0, 0, 0, 0.54)}.mdc-select.mdc-select--disabled .mdc-select__icon{color:rgba(0, 0, 0, 0.38)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:red}.mdc-select.mdc-select--disabled .mdc-floating-label{color:GrayText}.mdc-select.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}.mdc-select.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select.mdc-select--disabled .mdc-notched-outline__trailing{border-color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__icon{color:GrayText}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:GrayText}}.mdc-select .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-select .mdc-select__anchor{padding-left:16px;padding-right:0}[dir=rtl] .mdc-select .mdc-select__anchor,.mdc-select .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:16px}.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor{padding-left:0;padding-right:0}[dir=rtl] .mdc-select.mdc-select--with-leading-icon .mdc-select__anchor,.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:0}.mdc-select .mdc-select__icon{width:24px;height:24px;font-size:24px}.mdc-select .mdc-select__dropdown-icon{width:24px;height:24px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item,.mdc-select .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:12px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic,.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:12px;margin-right:0}.mdc-select__dropdown-icon{margin-left:12px;margin-right:12px;display:inline-flex;position:relative;align-self:center;align-items:center;justify-content:center;flex-shrink:0;pointer-events:none}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active,.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{position:absolute;top:0;left:0}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-graphic{width:41.6666666667%;height:20.8333333333%}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:1;transition:opacity 75ms linear 75ms}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:0;transition:opacity 75ms linear}[dir=rtl] .mdc-select__dropdown-icon,.mdc-select__dropdown-icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:0;transition:opacity 49.5ms linear}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:1;transition:opacity 100.5ms linear 49.5ms}.mdc-select__anchor{width:200px;min-width:0;flex:1 1 auto;position:relative;box-sizing:border-box;overflow:hidden;outline:none;cursor:pointer}.mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-select__selected-text-container{display:flex;appearance:none;pointer-events:none;box-sizing:border-box;width:auto;min-width:0;flex-grow:1;height:28px;border:none;outline:none;padding:0;background-color:transparent;color:inherit}.mdc-select__selected-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;width:100%;text-align:left}[dir=rtl] .mdc-select__selected-text,.mdc-select__selected-text[dir=rtl]{text-align:right}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--invalid+.mdc-select-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--disabled{cursor:default;pointer-events:none}.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item{padding-left:12px;padding-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item,.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:12px;padding-right:12px}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-select__menu::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid transparent;border-radius:inherit;content:"";pointer-events:none}}@media screen and (forced-colors: active)and (forced-colors: active),screen and (-ms-high-contrast: active)and (forced-colors: active){.mdc-select__menu::before{border-color:CanvasText}}.mdc-select__menu .mdc-deprecated-list .mdc-select__icon,.mdc-select__menu .mdc-list .mdc-select__icon{margin-left:0;margin-right:0}[dir=rtl] .mdc-select__menu .mdc-deprecated-list .mdc-select__icon,[dir=rtl] .mdc-select__menu .mdc-list .mdc-select__icon,.mdc-select__menu .mdc-deprecated-list .mdc-select__icon[dir=rtl],.mdc-select__menu .mdc-list .mdc-select__icon[dir=rtl]{margin-left:0;margin-right:0}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-list-item__start{display:inline-flex;align-items:center}.mdc-select__option{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select__option,.mdc-select__option[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select__one-line-option.mdc-list-item--with-one-line{height:48px}.mdc-select__two-line-option.mdc-list-item--with-two-lines{height:64px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__start{margin-top:20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end{display:block;margin-top:0;line-height:normal}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before{display:inline-block;width:0;height:36px;content:"";vertical-align:0}.mdc-select__option-with-leading-content{padding-left:0;padding-right:12px}.mdc-select__option-with-leading-content.mdc-list-item{padding-left:0;padding-right:auto}[dir=rtl] .mdc-select__option-with-leading-content.mdc-list-item,.mdc-select__option-with-leading-content.mdc-list-item[dir=rtl]{padding-left:auto;padding-right:0}.mdc-select__option-with-leading-content .mdc-list-item__start{margin-left:12px;margin-right:0}[dir=rtl] .mdc-select__option-with-leading-content .mdc-list-item__start,.mdc-select__option-with-leading-content .mdc-list-item__start[dir=rtl]{margin-left:0;margin-right:12px}.mdc-select__option-with-leading-content .mdc-list-item__start{width:36px;height:24px}[dir=rtl] .mdc-select__option-with-leading-content,.mdc-select__option-with-leading-content[dir=rtl]{padding-left:12px;padding-right:0}.mdc-select__option-with-meta.mdc-list-item{padding-left:auto;padding-right:0}[dir=rtl] .mdc-select__option-with-meta.mdc-list-item,.mdc-select__option-with-meta.mdc-list-item[dir=rtl]{padding-left:0;padding-right:auto}.mdc-select__option-with-meta .mdc-list-item__end{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select__option-with-meta .mdc-list-item__end,.mdc-select__option-with-meta .mdc-list-item__end[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--filled .mdc-select__anchor{height:56px;display:flex;align-items:baseline}.mdc-select--filled .mdc-select__anchor::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor::before{display:none}.mdc-select--filled .mdc-select__anchor{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0}.mdc-select--filled:not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke}.mdc-select--filled.mdc-select--disabled .mdc-select__anchor{background-color:#fafafa}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-select--filled:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--filled.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-select--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-select--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-select--filled .mdc-menu-surface--is-open-below{border-top-left-radius:0px;border-top-right-radius:0px}.mdc-select--filled.mdc-select--focused.mdc-line-ripple::after{transform:scale(1, 2);opacity:1}.mdc-select--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-select--filled .mdc-floating-label,.mdc-select--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{left:48px;right:initial}[dir=rtl] .mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined{border:none}.mdc-select--outlined .mdc-select__anchor{height:56px}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-56px{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-select--outlined .mdc-select__anchor{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-select--outlined+.mdc-select-helper-text{margin-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined.mdc-select--disabled .mdc-select__anchor{background-color:transparent}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}.mdc-select--outlined .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-select--outlined .mdc-select__anchor{display:flex;align-items:baseline;overflow:visible}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--outlined .mdc-select__anchor::before{display:none}.mdc-select--outlined .mdc-select__selected-text-container{display:flex;border:none;z-index:1;background-color:transparent}.mdc-select--outlined .mdc-select__icon{z-index:2}.mdc-select--outlined .mdc-floating-label{line-height:1.15rem;left:4px;right:initial}[dir=rtl] .mdc-select--outlined .mdc-floating-label,.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-select--outlined.mdc-select--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake,.mdc-select--outlined.mdc-select--with-leading-icon[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 96px)}.mdc-select--outlined .mdc-menu-surface{margin-bottom:8px}.mdc-select--outlined.mdc-select--no-label .mdc-menu-surface,.mdc-select--outlined .mdc-menu-surface--is-open-below{margin-bottom:0}.mdc-select__anchor{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-select__anchor .mdc-select__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-select__anchor .mdc-select__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-select__anchor.mdc-ripple-upgraded--unbounded .mdc-select__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-select__anchor.mdc-ripple-upgraded--foreground-activation .mdc-select__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-select__anchor.mdc-ripple-upgraded--foreground-deactivation .mdc-select__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-select__anchor:hover .mdc-select__ripple::before,.mdc-select__anchor.mdc-ripple-surface--hover .mdc-select__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__anchor.mdc-ripple-upgraded--background-focused .mdc-select__ripple::before,.mdc-select__anchor:not(.mdc-ripple-upgraded):focus .mdc-select__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__anchor .mdc-select__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-deprecated-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-deprecated-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-deprecated-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-deprecated-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select-helper-text{margin:0;margin-left:16px;margin-right:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal}[dir=rtl] .mdc-select-helper-text,.mdc-select-helper-text[dir=rtl]{margin-left:16px;margin-right:16px}.mdc-select-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-select-helper-text--validation-msg{opacity:0;transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select--invalid+.mdc-select-helper-text--validation-msg,.mdc-select-helper-text--validation-msg-persistent{opacity:1}.mdc-select--with-leading-icon .mdc-select__icon{display:inline-block;box-sizing:border-box;border:none;text-decoration:none;cursor:pointer;user-select:none;flex-shrink:0;align-self:center;background-color:transparent;fill:currentColor}.mdc-select--with-leading-icon .mdc-select__icon{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__icon,.mdc-select--with-leading-icon .mdc-select__icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select__icon:not([tabindex]),.mdc-select__icon[tabindex="-1"]{cursor:default;pointer-events:none}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-block;vertical-align:top;outline:none}.mdc-select{width:100%}[hidden]{display:none}.mdc-select__icon{z-index:2}.mdc-select--with-leading-icon{--mdc-list-item-graphic-margin: calc( 48px - var(--mdc-list-item-graphic-size, 24px) - var(--mdc-list-side-padding, 16px) )}.mdc-select .mdc-select__anchor .mdc-select__selected-text{overflow:hidden}.mdc-select .mdc-select__anchor *{display:inline-flex}.mdc-select .mdc-select__anchor .mdc-floating-label{display:inline-block}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-idle-border-color, rgba(0, 0, 0, 0.38) );--mdc-notched-outline-notch-offset: 1px}:host(:not([disabled]):hover) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87);color:var(--mdc-select-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-select-idle-line-color, rgba(0, 0, 0, 0.42))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-select-hover-line-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--outlined):not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke;background-color:var(--mdc-select-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-select__dropdown-icon{fill:var(--mdc-select-error-dropdown-icon-color, var(--mdc-select-error-color, var(--mdc-theme-error, #b00020)))}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label,:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label::after{color:var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select.mdc-select--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}.mdc-select__menu--invalid{--mdc-theme-primary: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.6);color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54);fill:var(--mdc-select-dropdown-icon-color, rgba(0, 0, 0, 0.54))}:host(:not([disabled])) .mdc-select.mdc-select--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px;--mdc-notched-outline-notch-offset: 2px}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-select__dropdown-icon{fill:rgba(98,0,238,.87);fill:var(--mdc-select-focused-dropdown-icon-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)))}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label::after{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select-helper-text:not(.mdc-select-helper-text--validation-msg){color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]){pointer-events:none}:host([disabled]) .mdc-select:not(.mdc-select--outlined).mdc-select--disabled .mdc-select__anchor{background-color:#fafafa;background-color:var(--mdc-select-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-select.mdc-select--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-select .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38);fill:var(--mdc-select-disabled-dropdown-icon-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select-helper-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}`;let Xr=class extends Hr{constructor(){super(...arguments),this._translationsUpdated=Ae((async()=>{await Ie(),this.layoutOptions()}),500)}renderLeadingIcon(){return this.icon?B``:Y}connectedCallback(){super.connectedCallback(),window.addEventListener("translations-updated",this._translationsUpdated)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("translations-updated",this._translationsUpdated)}};Xr.styles=[Yr,h` + .mdc-select__anchor { + height: var(--select-height, 56px) !important; + } + `],n([ht({type:Boolean})],Xr.prototype,"icon",void 0),Xr=n([dt("mushroom-select")],Xr);const Wr=["default","start","center","end","justify"],qr={default:"mdi:format-align-left",start:"mdi:format-align-left",center:"mdi:format-align-center",end:"mdi:format-align-right",justify:"mdi:format-align-justify"};let Kr=class extends st{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=ao(this.hass),e=this.value||"default";return B` - - ${Tr.map((e=>N` + + ${Wr.map((e=>B` ${t(`editor.form.alignment_picker.values.${e}`)} - + `))} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-select { width: 100%; } - `}};n([st()],Or.prototype,"label",void 0),n([st()],Or.prototype,"value",void 0),n([st()],Or.prototype,"configValue",void 0),n([st()],Or.prototype,"hass",void 0),Or=n([at("mushroom-alignment-picker")],Or);let Mr=class extends ot{render(){return N` + `}};n([ht()],Kr.prototype,"label",void 0),n([ht()],Kr.prototype,"value",void 0),n([ht()],Kr.prototype,"configValue",void 0),n([ht()],Kr.prototype,"hass",void 0),Kr=n([dt("mushroom-alignment-picker")],Kr);let Gr=class extends st{render(){return B` - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],Mr.prototype,"hass",void 0),n([st()],Mr.prototype,"selector",void 0),n([st()],Mr.prototype,"value",void 0),n([st()],Mr.prototype,"label",void 0),Mr=n([at("ha-selector-mush-alignment")],Mr); + `}_valueChanged(t){zt(this,"value-changed",{value:t.detail.value||void 0})}};n([ht()],Gr.prototype,"hass",void 0),n([ht()],Gr.prototype,"selector",void 0),n([ht()],Gr.prototype,"value",void 0),n([ht()],Gr.prototype,"label",void 0),Gr=n([dt("ha-selector-mush-alignment")],Gr); /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"style"!==t.name||(null===(e=t.strings)||void 0===e?void 0:e.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce(((e,i)=>{const n=t[i];return null==n?e:e+`${i=i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${n};`}),"")}update(t,[e]){const{style:i}=t.element;if(void 0===this.ct){this.ct=new Set;for(const t in e)this.ct.add(t);return this.render(e)}this.ct.forEach((t=>{null==e[t]&&(this.ct.delete(t),t.includes("-")?i.removeProperty(t):i[t]="")}));for(const t in e){const n=e[t];null!=n&&(this.ct.add(t),t.includes("-")?i.setProperty(t,n):i[t]=n)}return R}});var Dr={exports:{}},jr={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},Pr={exports:{}},Nr=function(t){return!(!t||"string"==typeof t)&&(t instanceof Array||Array.isArray(t)||t.length>=0&&(t.splice instanceof Function||Object.getOwnPropertyDescriptor(t,t.length-1)&&"String"!==t.constructor.name))},Vr=Array.prototype.concat,Rr=Array.prototype.slice,Fr=Pr.exports=function(t){for(var e=[],i=0,n=t.length;i=4&&1!==t[3]&&(e=", "+t[3]),"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+e+")"},Wr.to.keyword=function(t){return Yr[t.slice(0,3)]};const Gr=jr,Zr={};for(const t of Object.keys(Gr))Zr[Gr[t]]=t;const Jr={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};var Qr=Jr;for(const t of Object.keys(Jr)){if(!("channels"in Jr[t]))throw new Error("missing channels property: "+t);if(!("labels"in Jr[t]))throw new Error("missing channel labels property: "+t);if(Jr[t].labels.length!==Jr[t].channels)throw new Error("channel and label counts mismatch: "+t);const{channels:e,labels:i}=Jr[t];delete Jr[t].channels,delete Jr[t].labels,Object.defineProperty(Jr[t],"channels",{value:e}),Object.defineProperty(Jr[t],"labels",{value:i})}function ta(t,e){return(t[0]-e[0])**2+(t[1]-e[1])**2+(t[2]-e[2])**2}Jr.rgb.hsl=function(t){const e=t[0]/255,i=t[1]/255,n=t[2]/255,o=Math.min(e,i,n),r=Math.max(e,i,n),a=r-o;let l,s;r===o?l=0:e===r?l=(i-n)/a:i===r?l=2+(n-e)/a:n===r&&(l=4+(e-i)/a),l=Math.min(60*l,360),l<0&&(l+=360);const c=(o+r)/2;return s=r===o?0:c<=.5?a/(r+o):a/(2-r-o),[l,100*s,100*c]},Jr.rgb.hsv=function(t){let e,i,n,o,r;const a=t[0]/255,l=t[1]/255,s=t[2]/255,c=Math.max(a,l,s),d=c-Math.min(a,l,s),u=function(t){return(c-t)/6/d+.5};return 0===d?(o=0,r=0):(r=d/c,e=u(a),i=u(l),n=u(s),a===c?o=n-i:l===c?o=1/3+e-n:s===c&&(o=2/3+i-e),o<0?o+=1:o>1&&(o-=1)),[360*o,100*r,100*c]},Jr.rgb.hwb=function(t){const e=t[0],i=t[1];let n=t[2];const o=Jr.rgb.hsl(t)[0],r=1/255*Math.min(e,Math.min(i,n));return n=1-1/255*Math.max(e,Math.max(i,n)),[o,100*r,100*n]},Jr.rgb.cmyk=function(t){const e=t[0]/255,i=t[1]/255,n=t[2]/255,o=Math.min(1-e,1-i,1-n);return[100*((1-e-o)/(1-o)||0),100*((1-i-o)/(1-o)||0),100*((1-n-o)/(1-o)||0),100*o]},Jr.rgb.keyword=function(t){const e=Zr[t];if(e)return e;let i,n=1/0;for(const e of Object.keys(Gr)){const o=ta(t,Gr[e]);o.04045?((e+.055)/1.055)**2.4:e/12.92,i=i>.04045?((i+.055)/1.055)**2.4:i/12.92,n=n>.04045?((n+.055)/1.055)**2.4:n/12.92;return[100*(.4124*e+.3576*i+.1805*n),100*(.2126*e+.7152*i+.0722*n),100*(.0193*e+.1192*i+.9505*n)]},Jr.rgb.lab=function(t){const e=Jr.rgb.xyz(t);let i=e[0],n=e[1],o=e[2];i/=95.047,n/=100,o/=108.883,i=i>.008856?i**(1/3):7.787*i+16/116,n=n>.008856?n**(1/3):7.787*n+16/116,o=o>.008856?o**(1/3):7.787*o+16/116;return[116*n-16,500*(i-n),200*(n-o)]},Jr.hsl.rgb=function(t){const e=t[0]/360,i=t[1]/100,n=t[2]/100;let o,r,a;if(0===i)return a=255*n,[a,a,a];o=n<.5?n*(1+i):n+i-n*i;const l=2*n-o,s=[0,0,0];for(let t=0;t<3;t++)r=e+1/3*-(t-1),r<0&&r++,r>1&&r--,a=6*r<1?l+6*(o-l)*r:2*r<1?o:3*r<2?l+(o-l)*(2/3-r)*6:l,s[t]=255*a;return s},Jr.hsl.hsv=function(t){const e=t[0];let i=t[1]/100,n=t[2]/100,o=i;const r=Math.max(n,.01);n*=2,i*=n<=1?n:2-n,o*=r<=1?r:2-r;return[e,100*(0===n?2*o/(r+o):2*i/(n+i)),100*((n+i)/2)]},Jr.hsv.rgb=function(t){const e=t[0]/60,i=t[1]/100;let n=t[2]/100;const o=Math.floor(e)%6,r=e-Math.floor(e),a=255*n*(1-i),l=255*n*(1-i*r),s=255*n*(1-i*(1-r));switch(n*=255,o){case 0:return[n,s,a];case 1:return[l,n,a];case 2:return[a,n,s];case 3:return[a,l,n];case 4:return[s,a,n];case 5:return[n,a,l]}},Jr.hsv.hsl=function(t){const e=t[0],i=t[1]/100,n=t[2]/100,o=Math.max(n,.01);let r,a;a=(2-i)*n;const l=(2-i)*o;return r=i*o,r/=l<=1?l:2-l,r=r||0,a/=2,[e,100*r,100*a]},Jr.hwb.rgb=function(t){const e=t[0]/360;let i=t[1]/100,n=t[2]/100;const o=i+n;let r;o>1&&(i/=o,n/=o);const a=Math.floor(6*e),l=1-n;r=6*e-a,0!=(1&a)&&(r=1-r);const s=i+r*(l-i);let c,d,u;switch(a){default:case 6:case 0:c=l,d=s,u=i;break;case 1:c=s,d=l,u=i;break;case 2:c=i,d=l,u=s;break;case 3:c=i,d=s,u=l;break;case 4:c=s,d=i,u=l;break;case 5:c=l,d=i,u=s}return[255*c,255*d,255*u]},Jr.cmyk.rgb=function(t){const e=t[0]/100,i=t[1]/100,n=t[2]/100,o=t[3]/100;return[255*(1-Math.min(1,e*(1-o)+o)),255*(1-Math.min(1,i*(1-o)+o)),255*(1-Math.min(1,n*(1-o)+o))]},Jr.xyz.rgb=function(t){const e=t[0]/100,i=t[1]/100,n=t[2]/100;let o,r,a;return o=3.2406*e+-1.5372*i+-.4986*n,r=-.9689*e+1.8758*i+.0415*n,a=.0557*e+-.204*i+1.057*n,o=o>.0031308?1.055*o**(1/2.4)-.055:12.92*o,r=r>.0031308?1.055*r**(1/2.4)-.055:12.92*r,a=a>.0031308?1.055*a**(1/2.4)-.055:12.92*a,o=Math.min(Math.max(0,o),1),r=Math.min(Math.max(0,r),1),a=Math.min(Math.max(0,a),1),[255*o,255*r,255*a]},Jr.xyz.lab=function(t){let e=t[0],i=t[1],n=t[2];e/=95.047,i/=100,n/=108.883,e=e>.008856?e**(1/3):7.787*e+16/116,i=i>.008856?i**(1/3):7.787*i+16/116,n=n>.008856?n**(1/3):7.787*n+16/116;return[116*i-16,500*(e-i),200*(i-n)]},Jr.lab.xyz=function(t){let e,i,n;i=(t[0]+16)/116,e=t[1]/500+i,n=i-t[2]/200;const o=i**3,r=e**3,a=n**3;return i=o>.008856?o:(i-16/116)/7.787,e=r>.008856?r:(e-16/116)/7.787,n=a>.008856?a:(n-16/116)/7.787,e*=95.047,i*=100,n*=108.883,[e,i,n]},Jr.lab.lch=function(t){const e=t[0],i=t[1],n=t[2];let o;o=360*Math.atan2(n,i)/2/Math.PI,o<0&&(o+=360);return[e,Math.sqrt(i*i+n*n),o]},Jr.lch.lab=function(t){const e=t[0],i=t[1],n=t[2]/360*2*Math.PI;return[e,i*Math.cos(n),i*Math.sin(n)]},Jr.rgb.ansi16=function(t,e=null){const[i,n,o]=t;let r=null===e?Jr.rgb.hsv(t)[2]:e;if(r=Math.round(r/50),0===r)return 30;let a=30+(Math.round(o/255)<<2|Math.round(n/255)<<1|Math.round(i/255));return 2===r&&(a+=60),a},Jr.hsv.ansi16=function(t){return Jr.rgb.ansi16(Jr.hsv.rgb(t),t[2])},Jr.rgb.ansi256=function(t){const e=t[0],i=t[1],n=t[2];if(e===i&&i===n)return e<8?16:e>248?231:Math.round((e-8)/247*24)+232;return 16+36*Math.round(e/255*5)+6*Math.round(i/255*5)+Math.round(n/255*5)},Jr.ansi16.rgb=function(t){let e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),e=e/10.5*255,[e,e,e];const i=.5*(1+~~(t>50));return[(1&e)*i*255,(e>>1&1)*i*255,(e>>2&1)*i*255]},Jr.ansi256.rgb=function(t){if(t>=232){const e=10*(t-232)+8;return[e,e,e]}let e;t-=16;return[Math.floor(t/36)/5*255,Math.floor((e=t%36)/6)/5*255,e%6/5*255]},Jr.rgb.hex=function(t){const e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},Jr.hex.rgb=function(t){const e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];let i=e[0];3===e[0].length&&(i=i.split("").map((t=>t+t)).join(""));const n=parseInt(i,16);return[n>>16&255,n>>8&255,255&n]},Jr.rgb.hcg=function(t){const e=t[0]/255,i=t[1]/255,n=t[2]/255,o=Math.max(Math.max(e,i),n),r=Math.min(Math.min(e,i),n),a=o-r;let l,s;return l=a<1?r/(1-a):0,s=a<=0?0:o===e?(i-n)/a%6:o===i?2+(n-e)/a:4+(e-i)/a,s/=6,s%=1,[360*s,100*a,100*l]},Jr.hsl.hcg=function(t){const e=t[1]/100,i=t[2]/100,n=i<.5?2*e*i:2*e*(1-i);let o=0;return n<1&&(o=(i-.5*n)/(1-n)),[t[0],100*n,100*o]},Jr.hsv.hcg=function(t){const e=t[1]/100,i=t[2]/100,n=e*i;let o=0;return n<1&&(o=(i-n)/(1-n)),[t[0],100*n,100*o]},Jr.hcg.rgb=function(t){const e=t[0]/360,i=t[1]/100,n=t[2]/100;if(0===i)return[255*n,255*n,255*n];const o=[0,0,0],r=e%1*6,a=r%1,l=1-a;let s=0;switch(Math.floor(r)){case 0:o[0]=1,o[1]=a,o[2]=0;break;case 1:o[0]=l,o[1]=1,o[2]=0;break;case 2:o[0]=0,o[1]=1,o[2]=a;break;case 3:o[0]=0,o[1]=l,o[2]=1;break;case 4:o[0]=a,o[1]=0,o[2]=1;break;default:o[0]=1,o[1]=0,o[2]=l}return s=(1-i)*n,[255*(i*o[0]+s),255*(i*o[1]+s),255*(i*o[2]+s)]},Jr.hcg.hsv=function(t){const e=t[1]/100,i=e+t[2]/100*(1-e);let n=0;return i>0&&(n=e/i),[t[0],100*n,100*i]},Jr.hcg.hsl=function(t){const e=t[1]/100,i=t[2]/100*(1-e)+.5*e;let n=0;return i>0&&i<.5?n=e/(2*i):i>=.5&&i<1&&(n=e/(2*(1-i))),[t[0],100*n,100*i]},Jr.hcg.hwb=function(t){const e=t[1]/100,i=e+t[2]/100*(1-e);return[t[0],100*(i-e),100*(1-i)]},Jr.hwb.hcg=function(t){const e=t[1]/100,i=1-t[2]/100,n=i-e;let o=0;return n<1&&(o=(i-n)/(1-n)),[t[0],100*n,100*o]},Jr.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},Jr.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},Jr.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},Jr.gray.hsl=function(t){return[0,0,t[0]]},Jr.gray.hsv=Jr.gray.hsl,Jr.gray.hwb=function(t){return[0,100,t[0]]},Jr.gray.cmyk=function(t){return[0,0,0,t[0]]},Jr.gray.lab=function(t){return[t[0],0,0]},Jr.gray.hex=function(t){const e=255&Math.round(t[0]/100*255),i=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(i.length)+i},Jr.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]};const ea=Qr;function ia(t){const e=function(){const t={},e=Object.keys(ea);for(let i=e.length,n=0;n{la[t]={},Object.defineProperty(la[t],"channels",{value:ra[t].channels}),Object.defineProperty(la[t],"labels",{value:ra[t].labels});const e=aa(t);Object.keys(e).forEach((i=>{const n=e[i];la[t][i]=function(t){const e=function(...e){const i=e[0];if(null==i)return i;i.length>1&&(e=i);const n=t(e);if("object"==typeof n)for(let t=n.length,e=0;e1&&(e=i),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(n)}))}));var sa=la;const ca=Dr.exports,da=sa,ua=["keyword","gray","hex"],ha={};for(const t of Object.keys(da))ha[[...da[t].labels].sort().join("")]=t;const ma={};function pa(t,e){if(!(this instanceof pa))return new pa(t,e);if(e&&e in ua&&(e=null),e&&!(e in da))throw new Error("Unknown model: "+e);let i,n;if(null==t)this.model="rgb",this.color=[0,0,0],this.valpha=1;else if(t instanceof pa)this.model=t.model,this.color=[...t.color],this.valpha=t.valpha;else if("string"==typeof t){const e=ca.get(t);if(null===e)throw new Error("Unable to parse color from string: "+t);this.model=e.model,n=da[this.model].channels,this.color=e.value.slice(0,n),this.valpha="number"==typeof e.value[n]?e.value[n]:1}else if(t.length>0){this.model=e||"rgb",n=da[this.model].channels;const i=Array.prototype.slice.call(t,0,n);this.color=va(i,n),this.valpha="number"==typeof t[n]?t[n]:1}else if("number"==typeof t)this.model="rgb",this.color=[t>>16&255,t>>8&255,255&t],this.valpha=1;else{this.valpha=1;const e=Object.keys(t);"alpha"in t&&(e.splice(e.indexOf("alpha"),1),this.valpha="number"==typeof t.alpha?t.alpha:0);const n=e.sort().join("");if(!(n in ha))throw new Error("Unable to parse color from object: "+JSON.stringify(t));this.model=ha[n];const{labels:o}=da[this.model],r=[];for(i=0;i(t%360+360)%360)),saturationl:ga("hsl",1,_a(100)),lightness:ga("hsl",2,_a(100)),saturationv:ga("hsv",1,_a(100)),value:ga("hsv",2,_a(100)),chroma:ga("hcg",1,_a(100)),gray:ga("hcg",2,_a(100)),white:ga("hwb",1,_a(100)),wblack:ga("hwb",2,_a(100)),cyan:ga("cmyk",0,_a(100)),magenta:ga("cmyk",1,_a(100)),yellow:ga("cmyk",2,_a(100)),black:ga("cmyk",3,_a(100)),x:ga("xyz",0,_a(95.047)),y:ga("xyz",1,_a(100)),z:ga("xyz",2,_a(108.833)),l:ga("lab",0,_a(100)),a:ga("lab",1),b:ga("lab",2),keyword(t){return void 0!==t?new pa(t):da[this.model].keyword(this.color)},hex(t){return void 0!==t?new pa(t):ca.to.hex(this.rgb().round().color)},hexa(t){if(void 0!==t)return new pa(t);const e=this.rgb().round().color;let i=Math.round(255*this.valpha).toString(16).toUpperCase();return 1===i.length&&(i="0"+i),ca.to.hex(e)+i},rgbNumber(){const t=this.rgb().color;return(255&t[0])<<16|(255&t[1])<<8|255&t[2]},luminosity(){const t=this.rgb().color,e=[];for(const[i,n]of t.entries()){const t=n/255;e[i]=t<=.04045?t/12.92:((t+.055)/1.055)**2.4}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast(t){const e=this.luminosity(),i=t.luminosity();return e>i?(e+.05)/(i+.05):(i+.05)/(e+.05)},level(t){const e=this.contrast(t);return e>=7?"AAA":e>=4.5?"AA":""},isDark(){const t=this.rgb().color;return(2126*t[0]+7152*t[1]+722*t[2])/1e4<128},isLight(){return!this.isDark()},negate(){const t=this.rgb();for(let e=0;e<3;e++)t.color[e]=255-t.color[e];return t},lighten(t){const e=this.hsl();return e.color[2]+=e.color[2]*t,e},darken(t){const e=this.hsl();return e.color[2]-=e.color[2]*t,e},saturate(t){const e=this.hsl();return e.color[1]+=e.color[1]*t,e},desaturate(t){const e=this.hsl();return e.color[1]-=e.color[1]*t,e},whiten(t){const e=this.hwb();return e.color[1]+=e.color[1]*t,e},blacken(t){const e=this.hwb();return e.color[2]+=e.color[2]*t,e},grayscale(){const t=this.rgb().color,e=.3*t[0]+.59*t[1]+.11*t[2];return pa.rgb(e,e,e)},fade(t){return this.alpha(this.valpha-this.valpha*t)},opaquer(t){return this.alpha(this.valpha+this.valpha*t)},rotate(t){const e=this.hsl();let i=e.color[0];return i=(i+t)%360,i=i<0?360+i:i,e.color[0]=i,e},mix(t,e){if(!t||!t.rgb)throw new Error('Argument to "mix" was not a Color instance, but rather an instance of '+typeof t);const i=t.rgb(),n=this.rgb(),o=void 0===e?.5:e,r=2*o-1,a=i.alpha()-n.alpha(),l=((r*a==-1?r:(r+a)/(1+r*a))+1)/2,s=1-l;return pa.rgb(l*i.red()+s*n.red(),l*i.green()+s*n.green(),l*i.blue()+s*n.blue(),i.alpha()*o+n.alpha()*(1-o))}};for(const t of Object.keys(da)){if(ua.includes(t))continue;const{channels:e}=da[t];pa.prototype[t]=function(...e){return this.model===t?new pa(this):e.length>0?new pa(e,t):new pa([...(i=da[this.model][t].raw(this.color),Array.isArray(i)?i:[i]),this.valpha],t);var i},pa[t]=function(...i){let n=i[0];return"number"==typeof n&&(n=va(i,e)),new pa(n,t)}}function fa(t){return function(e){return function(t,e){return Number(t.toFixed(e))}(e,t)}}function ga(t,e,i){t=Array.isArray(t)?t:[t];for(const n of t)(ma[n]||(ma[n]=[]))[e]=i;return t=t[0],function(n){let o;return void 0!==n?(i&&(n=i(n)),o=this[t](),o.color[e]=n,o):(o=this[t]().color[e],i&&(o=i(o)),o)}}function _a(t){return function(e){return Math.max(0,Math.min(t,e))}}function va(t,e){for(let i=0;i2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce(((e,i)=>{const o=t[i];return null==o?e:e+`${i=i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${o};`}),"")}update(t,[e]){const{style:i}=t.element;if(void 0===this.vt){this.vt=new Set;for(const t in e)this.vt.add(t);return this.render(e)}this.vt.forEach((t=>{null==e[t]&&(this.vt.delete(t),t.includes("-")?i.removeProperty(t):i[t]="")}));for(const t in e){const o=e[t];null!=o&&(this.vt.add(t),t.includes("-")?i.setProperty(t,o):i[t]=o)}return H}});var Jr={},Qr={get exports(){return Jr},set exports(t){Jr=t}},ta={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},ea={},ia={get exports(){return ea},set exports(t){ea=t}},oa=function(t){return!(!t||"string"==typeof t)&&(t instanceof Array||Array.isArray(t)||t.length>=0&&(t.splice instanceof Function||Object.getOwnPropertyDescriptor(t,t.length-1)&&"String"!==t.constructor.name))},na=Array.prototype.concat,ra=Array.prototype.slice,aa=ia.exports=function(t){for(var e=[],i=0,o=t.length;i=4&&1!==t[3]&&(e=", "+t[3]),"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+e+")"},ha.to.keyword=function(t){return da[t.slice(0,3)]};const fa=ta,ga={};for(const t of Object.keys(fa))ga[fa[t]]=t;const _a={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};var va=_a;for(const t of Object.keys(_a)){if(!("channels"in _a[t]))throw new Error("missing channels property: "+t);if(!("labels"in _a[t]))throw new Error("missing channel labels property: "+t);if(_a[t].labels.length!==_a[t].channels)throw new Error("channel and label counts mismatch: "+t);const{channels:e,labels:i}=_a[t];delete _a[t].channels,delete _a[t].labels,Object.defineProperty(_a[t],"channels",{value:e}),Object.defineProperty(_a[t],"labels",{value:i})}function ba(t,e){return(t[0]-e[0])**2+(t[1]-e[1])**2+(t[2]-e[2])**2}_a.rgb.hsl=function(t){const e=t[0]/255,i=t[1]/255,o=t[2]/255,n=Math.min(e,i,o),r=Math.max(e,i,o),a=r-n;let l,s;r===n?l=0:e===r?l=(i-o)/a:i===r?l=2+(o-e)/a:o===r&&(l=4+(e-i)/a),l=Math.min(60*l,360),l<0&&(l+=360);const c=(n+r)/2;return s=r===n?0:c<=.5?a/(r+n):a/(2-r-n),[l,100*s,100*c]},_a.rgb.hsv=function(t){let e,i,o,n,r;const a=t[0]/255,l=t[1]/255,s=t[2]/255,c=Math.max(a,l,s),d=c-Math.min(a,l,s),u=function(t){return(c-t)/6/d+.5};return 0===d?(n=0,r=0):(r=d/c,e=u(a),i=u(l),o=u(s),a===c?n=o-i:l===c?n=1/3+e-o:s===c&&(n=2/3+i-e),n<0?n+=1:n>1&&(n-=1)),[360*n,100*r,100*c]},_a.rgb.hwb=function(t){const e=t[0],i=t[1];let o=t[2];const n=_a.rgb.hsl(t)[0],r=1/255*Math.min(e,Math.min(i,o));return o=1-1/255*Math.max(e,Math.max(i,o)),[n,100*r,100*o]},_a.rgb.cmyk=function(t){const e=t[0]/255,i=t[1]/255,o=t[2]/255,n=Math.min(1-e,1-i,1-o);return[100*((1-e-n)/(1-n)||0),100*((1-i-n)/(1-n)||0),100*((1-o-n)/(1-n)||0),100*n]},_a.rgb.keyword=function(t){const e=ga[t];if(e)return e;let i,o=1/0;for(const e of Object.keys(fa)){const n=ba(t,fa[e]);n.04045?((e+.055)/1.055)**2.4:e/12.92,i=i>.04045?((i+.055)/1.055)**2.4:i/12.92,o=o>.04045?((o+.055)/1.055)**2.4:o/12.92;return[100*(.4124*e+.3576*i+.1805*o),100*(.2126*e+.7152*i+.0722*o),100*(.0193*e+.1192*i+.9505*o)]},_a.rgb.lab=function(t){const e=_a.rgb.xyz(t);let i=e[0],o=e[1],n=e[2];i/=95.047,o/=100,n/=108.883,i=i>.008856?i**(1/3):7.787*i+16/116,o=o>.008856?o**(1/3):7.787*o+16/116,n=n>.008856?n**(1/3):7.787*n+16/116;return[116*o-16,500*(i-o),200*(o-n)]},_a.hsl.rgb=function(t){const e=t[0]/360,i=t[1]/100,o=t[2]/100;let n,r,a;if(0===i)return a=255*o,[a,a,a];n=o<.5?o*(1+i):o+i-o*i;const l=2*o-n,s=[0,0,0];for(let t=0;t<3;t++)r=e+1/3*-(t-1),r<0&&r++,r>1&&r--,a=6*r<1?l+6*(n-l)*r:2*r<1?n:3*r<2?l+(n-l)*(2/3-r)*6:l,s[t]=255*a;return s},_a.hsl.hsv=function(t){const e=t[0];let i=t[1]/100,o=t[2]/100,n=i;const r=Math.max(o,.01);o*=2,i*=o<=1?o:2-o,n*=r<=1?r:2-r;return[e,100*(0===o?2*n/(r+n):2*i/(o+i)),100*((o+i)/2)]},_a.hsv.rgb=function(t){const e=t[0]/60,i=t[1]/100;let o=t[2]/100;const n=Math.floor(e)%6,r=e-Math.floor(e),a=255*o*(1-i),l=255*o*(1-i*r),s=255*o*(1-i*(1-r));switch(o*=255,n){case 0:return[o,s,a];case 1:return[l,o,a];case 2:return[a,o,s];case 3:return[a,l,o];case 4:return[s,a,o];case 5:return[o,a,l]}},_a.hsv.hsl=function(t){const e=t[0],i=t[1]/100,o=t[2]/100,n=Math.max(o,.01);let r,a;a=(2-i)*o;const l=(2-i)*n;return r=i*n,r/=l<=1?l:2-l,r=r||0,a/=2,[e,100*r,100*a]},_a.hwb.rgb=function(t){const e=t[0]/360;let i=t[1]/100,o=t[2]/100;const n=i+o;let r;n>1&&(i/=n,o/=n);const a=Math.floor(6*e),l=1-o;r=6*e-a,0!=(1&a)&&(r=1-r);const s=i+r*(l-i);let c,d,u;switch(a){default:case 6:case 0:c=l,d=s,u=i;break;case 1:c=s,d=l,u=i;break;case 2:c=i,d=l,u=s;break;case 3:c=i,d=s,u=l;break;case 4:c=s,d=i,u=l;break;case 5:c=l,d=i,u=s}return[255*c,255*d,255*u]},_a.cmyk.rgb=function(t){const e=t[0]/100,i=t[1]/100,o=t[2]/100,n=t[3]/100;return[255*(1-Math.min(1,e*(1-n)+n)),255*(1-Math.min(1,i*(1-n)+n)),255*(1-Math.min(1,o*(1-n)+n))]},_a.xyz.rgb=function(t){const e=t[0]/100,i=t[1]/100,o=t[2]/100;let n,r,a;return n=3.2406*e+-1.5372*i+-.4986*o,r=-.9689*e+1.8758*i+.0415*o,a=.0557*e+-.204*i+1.057*o,n=n>.0031308?1.055*n**(1/2.4)-.055:12.92*n,r=r>.0031308?1.055*r**(1/2.4)-.055:12.92*r,a=a>.0031308?1.055*a**(1/2.4)-.055:12.92*a,n=Math.min(Math.max(0,n),1),r=Math.min(Math.max(0,r),1),a=Math.min(Math.max(0,a),1),[255*n,255*r,255*a]},_a.xyz.lab=function(t){let e=t[0],i=t[1],o=t[2];e/=95.047,i/=100,o/=108.883,e=e>.008856?e**(1/3):7.787*e+16/116,i=i>.008856?i**(1/3):7.787*i+16/116,o=o>.008856?o**(1/3):7.787*o+16/116;return[116*i-16,500*(e-i),200*(i-o)]},_a.lab.xyz=function(t){let e,i,o;i=(t[0]+16)/116,e=t[1]/500+i,o=i-t[2]/200;const n=i**3,r=e**3,a=o**3;return i=n>.008856?n:(i-16/116)/7.787,e=r>.008856?r:(e-16/116)/7.787,o=a>.008856?a:(o-16/116)/7.787,e*=95.047,i*=100,o*=108.883,[e,i,o]},_a.lab.lch=function(t){const e=t[0],i=t[1],o=t[2];let n;n=360*Math.atan2(o,i)/2/Math.PI,n<0&&(n+=360);return[e,Math.sqrt(i*i+o*o),n]},_a.lch.lab=function(t){const e=t[0],i=t[1],o=t[2]/360*2*Math.PI;return[e,i*Math.cos(o),i*Math.sin(o)]},_a.rgb.ansi16=function(t,e=null){const[i,o,n]=t;let r=null===e?_a.rgb.hsv(t)[2]:e;if(r=Math.round(r/50),0===r)return 30;let a=30+(Math.round(n/255)<<2|Math.round(o/255)<<1|Math.round(i/255));return 2===r&&(a+=60),a},_a.hsv.ansi16=function(t){return _a.rgb.ansi16(_a.hsv.rgb(t),t[2])},_a.rgb.ansi256=function(t){const e=t[0],i=t[1],o=t[2];if(e===i&&i===o)return e<8?16:e>248?231:Math.round((e-8)/247*24)+232;return 16+36*Math.round(e/255*5)+6*Math.round(i/255*5)+Math.round(o/255*5)},_a.ansi16.rgb=function(t){let e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),e=e/10.5*255,[e,e,e];const i=.5*(1+~~(t>50));return[(1&e)*i*255,(e>>1&1)*i*255,(e>>2&1)*i*255]},_a.ansi256.rgb=function(t){if(t>=232){const e=10*(t-232)+8;return[e,e,e]}let e;t-=16;return[Math.floor(t/36)/5*255,Math.floor((e=t%36)/6)/5*255,e%6/5*255]},_a.rgb.hex=function(t){const e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},_a.hex.rgb=function(t){const e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];let i=e[0];3===e[0].length&&(i=i.split("").map((t=>t+t)).join(""));const o=parseInt(i,16);return[o>>16&255,o>>8&255,255&o]},_a.rgb.hcg=function(t){const e=t[0]/255,i=t[1]/255,o=t[2]/255,n=Math.max(Math.max(e,i),o),r=Math.min(Math.min(e,i),o),a=n-r;let l,s;return l=a<1?r/(1-a):0,s=a<=0?0:n===e?(i-o)/a%6:n===i?2+(o-e)/a:4+(e-i)/a,s/=6,s%=1,[360*s,100*a,100*l]},_a.hsl.hcg=function(t){const e=t[1]/100,i=t[2]/100,o=i<.5?2*e*i:2*e*(1-i);let n=0;return o<1&&(n=(i-.5*o)/(1-o)),[t[0],100*o,100*n]},_a.hsv.hcg=function(t){const e=t[1]/100,i=t[2]/100,o=e*i;let n=0;return o<1&&(n=(i-o)/(1-o)),[t[0],100*o,100*n]},_a.hcg.rgb=function(t){const e=t[0]/360,i=t[1]/100,o=t[2]/100;if(0===i)return[255*o,255*o,255*o];const n=[0,0,0],r=e%1*6,a=r%1,l=1-a;let s=0;switch(Math.floor(r)){case 0:n[0]=1,n[1]=a,n[2]=0;break;case 1:n[0]=l,n[1]=1,n[2]=0;break;case 2:n[0]=0,n[1]=1,n[2]=a;break;case 3:n[0]=0,n[1]=l,n[2]=1;break;case 4:n[0]=a,n[1]=0,n[2]=1;break;default:n[0]=1,n[1]=0,n[2]=l}return s=(1-i)*o,[255*(i*n[0]+s),255*(i*n[1]+s),255*(i*n[2]+s)]},_a.hcg.hsv=function(t){const e=t[1]/100,i=e+t[2]/100*(1-e);let o=0;return i>0&&(o=e/i),[t[0],100*o,100*i]},_a.hcg.hsl=function(t){const e=t[1]/100,i=t[2]/100*(1-e)+.5*e;let o=0;return i>0&&i<.5?o=e/(2*i):i>=.5&&i<1&&(o=e/(2*(1-i))),[t[0],100*o,100*i]},_a.hcg.hwb=function(t){const e=t[1]/100,i=e+t[2]/100*(1-e);return[t[0],100*(i-e),100*(1-i)]},_a.hwb.hcg=function(t){const e=t[1]/100,i=1-t[2]/100,o=i-e;let n=0;return o<1&&(n=(i-o)/(1-o)),[t[0],100*o,100*n]},_a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},_a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},_a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},_a.gray.hsl=function(t){return[0,0,t[0]]},_a.gray.hsv=_a.gray.hsl,_a.gray.hwb=function(t){return[0,100,t[0]]},_a.gray.cmyk=function(t){return[0,0,0,t[0]]},_a.gray.lab=function(t){return[t[0],0,0]},_a.gray.hex=function(t){const e=255&Math.round(t[0]/100*255),i=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(i.length)+i},_a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]};const ya=va;function xa(t){const e=function(){const t={},e=Object.keys(ya);for(let i=e.length,o=0;o{Ea[t]={},Object.defineProperty(Ea[t],"channels",{value:Ca[t].channels}),Object.defineProperty(Ea[t],"labels",{value:Ca[t].labels});const e=$a(t);Object.keys(e).forEach((i=>{const o=e[i];Ea[t][i]=function(t){const e=function(...e){const i=e[0];if(null==i)return i;i.length>1&&(e=i);const o=t(e);if("object"==typeof o)for(let t=o.length,e=0;e1&&(e=i),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(o)}))}));const Aa=Jr,Sa=Ea,Ia=["keyword","gray","hex"],Ta={};for(const t of Object.keys(Sa))Ta[[...Sa[t].labels].sort().join("")]=t;const za={};function Oa(t,e){if(!(this instanceof Oa))return new Oa(t,e);if(e&&e in Ia&&(e=null),e&&!(e in Sa))throw new Error("Unknown model: "+e);let i,o;if(null==t)this.model="rgb",this.color=[0,0,0],this.valpha=1;else if(t instanceof Oa)this.model=t.model,this.color=[...t.color],this.valpha=t.valpha;else if("string"==typeof t){const e=Aa.get(t);if(null===e)throw new Error("Unable to parse color from string: "+t);this.model=e.model,o=Sa[this.model].channels,this.color=e.value.slice(0,o),this.valpha="number"==typeof e.value[o]?e.value[o]:1}else if(t.length>0){this.model=e||"rgb",o=Sa[this.model].channels;const i=Array.prototype.slice.call(t,0,o);this.color=ja(i,o),this.valpha="number"==typeof t[o]?t[o]:1}else if("number"==typeof t)this.model="rgb",this.color=[t>>16&255,t>>8&255,255&t],this.valpha=1;else{this.valpha=1;const e=Object.keys(t);"alpha"in t&&(e.splice(e.indexOf("alpha"),1),this.valpha="number"==typeof t.alpha?t.alpha:0);const o=e.sort().join("");if(!(o in Ta))throw new Error("Unable to parse color from object: "+JSON.stringify(t));this.model=Ta[o];const{labels:n}=Sa[this.model],r=[];for(i=0;i(t%360+360)%360)),saturationl:Da("hsl",1,La(100)),lightness:Da("hsl",2,La(100)),saturationv:Da("hsv",1,La(100)),value:Da("hsv",2,La(100)),chroma:Da("hcg",1,La(100)),gray:Da("hcg",2,La(100)),white:Da("hwb",1,La(100)),wblack:Da("hwb",2,La(100)),cyan:Da("cmyk",0,La(100)),magenta:Da("cmyk",1,La(100)),yellow:Da("cmyk",2,La(100)),black:Da("cmyk",3,La(100)),x:Da("xyz",0,La(95.047)),y:Da("xyz",1,La(100)),z:Da("xyz",2,La(108.833)),l:Da("lab",0,La(100)),a:Da("lab",1),b:Da("lab",2),keyword(t){return void 0!==t?new Oa(t):Sa[this.model].keyword(this.color)},hex(t){return void 0!==t?new Oa(t):Aa.to.hex(this.rgb().round().color)},hexa(t){if(void 0!==t)return new Oa(t);const e=this.rgb().round().color;let i=Math.round(255*this.valpha).toString(16).toUpperCase();return 1===i.length&&(i="0"+i),Aa.to.hex(e)+i},rgbNumber(){const t=this.rgb().color;return(255&t[0])<<16|(255&t[1])<<8|255&t[2]},luminosity(){const t=this.rgb().color,e=[];for(const[i,o]of t.entries()){const t=o/255;e[i]=t<=.04045?t/12.92:((t+.055)/1.055)**2.4}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast(t){const e=this.luminosity(),i=t.luminosity();return e>i?(e+.05)/(i+.05):(i+.05)/(e+.05)},level(t){const e=this.contrast(t);return e>=7?"AAA":e>=4.5?"AA":""},isDark(){const t=this.rgb().color;return(2126*t[0]+7152*t[1]+722*t[2])/1e4<128},isLight(){return!this.isDark()},negate(){const t=this.rgb();for(let e=0;e<3;e++)t.color[e]=255-t.color[e];return t},lighten(t){const e=this.hsl();return e.color[2]+=e.color[2]*t,e},darken(t){const e=this.hsl();return e.color[2]-=e.color[2]*t,e},saturate(t){const e=this.hsl();return e.color[1]+=e.color[1]*t,e},desaturate(t){const e=this.hsl();return e.color[1]-=e.color[1]*t,e},whiten(t){const e=this.hwb();return e.color[1]+=e.color[1]*t,e},blacken(t){const e=this.hwb();return e.color[2]+=e.color[2]*t,e},grayscale(){const t=this.rgb().color,e=.3*t[0]+.59*t[1]+.11*t[2];return Oa.rgb(e,e,e)},fade(t){return this.alpha(this.valpha-this.valpha*t)},opaquer(t){return this.alpha(this.valpha+this.valpha*t)},rotate(t){const e=this.hsl();let i=e.color[0];return i=(i+t)%360,i=i<0?360+i:i,e.color[0]=i,e},mix(t,e){if(!t||!t.rgb)throw new Error('Argument to "mix" was not a Color instance, but rather an instance of '+typeof t);const i=t.rgb(),o=this.rgb(),n=void 0===e?.5:e,r=2*n-1,a=i.alpha()-o.alpha(),l=((r*a==-1?r:(r+a)/(1+r*a))+1)/2,s=1-l;return Oa.rgb(l*i.red()+s*o.red(),l*i.green()+s*o.green(),l*i.blue()+s*o.blue(),i.alpha()*n+o.alpha()*(1-n))}};for(const t of Object.keys(Sa)){if(Ia.includes(t))continue;const{channels:e}=Sa[t];Oa.prototype[t]=function(...e){return this.model===t?new Oa(this):e.length>0?new Oa(e,t):new Oa([...(i=Sa[this.model][t].raw(this.color),Array.isArray(i)?i:[i]),this.valpha],t);var i},Oa[t]=function(...i){let o=i[0];return"number"==typeof o&&(o=ja(i,e)),new Oa(o,t)}}function Ma(t){return function(e){return function(t,e){return Number(t.toFixed(e))}(e,t)}}function Da(t,e,i){t=Array.isArray(t)?t:[t];for(const o of t)(za[o]||(za[o]=[]))[e]=i;return t=t[0],function(o){let n;return void 0!==o?(i&&(o=i(o)),n=this[t](),n.color[e]=o,n):(n=this[t]().color[e],i&&(n=i(n)),n)}}function La(t){return function(e){return Math.max(0,Math.min(t,e))}}function ja(t,e){for(let i=0;i ${t("editor.form.color_picker.values.default")} - ${ya.map((t=>N` + ${Na.map((t=>B` ${function(t){return t.split("-").map((t=>function(t){return t.charAt(0).toUpperCase()+t.slice(1)}(t))).join(" ")}(t)} ${this.renderColorCircle(t)} `))} - `}renderColorCircle(t){return N` + `}renderColorCircle(t){return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-select { width: 100%; } @@ -691,32 +665,32 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl width: 20px; height: 20px; } - `}};n([st()],Ca.prototype,"label",void 0),n([st()],Ca.prototype,"value",void 0),n([st()],Ca.prototype,"configValue",void 0),n([st()],Ca.prototype,"hass",void 0),Ca=n([at("mushroom-color-picker")],Ca);let $a=class extends ot{render(){return N` + `}};n([ht()],Ba.prototype,"label",void 0),n([ht()],Ba.prototype,"value",void 0),n([ht()],Ba.prototype,"configValue",void 0),n([ht()],Ba.prototype,"hass",void 0),Ba=n([dt("mushroom-color-picker")],Ba);let Ua=class extends st{render(){return B` - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],$a.prototype,"hass",void 0),n([st()],$a.prototype,"selector",void 0),n([st()],$a.prototype,"value",void 0),n([st()],$a.prototype,"label",void 0),$a=n([at("ha-selector-mush-color")],$a);const Ea=["button","input_button","scene"],Aa=["name","state","last-changed","last-updated","none"],Sa=["icon","entity-picture","none"];function Ia(t,e,i,n,o){switch(t){case"name":return e;case"state":const t=n.entity_id.split(".")[0];return"timestamp"!==n.attributes.device_class&&!Ea.includes(t)||!Dt(n)||function(t){return t.state===zt}(n)?i:N` + `}_valueChanged(t){zt(this,"value-changed",{value:t.detail.value||void 0})}};n([ht()],Ua.prototype,"hass",void 0),n([ht()],Ua.prototype,"selector",void 0),n([ht()],Ua.prototype,"value",void 0),n([ht()],Ua.prototype,"label",void 0),Ua=n([dt("ha-selector-mush-color")],Ua);const Ha=["button","input_button","scene"],Ya=["name","state","last-changed","last-updated","none"],Xa=["icon","entity-picture","none"];function Wa(t,e,i,o,n){switch(t){case"name":return e;case"state":const t=o.entity_id.split(".")[0];return"timestamp"!==o.attributes.device_class&&!Ha.includes(t)||!Rt(o)||function(t){return t.state===Lt}(o)?i:B` - `;case"last-changed":return N` + `;case"last-changed":return B` - `;case"last-updated":return N` + `;case"last-updated":return B` - `;case"none":return}}function Ta(t,e){return"entity-picture"===e?Pt(t):void 0}let za=class extends ot{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=Hi(this.hass);return N` + `;case"none":return}}function qa(t,e){return"entity-picture"===e?Ft(t):void 0}let Ka=class extends st{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=ao(this.hass);return B` ${t("editor.form.icon_type_picker.values.default")} - ${Sa.map((e=>N` + ${Xa.map((e=>B` ${t(`editor.form.icon_type_picker.values.${e}`)||function(t){return t.charAt(0).toUpperCase()+t.slice(1)}(e)} `))} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-select { width: 100%; } - `}};n([st()],za.prototype,"label",void 0),n([st()],za.prototype,"value",void 0),n([st()],za.prototype,"configValue",void 0),n([st()],za.prototype,"hass",void 0),za=n([at("mushroom-icon-type-picker")],za);let Oa=class extends ot{render(){return N` + `}};n([ht()],Ka.prototype,"label",void 0),n([ht()],Ka.prototype,"value",void 0),n([ht()],Ka.prototype,"configValue",void 0),n([ht()],Ka.prototype,"hass",void 0),Ka=n([dt("mushroom-icon-type-picker")],Ka);let Ga=class extends st{render(){return B` - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],Oa.prototype,"hass",void 0),n([st()],Oa.prototype,"selector",void 0),n([st()],Oa.prototype,"value",void 0),n([st()],Oa.prototype,"label",void 0),Oa=n([at("ha-selector-mush-icon-type")],Oa);let Ma=class extends ot{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){var t;const e=Hi(this.hass);return N` + `}_valueChanged(t){zt(this,"value-changed",{value:t.detail.value||void 0})}};n([ht()],Ga.prototype,"hass",void 0),n([ht()],Ga.prototype,"selector",void 0),n([ht()],Ga.prototype,"value",void 0),n([ht()],Ga.prototype,"label",void 0),Ga=n([dt("ha-selector-mush-icon-type")],Ga);let Za=class extends st{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){var t;const e=ao(this.hass);return B` ${e("editor.form.info_picker.values.default")} - ${(null!==(t=this.infos)&&void 0!==t?t:Aa).map((t=>N` + ${(null!==(t=this.infos)&&void 0!==t?t:Ya).map((t=>B` ${e(`editor.form.info_picker.values.${t}`)||function(t){return t.charAt(0).toUpperCase()+t.slice(1)}(t)} `))} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-select { width: 100%; } - `}};n([st()],Ma.prototype,"label",void 0),n([st()],Ma.prototype,"value",void 0),n([st()],Ma.prototype,"configValue",void 0),n([st()],Ma.prototype,"infos",void 0),n([st()],Ma.prototype,"hass",void 0),Ma=n([at("mushroom-info-picker")],Ma);let La=class extends ot{render(){return N` + `}};n([ht()],Za.prototype,"label",void 0),n([ht()],Za.prototype,"value",void 0),n([ht()],Za.prototype,"configValue",void 0),n([ht()],Za.prototype,"infos",void 0),n([ht()],Za.prototype,"hass",void 0),Za=n([dt("mushroom-info-picker")],Za);let Ja=class extends st{render(){return B` - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],La.prototype,"hass",void 0),n([st()],La.prototype,"selector",void 0),n([st()],La.prototype,"value",void 0),n([st()],La.prototype,"label",void 0),La=n([at("ha-selector-mush-info")],La);const Da=["default","horizontal","vertical"],ja={default:"mdi:card-text-outline",vertical:"mdi:focus-field-vertical",horizontal:"mdi:focus-field-horizontal"};let Pa=class extends ot{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=Hi(this.hass),e=this.value||"default";return N` + `}_valueChanged(t){zt(this,"value-changed",{value:t.detail.value||void 0})}};n([ht()],Ja.prototype,"hass",void 0),n([ht()],Ja.prototype,"selector",void 0),n([ht()],Ja.prototype,"value",void 0),n([ht()],Ja.prototype,"label",void 0),Ja=n([dt("ha-selector-mush-info")],Ja);const Qa=["default","horizontal","vertical"],tl={default:"mdi:card-text-outline",vertical:"mdi:focus-field-vertical",horizontal:"mdi:focus-field-horizontal"};let el=class extends st{constructor(){super(...arguments),this.label="",this.configValue=""}_selectChanged(t){const e=t.target.value;e&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:"default"!==e?e:""}}))}render(){const t=ao(this.hass),e=this.value||"default";return B` - - ${Da.map((e=>N` + + ${Qa.map((e=>B` ${t(`editor.form.layout_picker.values.${e}`)} - + `))} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-select { width: 100%; } - `}};n([st()],Pa.prototype,"label",void 0),n([st()],Pa.prototype,"value",void 0),n([st()],Pa.prototype,"configValue",void 0),n([st()],Pa.prototype,"hass",void 0),Pa=n([at("mushroom-layout-picker")],Pa);let Na=class extends ot{render(){return N` + `}};n([ht()],el.prototype,"label",void 0),n([ht()],el.prototype,"value",void 0),n([ht()],el.prototype,"configValue",void 0),n([ht()],el.prototype,"hass",void 0),el=n([dt("mushroom-layout-picker")],el);let il=class extends st{render(){return B` - `}_valueChanged(t){At(this,"value-changed",{value:t.detail.value||void 0})}};n([st()],Na.prototype,"hass",void 0),n([st()],Na.prototype,"selector",void 0),n([st()],Na.prototype,"value",void 0),n([st()],Na.prototype,"label",void 0),Na=n([at("ha-selector-mush-layout")],Na);let Va=class extends ot{constructor(){super(...arguments),this.icon=""}render(){return N` + `}_valueChanged(t){zt(this,"value-changed",{value:t.detail.value||void 0})}};n([ht()],il.prototype,"hass",void 0),n([ht()],il.prototype,"selector",void 0),n([ht()],il.prototype,"value",void 0),n([ht()],il.prototype,"label",void 0),il=n([dt("ha-selector-mush-layout")],il);let ol=class extends st{constructor(){super(...arguments),this.icon=""}render(){return B`
- `}static get styles(){return d` + `}static get styles(){return h` :host { --main-color: rgb(var(--rgb-grey)); --icon-color: rgb(var(--rgb-white)); @@ -832,11 +806,11 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --mdc-icon-size: var(--badge-icon-size); color: var(--icon-color); } - `}};n([st()],Va.prototype,"icon",void 0),Va=n([at("mushroom-badge-icon")],Va);let Ra=class extends ot{constructor(){super(...arguments),this.icon="",this.title="",this.disabled=!1}render(){return N` + `}};n([ht()],ol.prototype,"icon",void 0),ol=n([dt("mushroom-badge-icon")],ol);let nl=class extends st{constructor(){super(...arguments),this.icon="",this.title="",this.disabled=!1}render(){return B` - `}static get styles(){return d` + `}static get styles(){return h` :host { --icon-color: var(--primary-text-color); --icon-color-disabled: rgb(var(--rgb-disabled)); @@ -875,13 +849,13 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .button:disabled ha-icon { color: var(--icon-color-disabled); } - `}};n([st()],Ra.prototype,"icon",void 0),n([st()],Ra.prototype,"title",void 0),n([st({type:Boolean})],Ra.prototype,"disabled",void 0),Ra=n([at("mushroom-button")],Ra);let Fa=class extends ot{constructor(){super(...arguments),this.fill=!1,this.rtl=!1}render(){return N` + `}};n([ht()],nl.prototype,"icon",void 0),n([ht()],nl.prototype,"title",void 0),n([ht({type:Boolean})],nl.prototype,"disabled",void 0),nl=n([dt("mushroom-button")],nl);let rl=class extends st{constructor(){super(...arguments),this.fill=!1,this.rtl=!1}render(){return B`
- `}static get styles(){return d` + `}static get styles(){return h` :host { display: flex; flex-direction: row; @@ -900,17 +874,29 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl margin-right: initial; margin-left: var(--spacing); } - .container.fill > ::slotted(*) { - flex: 1; + .container > ::slotted(mushroom-button) { width: 0; + flex-grow: 0; + flex-shrink: 1; + flex-basis: calc(var(--control-height) * var(--control-button-ratio)); + } + .container > ::slotted(mushroom-input-number) { + width: 0; + flex-grow: 0; + flex-shrink: 1; + flex-basis: calc(var(--control-height) * var(--control-button-ratio) * 3); } - `}};n([st()],Fa.prototype,"fill",void 0),n([st()],Fa.prototype,"rtl",void 0),Fa=n([at("mushroom-button-group")],Fa);let Ba=class extends ot{render(){var t,e,i,n;return N` + .container.fill > ::slotted(mushroom-button), + .container.fill > ::slotted(mushroom-input-number) { + flex-grow: 1; + } + `}};n([ht()],rl.prototype,"fill",void 0),n([ht()],rl.prototype,"rtl",void 0),rl=n([dt("mushroom-button-group")],rl);let al=class extends st{render(){var t,e,i,o;return B`
- `}static get styles(){return d` + `}static get styles(){return h` .container { display: flex; flex-direction: column; @@ -946,23 +932,23 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl margin-left: var(--spacing); margin-bottom: 0; } - `}};n([st()],Ba.prototype,"appearance",void 0),Ba=n([at("mushroom-card")],Ba);const Ua={pulse:"@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n }",spin:"@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }",cleaning:"@keyframes cleaning {\n 0% {\n transform: rotate(0) translate(0);\n }\n 5% {\n transform: rotate(0) translate(0, -3px);\n }\n 10% {\n transform: rotate(0) translate(0, 1px);\n }\n 15% {\n transform: rotate(0) translate(0);\n }\n\n 20% {\n transform: rotate(30deg) translate(0);\n }\n 25% {\n transform: rotate(30deg) translate(0, -3px);\n }\n 30% {\n transform: rotate(30deg) translate(0, 1px);\n }\n 35% {\n transform: rotate(30deg) translate(0);\n }\n 40% {\n transform: rotate(0) translate(0);\n }\n\n 45% {\n transform: rotate(-30deg) translate(0);\n }\n 50% {\n transform: rotate(-30deg) translate(0, -3px);\n }\n 55% {\n transform: rotate(-30deg) translate(0, 1px);\n }\n 60% {\n transform: rotate(-30deg) translate(0);\n }\n 70% {\n transform: rotate(0deg) translate(0);\n }\n 100% {\n transform: rotate(0deg);\n }\n }",returning:"@keyframes returning {\n 0% {\n transform: rotate(0);\n }\n 25% {\n transform: rotate(20deg);\n }\n 50% {\n transform: rotate(0);\n }\n 75% {\n transform: rotate(-20deg);\n }\n 100% {\n transform: rotate(0);\n }\n }"},Ha=d` - ${c(Ua.pulse)} - `,Ya=(d` - ${c(Ua.spin)} - `,d` - ${c(Ua.cleaning)} - `,d` - ${c(Ua.returning)} - `,d` - ${c(Object.values(Ua).join("\n"))} -`);let Xa=class extends ot{constructor(){super(...arguments),this.icon="",this.disabled=!1}render(){return N` + `}};n([ht()],al.prototype,"appearance",void 0),al=n([dt("mushroom-card")],al);const ll={pulse:"@keyframes pulse {\n 0% {\n opacity: 1;\n }\n 50% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n }",spin:"@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }",cleaning:"@keyframes cleaning {\n 0% {\n transform: rotate(0) translate(0);\n }\n 5% {\n transform: rotate(0) translate(0, -3px);\n }\n 10% {\n transform: rotate(0) translate(0, 1px);\n }\n 15% {\n transform: rotate(0) translate(0);\n }\n\n 20% {\n transform: rotate(30deg) translate(0);\n }\n 25% {\n transform: rotate(30deg) translate(0, -3px);\n }\n 30% {\n transform: rotate(30deg) translate(0, 1px);\n }\n 35% {\n transform: rotate(30deg) translate(0);\n }\n 40% {\n transform: rotate(0) translate(0);\n }\n\n 45% {\n transform: rotate(-30deg) translate(0);\n }\n 50% {\n transform: rotate(-30deg) translate(0, -3px);\n }\n 55% {\n transform: rotate(-30deg) translate(0, 1px);\n }\n 60% {\n transform: rotate(-30deg) translate(0);\n }\n 70% {\n transform: rotate(0deg) translate(0);\n }\n 100% {\n transform: rotate(0deg);\n }\n }",returning:"@keyframes returning {\n 0% {\n transform: rotate(0);\n }\n 25% {\n transform: rotate(20deg);\n }\n 50% {\n transform: rotate(0);\n }\n 75% {\n transform: rotate(-20deg);\n }\n 100% {\n transform: rotate(0);\n }\n }"},sl=h` + ${u(ll.pulse)} + `,cl=(h` + ${u(ll.spin)} + `,h` + ${u(ll.cleaning)} + `,h` + ${u(ll.returning)} + `,h` + ${u(Object.values(ll).join("\n"))} +`);let dl=class extends st{constructor(){super(...arguments),this.icon="",this.disabled=!1}render(){return B`
- `}static get styles(){return d` + `}static get styles(){return h` :host { --icon-color: var(--primary-text-color); --icon-color-disabled: rgb(var(--rgb-disabled)); @@ -1002,16 +988,16 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .shape.disabled ha-icon { color: var(--icon-color-disabled); } - ${Ya} - `}};n([st()],Xa.prototype,"icon",void 0),n([st()],Xa.prototype,"disabled",void 0),Xa=n([at("mushroom-shape-icon")],Xa);let Wa=class extends ot{constructor(){super(...arguments),this.primary="",this.multiline_secondary=!1}render(){return N` + ${cl} + `}};n([ht()],dl.prototype,"icon",void 0),n([ht()],dl.prototype,"disabled",void 0),dl=n([dt("mushroom-shape-icon")],dl);let ul=class extends st{constructor(){super(...arguments),this.primary="",this.multiline_secondary=!1}render(){return B`
${this.primary} - ${this.secondary?N`${this.secondary}`:null}
- `}static get styles(){return d` + `}static get styles(){return h` .container { min-width: 0; flex: 1; @@ -1039,23 +1025,23 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .multiline_secondary { white-space: pre-wrap; } - `}};n([st()],Wa.prototype,"primary",void 0),n([st()],Wa.prototype,"secondary",void 0),n([st()],Wa.prototype,"multiline_secondary",void 0),Wa=n([at("mushroom-state-info")],Wa);let qa=class extends ot{render(){var t,e,i,n;return N` + `}};n([ht()],ul.prototype,"primary",void 0),n([ht()],ul.prototype,"secondary",void 0),n([ht()],ul.prototype,"multiline_secondary",void 0),ul=n([dt("mushroom-state-info")],ul);let hl=class extends st{render(){var t,e,i,o;return B`
- ${"none"!==(null===(e=this.appearance)||void 0===e?void 0:e.icon_type)?N` + ${"none"!==(null===(e=this.appearance)||void 0===e?void 0:e.icon_type)?B`
`:null} - ${"none"!==(null===(i=this.appearance)||void 0===i?void 0:i.primary_info)||"none"!==(null===(n=this.appearance)||void 0===n?void 0:n.secondary_info)?N` + ${"none"!==(null===(i=this.appearance)||void 0===i?void 0:i.primary_info)||"none"!==(null===(o=this.appearance)||void 0===o?void 0:o.secondary_info)?B`
`:null}
- `}static get styles(){return d` + `}static get styles(){return h` .container { display: flex; flex-direction: row; @@ -1102,11 +1088,11 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .container.vertical .info { text-align: center; } - `}};function Ka(t){var e,i,n,o,r;return{layout:null!==(e=t.layout)&&void 0!==e?e:Ga(t),fill_container:null!==(i=t.fill_container)&&void 0!==i&&i,primary_info:null!==(n=t.primary_info)&&void 0!==n?n:Ja(t),secondary_info:null!==(o=t.secondary_info)&&void 0!==o?o:Qa(t),icon_type:null!==(r=t.icon_type)&&void 0!==r?r:Za(t)}}function Ga(t){return t.vertical?"vertical":"default"}function Za(t){return t.hide_icon?"none":t.use_entity_picture||t.use_media_artwork?"entity-picture":"icon"}function Ja(t){return t.hide_name?"none":"name"}function Qa(t){return t.hide_state?"none":"state"}n([st()],qa.prototype,"appearance",void 0),qa=n([at("mushroom-state-item")],qa);let tl=class extends ot{constructor(){super(...arguments),this.picture_url=""}render(){return N` -
+ `}};function ml(t){var e,i,o,n,r;return{layout:null!==(e=t.layout)&&void 0!==e?e:pl(t),fill_container:null!==(i=t.fill_container)&&void 0!==i&&i,primary_info:null!==(o=t.primary_info)&&void 0!==o?o:gl(t),secondary_info:null!==(n=t.secondary_info)&&void 0!==n?n:_l(t),icon_type:null!==(r=t.icon_type)&&void 0!==r?r:fl(t)}}function pl(t){return t.vertical?"vertical":"default"}function fl(t){return t.hide_icon?"none":t.use_entity_picture||t.use_media_artwork?"entity-picture":"icon"}function gl(t){return t.hide_name?"none":"name"}function _l(t){return t.hide_state?"none":"state"}n([ht()],hl.prototype,"appearance",void 0),hl=n([dt("mushroom-state-item")],hl);let vl=class extends st{constructor(){super(...arguments),this.picture_url=""}render(){return B` +
- `}static get styles(){return d` + `}static get styles(){return h` :host { --main-color: var(--primary-text-color); --icon-color-disabled: rgb(var(--rgb-disabled)); @@ -1128,7 +1114,7 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl height: 100%; border-radius: var(--icon-border-radius); } - `}};n([st()],tl.prototype,"picture_url",void 0),tl=n([at("mushroom-shape-avatar")],tl);const el=d` + `}};n([ht()],vl.prototype,"picture_url",void 0),vl=n([dt("mushroom-shape-avatar")],vl);const bl=h` --spacing: var(--mush-spacing, 12px); /* Title */ @@ -1193,7 +1179,7 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --icon-border-radius: var(--mush-icon-border-radius, 50%); --icon-size: var(--mush-icon-size, 42px); --icon-symbol-size: var(--mush-icon-symbol-size, 0.5em); -`,il=d` +`,yl=h` /* RGB */ /* Standard colors */ --rgb-red: var(--mush-rgb-red, var(--default-red)); @@ -1232,6 +1218,7 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --rgb-state-entity: var(--mush-rgb-state-entity, var(--rgb-blue)); --rgb-state-media-player: var(--mush-rgb-state-media-player, var(--rgb-indigo)); --rgb-state-lock: var(--mush-rgb-state-lock, var(--rgb-blue)); + --rgb-state-number: var(--mush-rgb-state-number, var(--rgb-blue)); --rgb-state-humidifier: var(--mush-rgb-state-humidifier, var(--rgb-purple)); /* State alarm colors */ @@ -1268,41 +1255,41 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --rgb-state-climate-heat-cool: var(--mush-rgb-state-climate-heat-cool, var(--rgb-green)); --rgb-state-climate-idle: var(--mush-rgb-state-climate-idle, var(--rgb-disabled)); --rgb-state-climate-off: var(--mush-rgb-state-climate-off, var(--rgb-disabled)); -`;function nl(t){return!!t&&t.themes.darkMode}class ol extends ot{updated(t){if(super.updated(t),t.has("hass")&&this.hass){const e=nl(t.get("hass")),i=nl(this.hass);e!==i&&this.toggleAttribute("dark-mode",i)}}static get styles(){return d` +`;function xl(t){return!!t&&t.themes.darkMode}class wl extends st{updated(t){if(super.updated(t),t.has("hass")&&this.hass){const e=xl(t.get("hass")),i=xl(this.hass);e!==i&&this.toggleAttribute("dark-mode",i)}}static get styles(){return h` :host { - ${wa} + ${Va} } :host([dark-mode]) { - ${ka} + ${Fa} } :host { - ${il} - ${el} + ${yl} + ${bl} } - `}}n([st({attribute:!1})],ol.prototype,"hass",void 0);class rl extends ol{renderPicture(t){return N` + `}}n([ht({attribute:!1})],wl.prototype,"hass",void 0);class kl extends wl{renderPicture(t){return B` - `}renderIcon(t,e){const i=Lt(t);return N` + `}renderIcon(t,e){const i=Nt(t);return B` - `}renderBadge(t){return!Dt(t)?N` + `}renderBadge(t){return!Rt(t)?B` - `:null}renderStateInfo(t,e,i,n){const o=Ht(this.hass.localize,t,this.hass.locale),r=null!=n?n:o,a=Ia(e.primary_info,i,r,t,this.hass),l=Ia(e.secondary_info,i,r,t,this.hass);return N` + `:null}renderStateInfo(t,e,i,o){const n=ee(this.hass.localize,t,this.hass.locale,this.hass.entities,this.hass.connection.haVersion),r=null!=o?o:n,a=Wa(e.primary_info,i,r,t,this.hass),l=Wa(e.secondary_info,i,r,t,this.hass);return B` - `}}const al=d` + `}}const Cl=h` ha-card { box-sizing: border-box; padding: var(--spacing); @@ -1338,25 +1325,25 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .unavailable { --main-color: var(--warning-color); } -`;function ll(t){const e=window;e.customCards=e.customCards||[],e.customCards.push(Object.assign(Object.assign({},t),{preview:!0}))}const sl={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"},cl={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"},dl={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"},ul=(t,e)=>{const i=Number(t);if(isNaN(i))return"off"===t?"mdi:battery":"on"===t?"mdi:battery-alert":"mdi:battery-unknown";const n=10*Math.round(i/10);return e&&i>=10?dl[n]:e?"mdi:battery-charging-outline":i<=5?"mdi:battery-alert-variant-outline":cl[n]},hl=t=>{const e=null==t?void 0:t.attributes.device_class;if(e&&e in sl)return sl[e];if("battery"===e)return t?((t,e)=>{const i=t.state,n="on"===(null==e?void 0:e.state);return ul(i,n)})(t):"mdi:battery";const i=null==t?void 0:t.attributes.unit_of_measurement;return"°C"===i||"°F"===i?"mdi:thermometer":void 0},ml={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",zone:"mdi:map-marker-radius"};function pl(t){if(t.attributes.icon)return t.attributes.icon;return function(t,e,i){switch(t){case"alarm_control_panel":return(t=>{switch(t){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"}})(i);case"binary_sensor":return((t,e)=>{const i="off"===t;switch(null==e?void 0:e.attributes.device_class){case"battery":return i?"mdi:battery":"mdi:battery-outline";case"battery_charging":return i?"mdi:battery":"mdi:battery-charging";case"cold":return i?"mdi:thermometer":"mdi:snowflake";case"connectivity":return i?"mdi:close-network-outline":"mdi:check-network-outline";case"door":return i?"mdi:door-closed":"mdi:door-open";case"garage_door":return i?"mdi:garage":"mdi:garage-open";case"power":case"plug":return i?"mdi:power-plug-off":"mdi:power-plug";case"gas":case"problem":case"safety":case"tamper":return i?"mdi:check-circle":"mdi:alert-circle";case"smoke":return i?"mdi:check-circle":"mdi:smoke";case"heat":return i?"mdi:thermometer":"mdi:fire";case"light":return i?"mdi:brightness5":"mdi:brightness-7";case"lock":return i?"mdi:lock":"mdi:lock-open";case"moisture":return i?"mdi:water-off":"mdi:water";case"motion":return i?"mdi:motion-sensor-off":"mdi:motion-sensor";case"occupancy":case"presence":return i?"mdi:home-outline":"mdi:home";case"opening":return i?"mdi:square":"mdi:square-outline";case"running":return i?"mdi:stop":"mdi:play";case"sound":return i?"mdi:music-note-off":"mdi:music-note";case"update":return i?"mdi:package":"mdi:package-up";case"vibration":return i?"mdi:crop-portrait":"mdi:vibrate";case"window":return i?"mdi:window-closed":"mdi:window-open";default:return i?"mdi:radiobox-blank":"mdi:checkbox-marked-circle"}})(i,e);case"button":switch(null==e?void 0:e.attributes.device_class){case"restart":return"mdi:restart";case"update":return"mdi:package-up";default:return"mdi:gesture-tap-button"}case"cover":return((t,e)=>{const i="closed"!==t;switch(null==e?void 0:e.attributes.device_class){case"garage":switch(t){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(t){case"opening":case"closing":return"mdi:gate-arrow-right";case"closed":return"mdi:gate";default:return"mdi:gate-open"}case"door":return i?"mdi:door-open":"mdi:door-closed";case"damper":return i?"md:circle":"mdi:circle-slice-8";case"shutter":switch(t){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(t){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(t){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(t){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(t){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"}})(i,e);case"device_tracker":return"router"===(null==e?void 0:e.attributes.source_type)?"home"===i?"mdi:lan-connect":"mdi:lan-disconnect":["bluetooth","bluetooth_le"].includes(null==e?void 0:e.attributes.source_type)?"home"===i?"mdi:bluetooth-connect":"mdi:bluetooth":"not_home"===i?"mdi:account-arrow-right":"mdi:account";case"humidifier":return i&&"off"===i?"mdi:air-humidifier-off":"mdi:air-humidifier";case"input_boolean":return"on"===i?"mdi:check-circle-outline":"mdi:close-circle-outline";case"lock":switch(i){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":return"playing"===i?"mdi:cast-connected":"mdi:cast";case"switch":switch(null==e?void 0:e.attributes.device_class){case"outlet":return"on"===i?"mdi:power-plug":"mdi:power-plug-off";case"switch":return"on"===i?"mdi:toggle-switch":"mdi:toggle-switch-off";default:return"mdi:flash"}case"weather":switch(i){case"clear-night":return"mdi:weather-night";case"cloudy":default:return"mdi:weather-cloudy";case"exceptional":return"mdi:alert-circle-outline";case"fog":return"mdi:weather-fog";case"hail":return"mdi:weather-hail";case"lightning":return"mdi:weather-lightning";case"lightning-rainy":return"mdi:weather-lightning-rainy";case"partlycloudy":return"mdi:weather-partly-cloudy";case"pouring":return"mdi:weather-pouring";case"rainy":return"mdi:weather-rainy";case"snowy":return"mdi:weather-snowy";case"snowy-rainy":return"mdi:weather-snowy-rainy";case"sunny":return"mdi:weather-sunny";case"windy":return"mdi:weather-windy";case"windy-variant":return"mdi:weather-windy-variant"}case"zwave":switch(i){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 t=hl(e);if(t)return t;break}case"input_datetime":if(!(null==e?void 0:e.attributes.has_date))return"mdi:clock";if(!e.attributes.has_time)return"mdi:calendar";break;case"sun":return"above_horizon"===(null==e?void 0:e.state)?ml[t]:"mdi:weather-night";case"update":return"on"===(null==e?void 0:e.state)?Vt(e)?"mdi:package-down":"mdi:package-up":"mdi:package"}return t in ml?ml[t]:(console.warn(`Unable to find icon for domain ${t}`),"mdi:bookmark")}(It(t.entity_id),t,t.state)}const fl=["alarm_control_panel"],gl={disarmed:"var(--rgb-state-alarm-disarmed)",armed:"var(--rgb-state-alarm-armed)",triggered:"var(--rgb-state-alarm-triggered)",unavailable:"var(--rgb-warning)"},_l={disarmed:"alarm_disarm",armed_away:"alarm_arm_away",armed_home:"alarm_arm_home",armed_night:"alarm_arm_night",armed_vacation:"alarm_arm_vacation",armed_custom_bypass:"alarm_arm_custom_bypass"};function vl(t){var e;return null!==(e=gl[t.split("_")[0]])&&void 0!==e?e:"var(--rgb-grey)"}function bl(t){return["arming","triggered","pending",Tt].indexOf(t)>=0}function yl(t){return t.attributes.code_format&&"no_code"!==t.attributes.code_format}ll({type:"mushroom-alarm-control-panel-card",name:"Mushroom Alarm Control Panel Card",description:"Card for alarm control panel"});const xl=["1","2","3","4","5","6","7","8","9","","0","clear"];let wl=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Sc})),document.createElement("mushroom-alarm-control-panel-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>fl.includes(t.split(".")[0])));return{type:"custom:mushroom-alarm-control-panel-card",entity:e[0],states:["armed_home","armed_away"]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t),this.loadComponents()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.loadComponents()}async loadComponents(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity;yl(this.hass.states[t])&&Promise.resolve().then((function(){return Yc}))}_onTap(t,e){var i,n;const o=function(t){return _l[t]}(e);if(!o)return;t.stopPropagation();const r=(null===(i=this._input)||void 0===i?void 0:i.value)||void 0;this.hass.callService("alarm_control_panel",o,{entity_id:null===(n=this._config)||void 0===n?void 0:n.entity,code:r}),this._input&&(this._input.value="")}_handlePadClick(t){const e=t.currentTarget.value;this._input&&(this._input.value="clear"===e?"":this._input.value+e)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}get _hasCode(){var t,e,i;const n=null===(t=this._config)||void 0===t?void 0:t.entity;if(n){return yl(this.hass.states[n])&&null!==(i=null===(e=this._config)||void 0===e?void 0:e.show_keypad)&&void 0!==i&&i}return!1}render(){if(!this.hass||!this._config||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type),a=this._config.states&&this._config.states.length>0?function(t){return"disarmed"===t.state}(e)?this._config.states.map((t=>({state:t}))):[{state:"disarmed"}]:[],l=function(t){return Tt!==t.state}(e),s=pe(this.hass);return N` - - +`;function $l(e){const i=window;i.customCards=i.customCards||[];const o=e.type.replace("-card","").replace("mushroom-","");i.customCards.push(Object.assign(Object.assign({},e),{preview:!0,documentationURL:`${t}/blob/main/docs/cards/${o}.md`}))}const El={apparent_power:"mdi:flash",aqi:"mdi:air-filter",atmospheric_pressure:"mdi:thermometer-lines",carbon_dioxide:"mdi:molecule-co2",carbon_monoxide:"mdi:molecule-co",current:"mdi:current-ac",data_rate:"mdi:transmission-tower",data_size:"mdi:database",date:"mdi:calendar",distance:"mdi:arrow-left-right",duration:"mdi:progress-clock",energy:"mdi:lightning-bolt",frequency:"mdi:sine-wave",gas:"mdi:meter-gas",humidity:"mdi:water-percent",illuminance:"mdi:brightness-5",irradiance:"mdi:sun-wireless",moisture:"mdi:water-percent",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",precipitation:"mdi:weather-rainy",precipitation_intensity:"mdi:weather-pouring",pressure:"mdi:gauge",reactive_power:"mdi:flash",signal_strength:"mdi:wifi",sound_pressure:"mdi:ear-hearing",speed:"mdi:speedometer",sulphur_dioxide:"mdi:molecule",temperature:"mdi:thermometer",timestamp:"mdi:clock",volatile_organic_compounds:"mdi:molecule",voltage:"mdi:sine-wave",volume:"mdi:car-coolant-level",water:"mdi:water",weight:"mdi:weight",wind_speed:"mdi:weather-windy"},Al={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"},Sl={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"},Il=(t,e)=>{const i=Number(t);if(isNaN(i))return"off"===t?"mdi:battery":"on"===t?"mdi:battery-alert":"mdi:battery-unknown";const o=10*Math.round(i/10);return e&&i>=10?Sl[o]:e?"mdi:battery-charging-outline":i<=5?"mdi:battery-alert-variant-outline":Al[o]},Tl=t=>{const e=null==t?void 0:t.attributes.device_class;if(e&&e in El)return El[e];if("battery"===e)return t?((t,e)=>{const i=t.state,o="on"===(null==e?void 0:e.state);return Il(i,o)})(t):"mdi:battery";const i=null==t?void 0:t.attributes.unit_of_measurement;return"°C"===i||"°F"===i?"mdi:thermometer":void 0},zl={"clear-night":"mdi:weather-night",cloudy:"mdi:weather-cloudy",exceptional:"mdi:alert-circle-outline",fog:"mdi:weather-fog",hail:"mdi:weather-hail",lightning:"mdi:weather-lightning","lightning-rainy":"mdi:weather-lightning-rainy",partlycloudy:"mdi:weather-partly-cloudy",pouring:"mdi:weather-pouring",rainy:"mdi:weather-rainy",snowy:"mdi:weather-snowy","snowy-rainy":"mdi:weather-snowy-rainy",sunny:"mdi:weather-sunny",windy:"mdi:weather-windy","windy-variant":"mdi:weather-windy-variant"},Ol={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",zone:"mdi:map-marker-radius"};function Ml(t,e,i){const o=void 0!==i?i:null==e?void 0:e.state;switch(t){case"alarm_control_panel":return(t=>{switch(t){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"}})(o);case"binary_sensor":return((t,e)=>{const i="off"===t;switch(null==e?void 0:e.attributes.device_class){case"battery":return i?"mdi:battery":"mdi:battery-outline";case"battery_charging":return i?"mdi:battery":"mdi:battery-charging";case"carbon_monoxide":return i?"mdi:smoke-detector":"mdi:smoke-detector-alert";case"cold":return i?"mdi:thermometer":"mdi:snowflake";case"connectivity":return i?"mdi:close-network-outline":"mdi:check-network-outline";case"door":return i?"mdi:door-closed":"mdi:door-open";case"garage_door":return i?"mdi:garage":"mdi:garage-open";case"power":case"plug":return i?"mdi:power-plug-off":"mdi:power-plug";case"gas":case"problem":case"safety":case"tamper":return i?"mdi:check-circle":"mdi:alert-circle";case"smoke":return i?"mdi:smoke-detector-variant":"mdi:smoke-detector-variant-alert";case"heat":return i?"mdi:thermometer":"mdi:fire";case"light":return i?"mdi:brightness-5":"mdi:brightness-7";case"lock":return i?"mdi:lock":"mdi:lock-open";case"moisture":return i?"mdi:water-off":"mdi:water";case"motion":return i?"mdi:motion-sensor-off":"mdi:motion-sensor";case"occupancy":case"presence":return i?"mdi:home-outline":"mdi:home";case"opening":return i?"mdi:square":"mdi:square-outline";case"running":return i?"mdi:stop":"mdi:play";case"sound":return i?"mdi:music-note-off":"mdi:music-note";case"update":return i?"mdi:package":"mdi:package-up";case"vibration":return i?"mdi:crop-portrait":"mdi:vibrate";case"window":return i?"mdi:window-closed":"mdi:window-open";default:return i?"mdi:radiobox-blank":"mdi:checkbox-marked-circle"}})(o,e);case"button":switch(null==e?void 0:e.attributes.device_class){case"restart":return"mdi:restart";case"update":return"mdi:package-up";default:return"mdi:gesture-tap-button"}case"cover":return((t,e)=>{const i="closed"!==t;switch(null==e?void 0:e.attributes.device_class){case"garage":switch(t){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(t){case"opening":case"closing":return"mdi:gate-arrow-right";case"closed":return"mdi:gate";default:return"mdi:gate-open"}case"door":return i?"mdi:door-open":"mdi:door-closed";case"damper":return i?"mdi:circle":"mdi:circle-slice-8";case"shutter":switch(t){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(t){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":switch(t){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:blinds-horizontal-closed";default:return"mdi:blinds-horizontal"}case"shade":switch(t){case"opening":return"mdi:arrow-up-box";case"closing":return"mdi:arrow-down-box";case"closed":return"mdi:roller-shade-closed";default:return"mdi:roller-shade"}case"window":switch(t){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(t){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"}})(o,e);case"device_tracker":return"router"===(null==e?void 0:e.attributes.source_type)?"home"===o?"mdi:lan-connect":"mdi:lan-disconnect":["bluetooth","bluetooth_le"].includes(null==e?void 0:e.attributes.source_type)?"home"===o?"mdi:bluetooth-connect":"mdi:bluetooth":"not_home"===o?"mdi:account-arrow-right":"mdi:account";case"humidifier":return o&&"off"===o?"mdi:air-humidifier-off":"mdi:air-humidifier";case"input_boolean":return"on"===o?"mdi:check-circle-outline":"mdi:close-circle-outline";case"input_datetime":if(!(null==e?void 0:e.attributes.has_date))return"mdi:clock";if(!e.attributes.has_time)return"mdi:calendar";break;case"lock":switch(o){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(null==e?void 0:e.attributes.device_class){case"speaker":switch(o){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(o){case"playing":return"mdi:television-play";case"paused":return"mdi:television-pause";case"off":return"mdi:television-off";default:return"mdi:television"}case"receiver":return"off"===o?"mdi:audio-video-off":"mdi:audio-video";default:switch(o){case"playing":case"paused":return"mdi:cast-connected";case"off":return"mdi:cast-off";default:return"mdi:cast"}}case"person":return"not_home"===o?"mdi:account-arrow-right":"mdi:account";case"switch":switch(null==e?void 0:e.attributes.device_class){case"outlet":return"on"===o?"mdi:power-plug":"mdi:power-plug-off";case"switch":return"on"===o?"mdi:toggle-switch-variant":"mdi:toggle-switch-variant-off";default:return"mdi:toggle-switch-variant"}case"sensor":{const t=Tl(e);if(t)return t;break}case"sun":return"above_horizon"===(null==e?void 0:e.state)?Ol[t]:"mdi:weather-night";case"switch_as_x":return"mdi:swap-horizontal";case"threshold":return"mdi:chart-sankey";case"update":return"on"===(null==e?void 0:e.state)?Yt(e)?"mdi:package-down":"mdi:package-up":"mdi:package";case"weather":return((t,e)=>t?e&&"partlycloudy"===t?"mdiWeatherNightPartlyCloudy":zl[t]:void 0)(null==e?void 0:e.state)}return t in Ol?Ol[t]:"mdi:bookmark"}function Dl(t){if(t.attributes.icon)return t.attributes.icon;return Ml(Mt(t.entity_id),t,t.state)}const Ll=["alarm_control_panel"],jl={disarmed:"var(--rgb-state-alarm-disarmed)",armed:"var(--rgb-state-alarm-armed)",triggered:"var(--rgb-state-alarm-triggered)",unavailable:"var(--rgb-warning)"},Pl={disarmed:"alarm_disarm",armed_away:"alarm_arm_away",armed_home:"alarm_arm_home",armed_night:"alarm_arm_night",armed_vacation:"alarm_arm_vacation",armed_custom_bypass:"alarm_arm_custom_bypass"};function Nl(t){var e;return null!==(e=jl[t.split("_")[0]])&&void 0!==e?e:"var(--rgb-grey)"}function Rl(t){return["arming","triggered","pending",Dt].indexOf(t)>=0}function Vl(t){return t.attributes.code_format&&"no_code"!==t.attributes.code_format}$l({type:"mushroom-alarm-control-panel-card",name:"Mushroom Alarm Control Panel Card",description:"Card for alarm control panel"});const Fl=["1","2","3","4","5","6","7","8","9","","0","clear"];let Bl=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return od})),document.createElement("mushroom-alarm-control-panel-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ll.includes(t.split(".")[0])));return{type:"custom:mushroom-alarm-control-panel-card",entity:e[0],states:["armed_home","armed_away"]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t),this.loadComponents()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.loadComponents()}async loadComponents(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity;Vl(this.hass.states[t])&&Promise.resolve().then((function(){return xd}))}_onTap(t,e){var i,o;const n=function(t){return Pl[t]}(e);if(!n)return;t.stopPropagation();const r=(null===(i=this._input)||void 0===i?void 0:i.value)||void 0;this.hass.callService("alarm_control_panel",n,{entity_id:null===(o=this._config)||void 0===o?void 0:o.entity,code:r}),this._input&&(this._input.value="")}_handlePadClick(t){const e=t.currentTarget.value;this._input&&(this._input.value="clear"===e?"":this._input.value+e)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}get _hasCode(){var t,e,i;const o=null===(t=this._config)||void 0===t?void 0:t.entity;if(o){return Vl(this.hass.states[o])&&null!==(i=null===(e=this._config)||void 0===e?void 0:e.show_keypad)&&void 0!==i&&i}return!1}render(){if(!this.hass||!this._config||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type),a=this._config.states&&this._config.states.length>0?function(t){return"disarmed"===t.state}(e)?this._config.states.map((t=>({state:t}))):[{state:"disarmed"}]:[],l=function(t){return Dt!==t.state}(e),s=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i)}; + ${this.renderStateInfo(e,n,i)}; - ${a.length>0?N` + ${a.length>0?B` - ${a.map((t=>N` + ${a.map((t=>B` {switch(t){case"armed_away":return"mdi:shield-lock-outline";case"armed_vacation":return"mdi:shield-airplane-outline";case"armed_home":return"mdi:shield-home-outline";case"armed_night":return"mdi:shield-moon-outline";case"armed_custom_bypass":return"mdi:shield-half-full";case"disarmed":return"mdi:shield-off-outline";default:return"mdi:shield-outline"}})(t.state)} @click=${e=>this._onTap(e,t.state)} @@ -1366,37 +1353,37 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl `:null} - ${this._hasCode?N` + ${this._hasCode?B` - `:N``} - ${this._hasCode&&"number"===e.attributes.code_format?N` + `:B``} + ${this._hasCode&&"number"===e.attributes.code_format?B`
- ${xl.map((t=>""===t?N``:N` + ${Fl.map((t=>""===t?B``:B` ${"clear"===t?this.hass.localize("ui.card.alarm_control_panel.clear_code"):t} `))}
- `:N``} + `:B``}
- `}renderIcon(t,e){const i=vl(t.state),n=bl(t.state);return N` + `}renderIcon(t,e){const i=Nl(t.state),o=Rl(t.state);return B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -1425,16 +1412,16 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl width: 30%; box-sizing: border-box; } - `]}};n([ct()],wl.prototype,"_config",void 0),n([ht("#alarmCode")],wl.prototype,"_input",void 0),wl=n([at("mushroom-alarm-control-panel-card")],wl);let kl=class extends ot{constructor(){super(...arguments),this.icon="",this.label="",this.avatar="",this.avatarOnly=!1}render(){return N` + `]}};n([mt()],Bl.prototype,"_config",void 0),n([gt("#alarmCode")],Bl.prototype,"_input",void 0),Bl=n([dt("mushroom-alarm-control-panel-card")],Bl);let Ul=class extends st{constructor(){super(...arguments),this.icon="",this.label="",this.avatar="",this.avatarOnly=!1}render(){return B` - ${this.avatar?N` `:null} - ${this.avatarOnly?null:N` + ${this.avatar?B` `:null} + ${this.avatarOnly?null:B`
`}
- `}static get styles(){return d` + `}static get styles(){return h` :host { --icon-color: var(--primary-text-color); --text-color: var(--primary-text-color); @@ -1500,31 +1487,31 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl margin-right: initial; margin-left: 0.15em; } - `}};n([st()],kl.prototype,"icon",void 0),n([st()],kl.prototype,"label",void 0),n([st()],kl.prototype,"avatar",void 0),n([st()],kl.prototype,"avatarOnly",void 0),kl=n([at("mushroom-chip")],kl);const Cl=t=>{try{const e=document.createElement($l(t.type),t);return e.setConfig(t),e}catch(t){return}};function $l(t){return`mushroom-${t}-chip`}function El(t){return`mushroom-${t}-chip-editor`}let Al=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return qc})),document.createElement(El("entity"))}static async getStubConfig(t){return{type:"entity",entity:Object.keys(t.states)[0]}}setConfig(t){this._config=t}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){var t;if(!this.hass||!this._config||!this._config.entity)return N``;const e=this._config.entity,i=this.hass.states[e],n=this._config.name||i.attributes.friendly_name||"",o=this._config.icon||pl(i),r=this._config.icon_color,a=this._config.use_entity_picture?Pt(i):void 0,l=Ht(this.hass.localize,i,this.hass.locale),s=Lt(i);r&&xa(r);const c=Ia(null!==(t=this._config.content_info)&&void 0!==t?t:"state",n,l,i,this.hass),d=pe(this.hass);return N` + `}};n([ht()],Ul.prototype,"icon",void 0),n([ht()],Ul.prototype,"label",void 0),n([ht()],Ul.prototype,"avatar",void 0),n([ht()],Ul.prototype,"avatarOnly",void 0),Ul=n([dt("mushroom-chip")],Ul);const Hl=t=>{try{const e=document.createElement(Yl(t.type),t);return e.setConfig(t),e}catch(t){return}};function Yl(t){return`mushroom-${t}-chip`}function Xl(t){return`mushroom-${t}-chip-editor`}let Wl=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Cd})),document.createElement(Xl("entity"))}static async getStubConfig(t){return{type:"entity",entity:Object.keys(t.states)[0]}}setConfig(t){this._config=t}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){var t;if(!this.hass||!this._config||!this._config.entity)return B``;const e=this._config.entity,i=this.hass.states[e],o=this._config.name||i.attributes.friendly_name||"",n=this._config.icon||Dl(i),r=this._config.icon_color,a=this._config.use_entity_picture?Ft(i):void 0,l=ee(this.hass.localize,i,this.hass.locale,this.hass.entities,this.hass.connection.haVersion),s=Nt(i);r&&Ra(r);const c=Wa(null!==(t=this._config.content_info)&&void 0!==t?t:"state",o,l,i,this.hass),d=Ee(this.hass);return B` - ${a?null:this.renderIcon(o,r,s)} - ${c?N`${c}`:null} + ${a?null:this.renderIcon(n,r,s)} + ${c?B`${c}`:null} - `}renderIcon(t,e,i){const n={};if(e){const t=xa(e);n["--color"]=`rgb(${t})`}return N` + `}renderIcon(t,e,i){const o={};if(e){const t=Ra(e);o["--color"]=`rgb(${t})`}return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-chip { cursor: pointer; } ha-icon.active { color: var(--color); } - `}};n([st({attribute:!1})],Al.prototype,"hass",void 0),n([ct()],Al.prototype,"_config",void 0),Al=n([at($l("entity"))],Al);const Sl=new Set(["partlycloudy","cloudy","fog","windy","windy-variant","hail","rainy","snowy","snowy-rainy","pouring","lightning","lightning-rainy"]),Il=new Set(["hail","rainy","pouring"]),Tl=new Set(["windy","windy-variant"]),zl=new Set(["snowy","snowy-rainy"]),Ol=new Set(["lightning","lightning-rainy"]),Ml=d` + `}};n([ht({attribute:!1})],Wl.prototype,"hass",void 0),n([mt()],Wl.prototype,"_config",void 0),Wl=n([dt(Yl("entity"))],Wl);const ql=new Set(["partlycloudy","cloudy","fog","windy","windy-variant","hail","rainy","snowy","snowy-rainy","pouring","lightning","lightning-rainy"]),Kl=new Set(["hail","rainy","pouring"]),Gl=new Set(["windy","windy-variant"]),Zl=new Set(["snowy","snowy-rainy"]),Jl=new Set(["lightning","lightning-rainy"]),Ql=h` .rain { fill: var(--weather-icon-rain-color, #30b3ff); } @@ -1540,35 +1527,35 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .cloud-front { fill: var(--weather-icon-cloud-front-color, #f9f9f9); } -`;let Ll=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return td})),document.createElement(El("weather"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>"weather"===t.split(".")[0]));return{type:"weather",entity:e[0]}}setConfig(t){this._config=t}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=(n=e.state,o=!0,V` +`;let ts=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Td})),document.createElement(Xl("weather"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>"weather"===t.split(".")[0]));return{type:"weather",entity:e[0]}}setConfig(t){this._config=t}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=(o=e.state,n=!0,U` - ${"sunny"===n?V` + ${"sunny"===o?U` `:""} - ${"clear-night"===n?V` + ${"clear-night"===o?U` `:""} - ${"partlycloudy"===n&&o?V` + ${"partlycloudy"===o&&n?U` - `:"partlycloudy"===n?V` + `:"partlycloudy"===o?U` `:""} - ${Sl.has(n)?V` + ${ql.has(o)?U` `:""} - ${Il.has(n)?V` + ${Kl.has(o)?U` `:""} - ${"pouring"===n?V` + ${"pouring"===o?U` `:""} - ${Tl.has(n)?V` + ${Gl.has(o)?U` `:""} - ${zl.has(n)?V` + ${Zl.has(o)?U` `:""} - ${Ol.has(n)?V` + ${Jl.has(o)?U` `:""} - `);var n,o;const r=[];if(this._config.show_conditions){const t=Ht(this.hass.localize,e,this.hass.locale);r.push(t)}if(this._config.show_temperature){const t=`${Bt(e.attributes.temperature,this.hass.locale)} ${this.hass.config.unit_system.temperature}`;r.push(t)}const a=pe(this.hass);return N` + `);var o,n;const r=[];if(this._config.show_conditions){const t=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);r.push(t)}if(this._config.show_temperature){const t=`${Zt(e.attributes.temperature,this.hass.locale)} ${this.hass.config.unit_system.temperature}`;r.push(t)}const a=Ee(this.hass);return B` ${i} - ${r.length>0?N`${r.join(" / ")}`:null} + ${r.length>0?B`${r.join(" / ")}`:null} - `}static get styles(){return[Ml,d` + `}static get styles(){return[Ql,h` mushroom-chip { cursor: pointer; } - `]}};n([st({attribute:!1})],Ll.prototype,"hass",void 0),n([ct()],Ll.prototype,"_config",void 0),Ll=n([at($l("weather"))],Ll);let Dl=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return nd})),document.createElement(El("back"))}static async getStubConfig(t){return{type:"back"}}setConfig(t){this._config=t}_handleAction(){window.history.back()}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:arrow-left",e=pe(this.hass);return N` + `]}};n([ht({attribute:!1})],ts.prototype,"hass",void 0),n([mt()],ts.prototype,"_config",void 0),ts=n([dt(Yl("weather"))],ts);let es=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Md})),document.createElement(Xl("back"))}static async getStubConfig(t){return{type:"back"}}setConfig(t){this._config=t}_handleAction(){window.history.back()}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:arrow-left",e=Ee(this.hass);return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-chip { cursor: pointer; } - `}};n([st({attribute:!1})],Dl.prototype,"hass",void 0),n([ct()],Dl.prototype,"_config",void 0),Dl=n([at($l("back"))],Dl);let jl=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return ld})),document.createElement(El("action"))}static async getStubConfig(t){return{type:"action"}}setConfig(t){this._config=t}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:flash",e=this._config.icon_color,i={};if(e){const t=xa(e);i["--color"]=`rgb(${t})`}const n=pe(this.hass);return N` + `}};n([ht({attribute:!1})],es.prototype,"hass",void 0),n([mt()],es.prototype,"_config",void 0),es=n([dt(Yl("back"))],es);let is=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Pd})),document.createElement(Xl("action"))}static async getStubConfig(t){return{type:"action"}}setConfig(t){this._config=t}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:flash",e=this._config.icon_color,i={};if(e){const t=Ra(e);i["--color"]=`rgb(${t})`}const o=Ee(this.hass);return B` - + - `}static get styles(){return d` + `}static get styles(){return h` mushroom-chip { cursor: pointer; } ha-icon { color: var(--color); } - `}};n([st({attribute:!1})],jl.prototype,"hass",void 0),n([ct()],jl.prototype,"_config",void 0),jl=n([at($l("action"))],jl);let Pl=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return dd})),document.createElement(El("menu"))}static async getStubConfig(t){return{type:"menu"}}setConfig(t){this._config=t}_handleAction(){At(this,"hass-toggle-menu")}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:menu",e=pe(this.hass);return N` + `}};n([ht({attribute:!1})],is.prototype,"hass",void 0),n([mt()],is.prototype,"_config",void 0),is=n([dt(Yl("action"))],is);let os=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Vd})),document.createElement(Xl("menu"))}static async getStubConfig(t){return{type:"menu"}}setConfig(t){this._config=t}_handleAction(){zt(this,"hass-toggle-menu")}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:menu",e=Ee(this.hass);return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-chip { cursor: pointer; } - `}};n([st({attribute:!1})],Pl.prototype,"hass",void 0),n([ct()],Pl.prototype,"_config",void 0),Pl=n([at($l("menu"))],Pl);const Nl=["content","icon","icon_color","picture"];let Vl=class extends ot{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return vd})),document.createElement(El("template"))}static async getStubConfig(t){return{type:"template"}}setConfig(t){Nl.forEach((e=>{var i,n;(null===(i=this._config)||void 0===i?void 0:i[e])===t[e]&&(null===(n=this._config)||void 0===n?void 0:n.entity)==t.entity||this._tryDisconnectKey(e)})),this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}render(){if(!this.hass||!this._config)return N``;const t=this.getValue("icon"),e=this.getValue("icon_color"),i=this.getValue("content"),n=this.getValue("picture"),o=pe(this.hass);return N` + `}};n([ht({attribute:!1})],os.prototype,"hass",void 0),n([mt()],os.prototype,"_config",void 0),os=n([dt(Yl("menu"))],os);const ns=["content","icon","icon_color","picture"];let rs=class extends st{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return qd})),document.createElement(Xl("template"))}static async getStubConfig(t){return{type:"template"}}setConfig(t){ns.forEach((e=>{var i,o;(null===(i=this._config)||void 0===i?void 0:i[e])===t[e]&&(null===(o=this._config)||void 0===o?void 0:o.entity)==t.entity||this._tryDisconnectKey(e)})),this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}render(){if(!this.hass||!this._config)return B``;const t=this.getValue("icon"),e=this.getValue("icon_color"),i=this.getValue("content"),o=this.getValue("picture"),n=Ee(this.hass);return B` - ${t&&!n?this.renderIcon(t,e):null} + ${t&&!o?this.renderIcon(t,e):null} ${i?this.renderContent(i):null} - `}renderIcon(t,e){const i={};if(e){const t=xa(e);i["--color"]=`rgb(${t})`}return N``}renderContent(t){return N`${t}`}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){Nl.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=ke(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name,entity:this._config.entity},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const n={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:n}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){Nl.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return d` + `}renderIcon(t,e){const i={};if(e){const t=Ra(e);i["--color"]=`rgb(${t})`}return B``}renderContent(t){return B`${t}`}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){ns.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=Le(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name,entity:this._config.entity},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const o={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:o}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){ns.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return h` mushroom-chip { cursor: pointer; } ha-icon { color: var(--color); } - `}};n([st({attribute:!1})],Vl.prototype,"hass",void 0),n([ct()],Vl.prototype,"_config",void 0),n([ct()],Vl.prototype,"_templateResults",void 0),n([ct()],Vl.prototype,"_unsubRenderTemplates",void 0),Vl=n([at($l("template"))],Vl);let Rl=class extends b{constructor(){super(...arguments),this.hidden=!1}createRenderRoot(){return this}validateConfig(t){if(!t.conditions)throw new Error("No conditions configured");if(!Array.isArray(t.conditions))throw new Error("Conditions need to be an array");if(!t.conditions.every((t=>t.entity&&(t.state||t.state_not))))throw new Error("Conditions are invalid");this.lastChild&&this.removeChild(this.lastChild),this._config=t}update(t){if(super.update(t),!this._element||!this.hass||!this._config)return;this._element.editMode=this.editMode;const e=this.editMode||(i=this._config.conditions,n=this.hass,i.every((t=>{const e=n.states[t.entity]?n.states[t.entity].state:Tt;return t.state?e===t.state:e!==t.state_not})));var i,n;this.hidden=!e,this.style.setProperty("display",e?"":"none"),e&&(this._element.hass=this.hass,this._element.parentElement||this.appendChild(this._element))}};n([st({attribute:!1})],Rl.prototype,"hass",void 0),n([st()],Rl.prototype,"editMode",void 0),n([st()],Rl.prototype,"_config",void 0),n([st({type:Boolean,reflect:!0})],Rl.prototype,"hidden",void 0),Rl=n([at("mushroom-conditional-base")],Rl);let Fl=class extends Rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Fh})),document.createElement(El("conditional"))}static async getStubConfig(){return{type:"conditional",conditions:[]}}setConfig(t){if(this.validateConfig(t),!t.chip)throw new Error("No row configured");this._element=Cl(t.chip)}};function Bl(t){return null!=t.attributes.brightness?Math.max(Math.round(100*t.attributes.brightness/255),1):void 0}function Ul(t){return null!=t.attributes.rgb_color?t.attributes.rgb_color:void 0}function Hl(t){return ba.rgb(t).l()>96}function Yl(t){return ba.rgb(t).l()>97}function Xl(t){return(t=>{var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>xe.includes(t)))})(t)}function Wl(t){return(t=>{var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>we.includes(t)))})(t)}Fl=n([at($l("conditional"))],Fl);let ql=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return Kh})),document.createElement(El("light"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>"light"===t.split(".")[0]));return{type:"light",entity:e[0]}}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){var t,e;if(!this.hass||!this._config||!this._config.entity)return N``;const i=this._config.entity,n=this.hass.states[i],o=this._config.name||n.attributes.friendly_name||"",r=this._config.icon||pl(n),a=Ht(this.hass.localize,n,this.hass.locale),l=Lt(n),s=Ul(n),c={};if(s&&(null===(t=this._config)||void 0===t?void 0:t.use_light_color)){const t=s.join(",");c["--color"]=`rgb(${t})`,Yl(s)&&(c["--color"]="rgba(var(--rgb-primary-text-color), 0.2)")}const d=Ia(null!==(e=this._config.content_info)&&void 0!==e?e:"state",o,a,n,this.hass),u=pe(this.hass);return N` + `}};n([ht({attribute:!1})],rs.prototype,"hass",void 0),n([mt()],rs.prototype,"_config",void 0),n([mt()],rs.prototype,"_templateResults",void 0),n([mt()],rs.prototype,"_unsubRenderTemplates",void 0),rs=n([dt(Yl("template"))],rs);let as=class extends w{constructor(){super(...arguments),this.hidden=!1}createRenderRoot(){return this}validateConfig(t){if(!t.conditions)throw new Error("No conditions configured");if(!Array.isArray(t.conditions))throw new Error("Conditions need to be an array");if(!t.conditions.every((t=>t.entity&&(t.state||t.state_not))))throw new Error("Conditions are invalid");this.lastChild&&this.removeChild(this.lastChild),this._config=t}update(t){if(super.update(t),!this._element||!this.hass||!this._config)return;this._element.editMode=this.editMode;const e=this.editMode||(i=this._config.conditions,o=this.hass,i.every((t=>{const e=o.states[t.entity]?o.states[t.entity].state:Dt;return t.state?e===t.state:e!==t.state_not})));var i,o;this.hidden=!e,this.style.setProperty("display",e?"":"none"),e&&(this._element.hass=this.hass,this._element.parentElement||this.appendChild(this._element))}};n([ht({attribute:!1})],as.prototype,"hass",void 0),n([ht()],as.prototype,"editMode",void 0),n([ht()],as.prototype,"_config",void 0),n([ht({type:Boolean,reflect:!0})],as.prototype,"hidden",void 0),as=n([dt("mushroom-conditional-base")],as);let ls=class extends as{static async getConfigElement(){return await Promise.resolve().then((function(){return tm})),document.createElement(Xl("conditional"))}static async getStubConfig(){return{type:"conditional",conditions:[]}}setConfig(t){if(this.validateConfig(t),!t.chip)throw new Error("No row configured");this._element=Hl(t.chip)}};function ss(t){return null!=t.attributes.brightness?Math.max(Math.round(100*t.attributes.brightness/255),1):void 0}function cs(t){return null!=t.attributes.rgb_color?t.attributes.rgb_color:void 0}function ds(t){return Pa.rgb(t).l()>96}function us(t){return Pa.rgb(t).l()>97}function hs(t){return(t=>{var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>Me.includes(t)))})(t)}function ms(t){return(t=>{var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>De.includes(t)))})(t)}ls=n([dt(Yl("conditional"))],ls);let ps=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return sm})),document.createElement(Xl("light"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>"light"===t.split(".")[0]));return{type:"light",entity:e[0]}}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){var t,e;if(!this.hass||!this._config||!this._config.entity)return B``;const i=this._config.entity,o=this.hass.states[i],n=this._config.name||o.attributes.friendly_name||"",r=this._config.icon||Dl(o),a=ee(this.hass.localize,o,this.hass.locale,this.hass.entities,this.hass.connection.haVersion),l=Nt(o),s=cs(o),c={};if(s&&(null===(t=this._config)||void 0===t?void 0:t.use_light_color)){const t=s.join(",");c["--color"]=`rgb(${t})`,us(s)&&(c["--color"]="rgba(var(--rgb-primary-text-color), 0.2)")}const d=Wa(null!==(e=this._config.content_info)&&void 0!==e?e:"state",n,a,o,this.hass),u=Ee(this.hass);return B` - ${d?N`${d}`:null} + ${d?B`${d}`:null} - `}static get styles(){return d` + `}static get styles(){return h` :host { --color: rgb(var(--rgb-state-light)); } @@ -1729,20 +1716,20 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl ha-icon.active { color: var(--color); } - `}};n([st({attribute:!1})],ql.prototype,"hass",void 0),n([ct()],ql.prototype,"_config",void 0),ql=n([at($l("light"))],ql);let Kl=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return Qh})),document.createElement(El("alarm-control-panel"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>fl.includes(t.split(".")[0])));return{type:"alarm-control-panel",entity:e[0]}}setConfig(t){this._config=t}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){var t;if(!this.hass||!this._config||!this._config.entity)return N``;const e=this._config.entity,i=this.hass.states[e],n=this._config.name||i.attributes.friendly_name||"",o=this._config.icon||pl(i),r=vl(i.state),a=bl(i.state),l=Ht(this.hass.localize,i,this.hass.locale),s={};if(r){const t=xa(r);s["--color"]=`rgb(${t})`}const c=Ia(null!==(t=this._config.content_info)&&void 0!==t?t:"state",n,l,i,this.hass),d=pe(this.hass);return N` + `}};n([ht({attribute:!1})],ps.prototype,"hass",void 0),n([mt()],ps.prototype,"_config",void 0),ps=n([dt(Yl("light"))],ps);let fs=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return hm})),document.createElement(Xl("alarm-control-panel"))}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ll.includes(t.split(".")[0])));return{type:"alarm-control-panel",entity:e[0]}}setConfig(t){this._config=t}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){var t;if(!this.hass||!this._config||!this._config.entity)return B``;const e=this._config.entity,i=this.hass.states[e],o=this._config.name||i.attributes.friendly_name||"",n=this._config.icon||Dl(i),r=Nl(i.state),a=Rl(i.state),l=ee(this.hass.localize,i,this.hass.locale,this.hass.entities,this.hass.connection.haVersion),s={};if(r){const t=Ra(r);s["--color"]=`rgb(${t})`}const c=Wa(null!==(t=this._config.content_info)&&void 0!==t?t:"state",o,l,i,this.hass),d=Ee(this.hass);return B` - ${c?N`${c}`:null} + ${c?B`${c}`:null} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-chip { cursor: pointer; } @@ -1752,14 +1739,14 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl ha-icon.pulse { animation: 1s ease 0s infinite normal none running pulse; } - ${Ha} - `}};n([st({attribute:!1})],Kl.prototype,"hass",void 0),n([ct()],Kl.prototype,"_config",void 0),Kl=n([at($l("alarm-control-panel"))],Kl);ll({type:"mushroom-chips-card",name:"Mushroom Chips Card",description:"Card with chips to display informations"});let Gl=class extends ot{static async getConfigElement(){return await Promise.resolve().then((function(){return _m})),document.createElement("mushroom-chips-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-chips-card",chips:await Promise.all([Al.getStubConfig(t)])}}set hass(t){var e;const i=nl(this._hass),n=nl(t);i!==n&&this.toggleAttribute("dark-mode",n),this._hass=t,null===(e=this.shadowRoot)||void 0===e||e.querySelectorAll("div > *").forEach((e=>{e.hass=t}))}getCardSize(){return 1}setConfig(t){this._config=t}render(){if(!this._config||!this._hass)return N``;let t="";this._config.alignment&&(t=`align-${this._config.alignment}`);const e=pe(this._hass);return N` + ${sl} + `}};n([ht({attribute:!1})],fs.prototype,"hass",void 0),n([mt()],fs.prototype,"_config",void 0),fs=n([dt(Yl("alarm-control-panel"))],fs);$l({type:"mushroom-chips-card",name:"Mushroom Chips Card",description:"Card with chips to display informations"});let gs=class extends st{static async getConfigElement(){return await Promise.resolve().then((function(){return Tm})),document.createElement("mushroom-chips-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-chips-card",chips:await Promise.all([Wl.getStubConfig(t)])}}set hass(t){var e;const i=xl(this._hass),o=xl(t);i!==o&&this.toggleAttribute("dark-mode",o),this._hass=t,null===(e=this.shadowRoot)||void 0===e||e.querySelectorAll("div > *").forEach((e=>{e.hass=t}))}getCardSize(){return 1}setConfig(t){this._config=t}render(){if(!this._config||!this._hass)return B``;let t="";this._config.alignment&&(t=`align-${this._config.alignment}`);const e=Ee(this._hass);return B`
${this._config.chips.map((t=>this.renderChip(t)))}
- `}renderChip(t){const e=Cl(t);return e?(this._hass&&(e.hass=this._hass),N`${e}`):N``}static get styles(){return[ol.styles,d` + `}renderChip(t){const e=Hl(t);return e?(this._hass&&(e.hass=this._hass),B`${e}`):B``}static get styles(){return[wl.styles,h` ha-card { background: none; box-shadow: none; @@ -1793,33 +1780,33 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl margin-right: initial; margin-left: var(--chip-spacing); } - `]}};n([ct()],Gl.prototype,"_config",void 0),Gl=n([at("mushroom-chips-card")],Gl);const Zl=["climate"],Jl={auto:"var(--rgb-state-climate-auto)",cool:"var(--rgb-state-climate-cool)",dry:"var(--rgb-state-climate-dry)",fan_only:"var(--rgb-state-climate-fan-only)",heat:"var(--rgb-state-climate-heat)",heat_cool:"var(--rgb-state-climate-heat-cool)",off:"var(--rgb-state-climate-off)"},Ql={cooling:"var(--rgb-state-climate-cool)",drying:"var(--rgb-state-climate-dry)",heating:"var(--rgb-state-climate-heat)",idle:"var(--rgb-state-climate-idle)",off:"var(--rgb-state-climate-off)"},ts={auto:"mdi:calendar-sync",cool:"mdi:snowflake",dry:"mdi:water-percent",fan_only:"mdi:fan",heat:"mdi:fire",heat_cool:"mdi:autorenew",off:"mdi:power"},es={cooling:"mdi:snowflake",drying:"mdi:water-percent",heating:"mdi:fire",idle:"mdi:clock-outline",off:"mdi:power"};function is(t){var e;return null!==(e=Jl[t])&&void 0!==e?e:Jl.off}let ns=class extends ot{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.mode;this.hass.callService("climate","set_hvac_mode",{entity_id:this.entity.entity_id,hvac_mode:e})}render(){const t=pe(this.hass),e=this.entity.attributes.hvac_modes.filter((t=>{var e;return(null!==(e=this.modes)&&void 0!==e?e:[]).includes(t)})).sort(be);return N` + `]}};n([mt()],gs.prototype,"_config",void 0),gs=n([dt("mushroom-chips-card")],gs);const _s=["climate"],vs={auto:"var(--rgb-state-climate-auto)",cool:"var(--rgb-state-climate-cool)",dry:"var(--rgb-state-climate-dry)",fan_only:"var(--rgb-state-climate-fan-only)",heat:"var(--rgb-state-climate-heat)",heat_cool:"var(--rgb-state-climate-heat-cool)",off:"var(--rgb-state-climate-off)"},bs={cooling:"var(--rgb-state-climate-cool)",drying:"var(--rgb-state-climate-dry)",heating:"var(--rgb-state-climate-heat)",idle:"var(--rgb-state-climate-idle)",off:"var(--rgb-state-climate-off)"},ys={auto:"mdi:calendar-sync",cool:"mdi:snowflake",dry:"mdi:water-percent",fan_only:"mdi:fan",heat:"mdi:fire",heat_cool:"mdi:autorenew",off:"mdi:power"},xs={cooling:"mdi:snowflake",drying:"mdi:water-percent",heating:"mdi:fire",idle:"mdi:clock-outline",off:"mdi:power"};function ws(t){var e;return null!==(e=vs[t])&&void 0!==e?e:vs.off}let ks=class extends st{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.mode;this.hass.callService("climate","set_hvac_mode",{entity_id:this.entity.entity_id,hvac_mode:e})}render(){const t=Ee(this.hass),e=this.entity.attributes.hvac_modes.filter((t=>{var e;return(null!==(e=this.modes)&&void 0!==e?e:[]).includes(t)})).sort(ze);return B` ${e.map((t=>this.renderModeButton(t)))} - `}renderModeButton(t){const e={},i="off"===t?"var(--rgb-grey)":is(t);return t===this.entity.state&&(e["--icon-color"]=`rgb(${i})`,e["--bg-color"]=`rgba(${i}, 0.2)`),N` + `}renderModeButton(t){const e={},i="off"===t?"var(--rgb-grey)":ws(t);return t===this.entity.state&&(e["--icon-color"]=`rgb(${i})`,e["--bg-color"]=`rgba(${i}, 0.2)`),B` - `}};n([st({attribute:!1})],ns.prototype,"hass",void 0),n([st({attribute:!1})],ns.prototype,"entity",void 0),n([st({attribute:!1})],ns.prototype,"modes",void 0),n([st()],ns.prototype,"fill",void 0),ns=n([at("mushroom-climate-hvac-modes-control")],ns);let os=class extends ot{constructor(){super(...arguments),this.disabled=!1,this.formatOptions={},this.pending=!1,this.dispatchValue=t=>{this.pending=!1,this.dispatchEvent(new CustomEvent("change",{detail:{value:t}}))},this.debounceDispatchValue=this.dispatchValue}_incrementValue(t){var e;if(t.stopPropagation(),!this.value)return;const i=Rt(this.value+(null!==(e=this.step)&&void 0!==e?e:1),1);this._processNewValue(i)}_decrementValue(t){var e;if(t.stopPropagation(),!this.value)return;const i=Rt(this.value-(null!==(e=this.step)&&void 0!==e?e:1),1);this._processNewValue(i)}firstUpdated(t){super.firstUpdated(t);const e=(t=>{const e=window.getComputedStyle(t).getPropertyValue("--input-number-debounce"),i=parseFloat(e);return isNaN(i)?2e3:i})(this.container);e&&(this.debounceDispatchValue=fe(this.dispatchValue,e))}_processNewValue(t){const e=((t,e,i)=>{let n;return n=e?Math.max(t,e):t,n=i?Math.min(n,i):n,n})(t,this.min,this.max);this.value!==e&&(this.value=e,this.pending=!0),this.debounceDispatchValue(e)}render(){const t=null!=this.value?Bt(this.value,this.locale,this.formatOptions):"-";return N` + `}};n([ht({attribute:!1})],ks.prototype,"hass",void 0),n([ht({attribute:!1})],ks.prototype,"entity",void 0),n([ht({attribute:!1})],ks.prototype,"modes",void 0),n([ht()],ks.prototype,"fill",void 0),ks=n([dt("mushroom-climate-hvac-modes-control")],ks);let Cs=class extends st{constructor(){super(...arguments),this.disabled=!1,this.formatOptions={},this.pending=!1,this.dispatchValue=t=>{this.pending=!1,this.dispatchEvent(new CustomEvent("change",{detail:{value:t}}))},this.debounceDispatchValue=this.dispatchValue}_incrementValue(t){var e;if(t.stopPropagation(),null==this.value)return;const i=Gt(this.value+(null!==(e=this.step)&&void 0!==e?e:1),1);this._processNewValue(i)}_decrementValue(t){var e;if(t.stopPropagation(),null==this.value)return;const i=Gt(this.value-(null!==(e=this.step)&&void 0!==e?e:1),1);this._processNewValue(i)}firstUpdated(t){super.firstUpdated(t);const e=(t=>{const e=window.getComputedStyle(t).getPropertyValue("--input-number-debounce"),i=parseFloat(e);return isNaN(i)?2e3:i})(this.container);e&&(this.debounceDispatchValue=Ae(this.dispatchValue,e))}_processNewValue(t){const e=((t,e,i)=>{let o;return o=e?Math.max(t,e):t,o=i?Math.min(o,i):o,o})(t,this.min,this.max);this.value!==e&&(this.value=e,this.pending=!0),this.debounceDispatchValue(e)}render(){const t=null!=this.value?Zt(this.value,this.locale,this.formatOptions):"-";return B`
- ${t} -
- `}static get styles(){return d` + `}static get styles(){return h` :host { --text-color: var(--primary-text-color); --text-color-disabled: rgb(var(--rgb-disabled)); @@ -1839,23 +1826,32 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl display: flex; flex-direction: row; align-items: center; - justify-content: space-between; + justify-content: center; border-radius: var(--control-border-radius); border: none; background-color: var(--bg-color); transition: background-color 280ms ease-in-out; + height: var(--control-height); + overflow: hidden; } .button { display: flex; flex-direction: row; align-items: center; justify-content: center; - padding: 6px; + padding: 4px; border: none; background: none; cursor: pointer; border-radius: var(--control-border-radius); line-height: 0; + height: 100%; + } + .minus { + padding-right: 0; + } + .plus { + padding-left: 0; } .button:disabled { cursor: not-allowed; @@ -1869,19 +1865,23 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .button:disabled ha-icon { color: var(--icon-color-disabled); } - span { + .value { + text-align: center; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 20px; font-weight: bold; color: var(--text-color); } - span.disabled { + .value.disabled { color: var(--text-color-disabled); } - span.pending { + .value.pending { opacity: 0.5; } - `}};n([st({attribute:!1})],os.prototype,"locale",void 0),n([st({type:Boolean})],os.prototype,"disabled",void 0),n([st({attribute:!1,type:Number,reflect:!0})],os.prototype,"value",void 0),n([st({type:Number})],os.prototype,"step",void 0),n([st({type:Number})],os.prototype,"min",void 0),n([st({type:Number})],os.prototype,"max",void 0),n([st({attribute:"false"})],os.prototype,"formatOptions",void 0),n([ct()],os.prototype,"pending",void 0),n([ht("#container")],os.prototype,"container",void 0),os=n([at("mushroom-input-number")],os);let rs=class extends ot{constructor(){super(...arguments),this.fill=!1}get _stepSize(){return this.entity.attributes.target_temp_step?this.entity.attributes.target_temp_step:"°F"===this.hass.config.unit_system.temperature?1:.5}onValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,temperature:e})}onLowValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,target_temp_low:e,target_temp_high:this.entity.attributes.target_temp_high})}onHighValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,target_temp_low:this.entity.attributes.target_temp_low,target_temp_high:e})}render(){const t=pe(this.hass),e=Dt(this.entity),i=1===this._stepSize?{maximumFractionDigits:0}:{minimumFractionDigits:1,maximumFractionDigits:1},n=t=>({"--bg-color":`rgba(var(--rgb-state-climate-${t}), 0.05)`,"--icon-color":`rgb(var(--rgb-state-climate-${t}))`,"--text-color":`rgb(var(--rgb-state-climate-${t}))`});return N` + `}};n([ht({attribute:!1})],Cs.prototype,"locale",void 0),n([ht({type:Boolean})],Cs.prototype,"disabled",void 0),n([ht({attribute:!1,type:Number,reflect:!0})],Cs.prototype,"value",void 0),n([ht({type:Number})],Cs.prototype,"step",void 0),n([ht({type:Number})],Cs.prototype,"min",void 0),n([ht({type:Number})],Cs.prototype,"max",void 0),n([ht({attribute:"false"})],Cs.prototype,"formatOptions",void 0),n([mt()],Cs.prototype,"pending",void 0),n([gt("#container")],Cs.prototype,"container",void 0),Cs=n([dt("mushroom-input-number")],Cs);let $s=class extends st{constructor(){super(...arguments),this.fill=!1}get _stepSize(){return this.entity.attributes.target_temp_step?this.entity.attributes.target_temp_step:"°F"===this.hass.config.unit_system.temperature?1:.5}onValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,temperature:e})}onLowValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,target_temp_low:e,target_temp_high:this.entity.attributes.target_temp_high})}onHighValueChange(t){const e=t.detail.value;this.hass.callService("climate","set_temperature",{entity_id:this.entity.entity_id,target_temp_low:this.entity.attributes.target_temp_low,target_temp_high:e})}render(){const t=Ee(this.hass),e=Rt(this.entity),i=1===this._stepSize?{maximumFractionDigits:0}:{minimumFractionDigits:1,maximumFractionDigits:1},o=t=>({"--bg-color":`rgba(var(--rgb-state-climate-${t}), 0.05)`,"--icon-color":`rgb(var(--rgb-state-climate-${t}))`,"--text-color":`rgb(var(--rgb-state-climate-${t}))`});return B` - ${null!=this.entity.attributes.temperature?N` + ${null!=this.entity.attributes.temperature?B` `:null} - ${null!=this.entity.attributes.target_temp_low&&null!=this.entity.attributes.target_temp_high?N` + ${null!=this.entity.attributes.target_temp_low&&null!=this.entity.attributes.target_temp_high?B` `:null} - `}};n([st({attribute:!1})],rs.prototype,"hass",void 0),n([st({attribute:!1})],rs.prototype,"entity",void 0),n([st()],rs.prototype,"fill",void 0),rs=n([at("mushroom-climate-temperature-control")],rs);const as={temperature_control:"mdi:thermometer",hvac_mode_control:"mdi:thermostat"};ll({type:"mushroom-climate-card",name:"Mushroom Climate Card",description:"Card for climate entity"});let ls=class extends rl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return km})),document.createElement("mushroom-climate-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Zl.includes(t.split(".")[0])));return{type:"custom:mushroom-climate-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updateControls()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updateControls()}updateControls(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=[];this._config.collapsible_controls&&!Lt(e)||((t=>null!=t.attributes.temperature||null!=t.attributes.target_temp_low&&null!=t.attributes.target_temp_high)(e)&&this._config.show_temperature_control&&i.push("temperature_control"),((t,e)=>(t.attributes.hvac_modes||[]).some((t=>(null!=e?e:[]).includes(t))))(e,this._config.hvac_modes)&&i.push("hvac_mode_control")),this._controls=i;const n=!!this._activeControl&&i.includes(this._activeControl);this._activeControl=n?this._activeControl:i[0]}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type);let a=Ht(this.hass.localize,e,this.hass.locale);if(null!==e.attributes.current_temperature){a+=` - ${Bt(e.attributes.current_temperature,this.hass.locale)} ${this.hass.config.unit_system.temperature}`}const l=pe(this.hass);return N` - - + `}};n([ht({attribute:!1})],$s.prototype,"hass",void 0),n([ht({attribute:!1})],$s.prototype,"entity",void 0),n([ht()],$s.prototype,"fill",void 0),$s=n([dt("mushroom-climate-temperature-control")],$s);const Es={temperature_control:"mdi:thermometer",hvac_mode_control:"mdi:thermostat"};$l({type:"mushroom-climate-card",name:"Mushroom Climate Card",description:"Card for climate entity"});let As=class extends kl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return jm})),document.createElement("mushroom-climate-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>_s.includes(t.split(".")[0])));return{type:"custom:mushroom-climate-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updateControls()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updateControls()}updateControls(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=[];this._config.collapsible_controls&&!Nt(e)||((t=>null!=t.attributes.temperature||null!=t.attributes.target_temp_low&&null!=t.attributes.target_temp_high)(e)&&this._config.show_temperature_control&&i.push("temperature_control"),((t,e)=>(t.attributes.hvac_modes||[]).some((t=>(null!=e?e:[]).includes(t))))(e,this._config.hvac_modes)&&i.push("hvac_mode_control")),this._controls=i;const o=!!this._activeControl&&i.includes(this._activeControl);this._activeControl=o?this._activeControl:i[0]}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type);let a=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);if(null!==e.attributes.current_temperature){a+=` - ${Zt(e.attributes.current_temperature,this.hass.locale)} ${this.hass.config.unit_system.temperature}`}const l=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i,a)}; + ${this.renderStateInfo(e,n,i,a)}; - ${this._controls.length>0?N` + ${this._controls.length>0?B`
${this.renderActiveControl(e)}${this.renderOtherControls()}
`:null}
- `}renderIcon(t,e){const i=Dt(t),n=is(t.state),o={};return o["--icon-color"]=`rgb(${n})`,o["--shape-color"]=`rgba(${n}, 0.2)`,N` + `}renderIcon(t,e){const i=Rt(t),o=ws(t.state),n={};return n["--icon-color"]=`rgb(${o})`,n["--shape-color"]=`rgba(${o}, 0.2)`,B` - `}renderBadge(t){return!Dt(t)?super.renderBadge(t):this.renderActionBadge(t)}renderActionBadge(t){const e=t.attributes.hvac_action;if(!e||"off"==e)return null;const i=function(t){var e;return null!==(e=Ql[t])&&void 0!==e?e:Ql.off}(e),n=function(t){var e;return null!==(e=es[t])&&void 0!==e?e:""}(e);return n?N` + `}renderBadge(t){return!Rt(t)?super.renderBadge(t):this.renderActionBadge(t)}renderActionBadge(t){const e=t.attributes.hvac_action;if(!e||"off"==e)return null;const i=function(t){var e;return null!==(e=bs[t])&&void 0!==e?e:bs.off}(e),o=function(t){var e;return null!==(e=xs[t])&&void 0!==e?e:""}(e);return o?B` - `:null}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return N` - ${t.map((t=>N` + `:null}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return B` + ${t.map((t=>B` this._onControlTap(t,e)} > `))} - `}renderActiveControl(t){var e,i;const n=null!==(i=null===(e=this._config)||void 0===e?void 0:e.hvac_modes)&&void 0!==i?i:[];switch(this._activeControl){case"temperature_control":return N` + `}renderActiveControl(t){var e;const i=null!==(e=this._config.hvac_modes)&&void 0!==e?e:[],o=ml(this._config);switch(this._activeControl){case"temperature_control":return B` - `;case"hvac_mode_control":return N` + `;case"hvac_mode_control":return B` - `;default:return null}}static get styles(){return[super.styles,al,d` + `;default:return null}}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -1979,50 +1979,51 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-climate-hvac-modes-control { flex: 1; } - `]}};n([ct()],ls.prototype,"_config",void 0),n([ct()],ls.prototype,"_activeControl",void 0),n([ct()],ls.prototype,"_controls",void 0),ls=n([at("mushroom-climate-card")],ls);const ss=["cover"];let cs=class extends ot{constructor(){super(...arguments),this.fill=!1}_onOpenTap(t){t.stopPropagation(),this.hass.callService("cover","open_cover",{entity_id:this.entity.entity_id})}_onCloseTap(t){t.stopPropagation(),this.hass.callService("cover","close_cover",{entity_id:this.entity.entity_id})}_onStopTap(t){t.stopPropagation(),this.hass.callService("cover","stop_cover",{entity_id:this.entity.entity_id})}get openDisabled(){const t=!0===this.entity.attributes.assumed_state;return((void 0!==(e=this.entity).attributes.current_position?100===e.attributes.current_position:"open"===e.state)||function(t){return"opening"===t.state}(this.entity))&&!t;var e}get closedDisabled(){const t=!0===this.entity.attributes.assumed_state;return((void 0!==(e=this.entity).attributes.current_position?0===e.attributes.current_position:"closed"===e.state)||function(t){return"closing"===t.state}(this.entity))&&!t;var e}render(){const t=pe(this.hass);return N` + `]}};n([mt()],As.prototype,"_config",void 0),n([mt()],As.prototype,"_activeControl",void 0),n([mt()],As.prototype,"_controls",void 0),As=n([dt("mushroom-climate-card")],As);const Ss=["cover"];let Is=class extends st{constructor(){super(...arguments),this.fill=!1}_onOpenTap(t){t.stopPropagation(),this.hass.callService("cover","open_cover",{entity_id:this.entity.entity_id})}_onCloseTap(t){t.stopPropagation(),this.hass.callService("cover","close_cover",{entity_id:this.entity.entity_id})}_onStopTap(t){t.stopPropagation(),this.hass.callService("cover","stop_cover",{entity_id:this.entity.entity_id})}get openDisabled(){const t=!0===this.entity.attributes.assumed_state;return((void 0!==(e=this.entity).attributes.current_position?100===e.attributes.current_position:"open"===e.state)||function(t){return"opening"===t.state}(this.entity))&&!t;var e}get closedDisabled(){const t=!0===this.entity.attributes.assumed_state;return((void 0!==(e=this.entity).attributes.current_position?0===e.attributes.current_position:"closed"===e.state)||function(t){return"closing"===t.state}(this.entity))&&!t;var e}render(){const t=Ee(this.hass);return B` - ${Nt(this.entity,2)?N` + ${Bt(this.entity,2)?B` {switch(t.attributes.device_class){case"awning":case"curtain":case"door":case"gate":return"mdi:arrow-collapse-horizontal";default:return"mdi:arrow-down"}})(this.entity)} - .disabled=${!Dt(this.entity)||this.closedDisabled} + .disabled=${!Rt(this.entity)||this.closedDisabled} @click=${this._onCloseTap} > `:void 0} - ${Nt(this.entity,8)?N` + ${Bt(this.entity,8)?B` `:void 0} - ${Nt(this.entity,1)?N` + ${Bt(this.entity,1)?B` {switch(t.attributes.device_class){case"awning":case"curtain":case"door":case"gate":return"mdi:arrow-expand-horizontal";default:return"mdi:arrow-up"}})(this.entity)} - .disabled=${!Dt(this.entity)||this.openDisabled} + .disabled=${!Rt(this.entity)||this.openDisabled} @click=${this._onOpenTap} > `:void 0} - `}};n([st({attribute:!1})],cs.prototype,"hass",void 0),n([st({attribute:!1})],cs.prototype,"entity",void 0),n([st()],cs.prototype,"fill",void 0),cs=n([at("mushroom-cover-buttons-control")],cs);var ds; + `}};n([ht({attribute:!1})],Is.prototype,"hass",void 0),n([ht({attribute:!1})],Is.prototype,"entity",void 0),n([ht()],Is.prototype,"fill",void 0),Is=n([dt("mushroom-cover-buttons-control")],Is);var Ts,zs={},Os={get exports(){return zs},set exports(t){zs=t}}; /*! Hammer.JS - v2.0.7 - 2016-04-22 * http://hammerjs.github.io/ * * Copyright (c) 2016 Jorik Tangelder; - * Licensed under the MIT license */ds={exports:{}},function(t,e,i,n){var o,r=["","webkit","Moz","MS","ms","o"],a=e.createElement("div"),l=Math.round,s=Math.abs,c=Date.now;function d(t,e,i){return setTimeout(_(t,i),e)}function u(t,e,i){return!!Array.isArray(t)&&(h(t,i[e],i),!0)}function h(t,e,i){var o;if(t)if(t.forEach)t.forEach(e,i);else if(t.length!==n)for(o=0;o\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",r=t.console&&(t.console.warn||t.console.log);return r&&r.call(t.console,o,n),e.apply(this,arguments)}}o="function"!=typeof Object.assign?function(t){if(t===n||null===t)throw new TypeError("Cannot convert undefined or null to object");for(var e=Object(t),i=1;i-1}function C(t){return t.trim().split(/\s+/g)}function $(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var n=0;ni[e]})):n.sort()),n}function S(t,e){for(var i,o,a=e[0].toUpperCase()+e.slice(1),l=0;l1&&!i.firstMultiple?i.firstMultiple=F(e):1===r&&(i.firstMultiple=!1);var a=i.firstInput,l=i.firstMultiple,d=l?l.center:a.center,u=e.center=B(o);e.timeStamp=c(),e.deltaTime=e.timeStamp-a.timeStamp,e.angle=X(d,u),e.distance=Y(d,u),function(t,e){var i=e.center,n=t.offsetDelta||{},o=t.prevDelta||{},r=t.prevInput||{};1!==e.eventType&&4!==r.eventType||(o=t.prevDelta={x:r.deltaX||0,y:r.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=H(e.deltaX,e.deltaY);var h,m,p=U(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=p.x,e.overallVelocityY=p.y,e.overallVelocity=s(p.x)>s(p.y)?p.x:p.y,e.scale=l?(h=l.pointers,Y((m=o)[0],m[1],N)/Y(h[0],h[1],N)):1,e.rotation=l?function(t,e){return X(e[1],e[0],N)+X(t[1],t[0],N)}(l.pointers,o):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,o,r,a,l=t.lastInterval||e,c=e.timeStamp-l.timeStamp;if(8!=e.eventType&&(c>25||l.velocity===n)){var d=e.deltaX-l.deltaX,u=e.deltaY-l.deltaY,h=U(c,d,u);o=h.x,r=h.y,i=s(h.x)>s(h.y)?h.x:h.y,a=H(d,u),t.lastInterval=e}else i=l.velocity,o=l.velocityX,r=l.velocityY,a=l.direction;e.velocity=i,e.velocityX=o,e.velocityY=r,e.direction=a}(i,e);var f=t.element;w(e.srcEvent.target,f)&&(f=e.srcEvent.target),e.target=f}(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function F(t){for(var e=[],i=0;i=s(e)?t<0?2:4:e<0?8:16}function Y(t,e,i){i||(i=P);var n=e[i[0]]-t[i[0]],o=e[i[1]]-t[i[1]];return Math.sqrt(n*n+o*o)}function X(t,e,i){i||(i=P);var n=e[i[0]]-t[i[0]],o=e[i[1]]-t[i[1]];return 180*Math.atan2(o,n)/Math.PI}V.prototype={handler:function(){},init:function(){this.evEl&&y(this.element,this.evEl,this.domHandler),this.evTarget&&y(this.target,this.evTarget,this.domHandler),this.evWin&&y(T(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&x(this.element,this.evEl,this.domHandler),this.evTarget&&x(this.target,this.evTarget,this.domHandler),this.evWin&&x(T(this.element),this.evWin,this.domHandler)}};var W={mousedown:1,mousemove:2,mouseup:4},q="mousedown",K="mousemove mouseup";function G(){this.evEl=q,this.evWin=K,this.pressed=!1,V.apply(this,arguments)}g(G,V,{handler:function(t){var e=W[t.type];1&e&&0===t.button&&(this.pressed=!0),2&e&&1!==t.which&&(e=4),this.pressed&&(4&e&&(this.pressed=!1),this.callback(this.manager,e,{pointers:[t],changedPointers:[t],pointerType:D,srcEvent:t}))}});var Z={pointerdown:1,pointermove:2,pointerup:4,pointercancel:8,pointerout:8},J={2:L,3:"pen",4:D,5:"kinect"},Q="pointerdown",tt="pointermove pointerup pointercancel";function et(){this.evEl=Q,this.evWin=tt,V.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}t.MSPointerEvent&&!t.PointerEvent&&(Q="MSPointerDown",tt="MSPointerMove MSPointerUp MSPointerCancel"),g(et,V,{handler:function(t){var e=this.store,i=!1,n=t.type.toLowerCase().replace("ms",""),o=Z[n],r=J[t.pointerType]||t.pointerType,a=r==L,l=$(e,t.pointerId,"pointerId");1&o&&(0===t.button||a)?l<0&&(e.push(t),l=e.length-1):12&o&&(i=!0),l<0||(e[l]=t,this.callback(this.manager,o,{pointers:e,changedPointers:[t],pointerType:r,srcEvent:t}),i&&e.splice(l,1))}});var it={touchstart:1,touchmove:2,touchend:4,touchcancel:8},nt="touchstart",ot="touchstart touchmove touchend touchcancel";function rt(){this.evTarget=nt,this.evWin=ot,this.started=!1,V.apply(this,arguments)}function at(t,e){var i=E(t.touches),n=E(t.changedTouches);return 12&e&&(i=A(i.concat(n),"identifier",!0)),[i,n]}g(rt,V,{handler:function(t){var e=it[t.type];if(1===e&&(this.started=!0),this.started){var i=at.call(this,t,e);12&e&&i[0].length-i[1].length==0&&(this.started=!1),this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:L,srcEvent:t})}}});var lt={touchstart:1,touchmove:2,touchend:4,touchcancel:8},st="touchstart touchmove touchend touchcancel";function ct(){this.evTarget=st,this.targetIds={},V.apply(this,arguments)}function dt(t,e){var i=E(t.touches),n=this.targetIds;if(3&e&&1===i.length)return n[i[0].identifier]=!0,[i,i];var o,r,a=E(t.changedTouches),l=[],s=this.target;if(r=i.filter((function(t){return w(t.target,s)})),1===e)for(o=0;o-1&&n.splice(t,1)}),2500)}}function pt(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,n=0;n-1&&this.requireFail.splice(e,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(t){return!!this.simultaneous[t.id]},emit:function(t){var e=this,i=this.state;function n(i){e.manager.emit(i,t)}i<8&&n(e.options.event+At(i)),n(e.options.event),t.additionalEvent&&n(t.additionalEvent),i>=8&&n(e.options.event+At(i))},tryEmit:function(t){if(this.canEmit())return this.emit(t);this.state=$t},canEmit:function(){for(var t=0;te.threshold&&o&e.direction},attrTest:function(t){return Tt.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},emit:function(t){this.pX=t.deltaX,this.pY=t.deltaY;var e=St(t.direction);e&&(t.additionalEvent=this.options.event+e),this._super.emit.call(this,t)}}),g(Ot,Tt,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[yt]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.scale-1)>this.options.threshold||2&this.state)},emit:function(t){if(1!==t.scale){var e=t.scale<1?"in":"out";t.additionalEvent=this.options.event+e}this._super.emit.call(this,t)}}),g(Mt,Et,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[vt]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,n=t.distancee.time;if(this._input=t,!n||!i||12&t.eventType&&!o)this.reset();else if(1&t.eventType)this.reset(),this._timer=d((function(){this.state=8,this.tryEmit()}),e.time,this);else if(4&t.eventType)return 8;return $t},reset:function(){clearTimeout(this._timer)},emit:function(t){8===this.state&&(t&&4&t.eventType?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=c(),this.manager.emit(this.options.event,this._input)))}}),g(Lt,Tt,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[yt]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.rotation)>this.options.threshold||2&this.state)}}),g(Dt,Tt,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:30,pointers:1},getTouchAction:function(){return zt.prototype.getTouchAction.call(this)},attrTest:function(t){var e,i=this.options.direction;return 30&i?e=t.overallVelocity:6&i?e=t.overallVelocityX:i&j&&(e=t.overallVelocityY),this._super.attrTest.call(this,t)&&i&t.offsetDirection&&t.distance>this.options.threshold&&t.maxPointers==this.options.pointers&&s(e)>this.options.velocity&&4&t.eventType},emit:function(t){var e=St(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)}}),g(jt,Et,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[bt]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,n=t.distance{const e=t.center.x,i=t.target.getBoundingClientRect().left,n=t.target.clientWidth;return Math.max(Math.min(1,(e-i)/n),0)};let hs=class extends ot{constructor(){super(...arguments),this.disabled=!1,this.inactive=!1,this.step=1,this.min=0,this.max=100,this.controlled=!1}valueToPercentage(t){return(t-this.min)/(this.max-this.min)}percentageToValue(t){return(this.max-this.min)*t+this.min}firstUpdated(t){super.firstUpdated(t),this.setupListeners()}connectedCallback(){super.connectedCallback(),this.setupListeners()}disconnectedCallback(){super.disconnectedCallback(),this.destroyListeners()}setupListeners(){if(this.slider&&!this._mc){const t=(t=>{const e=window.getComputedStyle(t).getPropertyValue("--slider-threshold"),i=parseFloat(e);return isNaN(i)?10:i})(this.slider);let e;this._mc=new Hammer.Manager(this.slider,{touchAction:"pan-y"}),this._mc.add(new Hammer.Pan({threshold:t,direction:Hammer.DIRECTION_ALL,enable:!0})),this._mc.add(new Hammer.Tap({event:"singletap"})),this._mc.on("panstart",(()=>{this.disabled||(this.controlled=!0,e=this.value)})),this._mc.on("pancancel",(()=>{this.disabled||(this.controlled=!1,this.value=e)})),this._mc.on("panmove",(t=>{if(this.disabled)return;const e=us(t);this.value=this.percentageToValue(e),this.dispatchEvent(new CustomEvent("current-change",{detail:{value:Math.round(this.value/this.step)*this.step}}))})),this._mc.on("panend",(t=>{if(this.disabled)return;this.controlled=!1;const e=us(t);this.value=Math.round(this.percentageToValue(e)/this.step)*this.step,this.dispatchEvent(new CustomEvent("current-change",{detail:{value:void 0}})),this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value}}))})),this._mc.on("singletap",(t=>{if(this.disabled)return;const e=us(t);this.value=Math.round(this.percentageToValue(e)/this.step)*this.step,this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value}}))}))}}destroyListeners(){this._mc&&(this._mc.destroy(),this._mc=void 0)}render(){var t;return N` + * Licensed under the MIT license */ +Ts=Os,function(t,e,i,o){var n,r=["","webkit","Moz","MS","ms","o"],a=e.createElement("div"),l=Math.round,s=Math.abs,c=Date.now;function d(t,e,i){return setTimeout(_(t,i),e)}function u(t,e,i){return!!Array.isArray(t)&&(h(t,i[e],i),!0)}function h(t,e,i){var n;if(t)if(t.forEach)t.forEach(e,i);else if(t.length!==o)for(n=0;n\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",r=t.console&&(t.console.warn||t.console.log);return r&&r.call(t.console,n,o),e.apply(this,arguments)}}n="function"!=typeof Object.assign?function(t){if(t===o||null===t)throw new TypeError("Cannot convert undefined or null to object");for(var e=Object(t),i=1;i-1}function C(t){return t.trim().split(/\s+/g)}function $(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var o=0;oi[e]})):o.sort()),o}function S(t,e){for(var i,n,a=e[0].toUpperCase()+e.slice(1),l=0;l1&&!i.firstMultiple?i.firstMultiple=F(e):1===r&&(i.firstMultiple=!1);var a=i.firstInput,l=i.firstMultiple,d=l?l.center:a.center,u=e.center=B(n);e.timeStamp=c(),e.deltaTime=e.timeStamp-a.timeStamp,e.angle=X(d,u),e.distance=Y(d,u),function(t,e){var i=e.center,o=t.offsetDelta||{},n=t.prevDelta||{},r=t.prevInput||{};1!==e.eventType&&4!==r.eventType||(n=t.prevDelta={x:r.deltaX||0,y:r.deltaY||0},o=t.offsetDelta={x:i.x,y:i.y}),e.deltaX=n.x+(i.x-o.x),e.deltaY=n.y+(i.y-o.y)}(i,e),e.offsetDirection=H(e.deltaX,e.deltaY);var h,m,p=U(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=p.x,e.overallVelocityY=p.y,e.overallVelocity=s(p.x)>s(p.y)?p.x:p.y,e.scale=l?(h=l.pointers,Y((m=n)[0],m[1],N)/Y(h[0],h[1],N)):1,e.rotation=l?function(t,e){return X(e[1],e[0],N)+X(t[1],t[0],N)}(l.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,r,a,l=t.lastInterval||e,c=e.timeStamp-l.timeStamp;if(8!=e.eventType&&(c>25||l.velocity===o)){var d=e.deltaX-l.deltaX,u=e.deltaY-l.deltaY,h=U(c,d,u);n=h.x,r=h.y,i=s(h.x)>s(h.y)?h.x:h.y,a=H(d,u),t.lastInterval=e}else i=l.velocity,n=l.velocityX,r=l.velocityY,a=l.direction;e.velocity=i,e.velocityX=n,e.velocityY=r,e.direction=a}(i,e);var f=t.element;w(e.srcEvent.target,f)&&(f=e.srcEvent.target),e.target=f}(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function F(t){for(var e=[],i=0;i=s(e)?t<0?2:4:e<0?8:16}function Y(t,e,i){i||(i=P);var o=e[i[0]]-t[i[0]],n=e[i[1]]-t[i[1]];return Math.sqrt(o*o+n*n)}function X(t,e,i){i||(i=P);var o=e[i[0]]-t[i[0]],n=e[i[1]]-t[i[1]];return 180*Math.atan2(n,o)/Math.PI}R.prototype={handler:function(){},init:function(){this.evEl&&y(this.element,this.evEl,this.domHandler),this.evTarget&&y(this.target,this.evTarget,this.domHandler),this.evWin&&y(T(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&x(this.element,this.evEl,this.domHandler),this.evTarget&&x(this.target,this.evTarget,this.domHandler),this.evWin&&x(T(this.element),this.evWin,this.domHandler)}};var W={mousedown:1,mousemove:2,mouseup:4},q="mousedown",K="mousemove mouseup";function G(){this.evEl=q,this.evWin=K,this.pressed=!1,R.apply(this,arguments)}g(G,R,{handler:function(t){var e=W[t.type];1&e&&0===t.button&&(this.pressed=!0),2&e&&1!==t.which&&(e=4),this.pressed&&(4&e&&(this.pressed=!1),this.callback(this.manager,e,{pointers:[t],changedPointers:[t],pointerType:L,srcEvent:t}))}});var Z={pointerdown:1,pointermove:2,pointerup:4,pointercancel:8,pointerout:8},J={2:D,3:"pen",4:L,5:"kinect"},Q="pointerdown",tt="pointermove pointerup pointercancel";function et(){this.evEl=Q,this.evWin=tt,R.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}t.MSPointerEvent&&!t.PointerEvent&&(Q="MSPointerDown",tt="MSPointerMove MSPointerUp MSPointerCancel"),g(et,R,{handler:function(t){var e=this.store,i=!1,o=t.type.toLowerCase().replace("ms",""),n=Z[o],r=J[t.pointerType]||t.pointerType,a=r==D,l=$(e,t.pointerId,"pointerId");1&n&&(0===t.button||a)?l<0&&(e.push(t),l=e.length-1):12&n&&(i=!0),l<0||(e[l]=t,this.callback(this.manager,n,{pointers:e,changedPointers:[t],pointerType:r,srcEvent:t}),i&&e.splice(l,1))}});var it={touchstart:1,touchmove:2,touchend:4,touchcancel:8},ot="touchstart",nt="touchstart touchmove touchend touchcancel";function rt(){this.evTarget=ot,this.evWin=nt,this.started=!1,R.apply(this,arguments)}function at(t,e){var i=E(t.touches),o=E(t.changedTouches);return 12&e&&(i=A(i.concat(o),"identifier",!0)),[i,o]}g(rt,R,{handler:function(t){var e=it[t.type];if(1===e&&(this.started=!0),this.started){var i=at.call(this,t,e);12&e&&i[0].length-i[1].length==0&&(this.started=!1),this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:D,srcEvent:t})}}});var lt={touchstart:1,touchmove:2,touchend:4,touchcancel:8},st="touchstart touchmove touchend touchcancel";function ct(){this.evTarget=st,this.targetIds={},R.apply(this,arguments)}function dt(t,e){var i=E(t.touches),o=this.targetIds;if(3&e&&1===i.length)return o[i[0].identifier]=!0,[i,i];var n,r,a=E(t.changedTouches),l=[],s=this.target;if(r=i.filter((function(t){return w(t.target,s)})),1===e)for(n=0;n-1&&o.splice(t,1)}),2500)}}function pt(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,o=0;o-1&&this.requireFail.splice(e,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(t){return!!this.simultaneous[t.id]},emit:function(t){var e=this,i=this.state;function o(i){e.manager.emit(i,t)}i<8&&o(e.options.event+At(i)),o(e.options.event),t.additionalEvent&&o(t.additionalEvent),i>=8&&o(e.options.event+At(i))},tryEmit:function(t){if(this.canEmit())return this.emit(t);this.state=$t},canEmit:function(){for(var t=0;te.threshold&&n&e.direction},attrTest:function(t){return Tt.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},emit:function(t){this.pX=t.deltaX,this.pY=t.deltaY;var e=St(t.direction);e&&(t.additionalEvent=this.options.event+e),this._super.emit.call(this,t)}}),g(Ot,Tt,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[yt]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.scale-1)>this.options.threshold||2&this.state)},emit:function(t){if(1!==t.scale){var e=t.scale<1?"in":"out";t.additionalEvent=this.options.event+e}this._super.emit.call(this,t)}}),g(Mt,Et,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[vt]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,o=t.distancee.time;if(this._input=t,!o||!i||12&t.eventType&&!n)this.reset();else if(1&t.eventType)this.reset(),this._timer=d((function(){this.state=8,this.tryEmit()}),e.time,this);else if(4&t.eventType)return 8;return $t},reset:function(){clearTimeout(this._timer)},emit:function(t){8===this.state&&(t&&4&t.eventType?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=c(),this.manager.emit(this.options.event,this._input)))}}),g(Dt,Tt,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[yt]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.rotation)>this.options.threshold||2&this.state)}}),g(Lt,Tt,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:30,pointers:1},getTouchAction:function(){return zt.prototype.getTouchAction.call(this)},attrTest:function(t){var e,i=this.options.direction;return 30&i?e=t.overallVelocity:6&i?e=t.overallVelocityX:i&j&&(e=t.overallVelocityY),this._super.attrTest.call(this,t)&&i&t.offsetDirection&&t.distance>this.options.threshold&&t.maxPointers==this.options.pointers&&s(e)>this.options.velocity&&4&t.eventType},emit:function(t){var e=St(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)}}),g(jt,Et,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[bt]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,o=t.distance{const e=t.center.x,i=t.target.getBoundingClientRect().left,o=t.target.clientWidth;return Math.max(Math.min(1,(e-i)/o),0)};let Ds=class extends st{constructor(){super(...arguments),this.disabled=!1,this.inactive=!1,this.step=1,this.min=0,this.max=100,this.controlled=!1}valueToPercentage(t){return(t-this.min)/(this.max-this.min)}percentageToValue(t){return(this.max-this.min)*t+this.min}firstUpdated(t){super.firstUpdated(t),this.setupListeners()}connectedCallback(){super.connectedCallback(),this.setupListeners()}disconnectedCallback(){super.disconnectedCallback(),this.destroyListeners()}setupListeners(){if(this.slider&&!this._mc){const t=(t=>{const e=window.getComputedStyle(t).getPropertyValue("--slider-threshold"),i=parseFloat(e);return isNaN(i)?10:i})(this.slider);let e;this._mc=new Hammer.Manager(this.slider,{touchAction:"pan-y"}),this._mc.add(new Hammer.Pan({threshold:t,direction:Hammer.DIRECTION_ALL,enable:!0})),this._mc.add(new Hammer.Tap({event:"singletap"})),this._mc.on("panstart",(()=>{this.disabled||(this.controlled=!0,e=this.value)})),this._mc.on("pancancel",(()=>{this.disabled||(this.controlled=!1,this.value=e)})),this._mc.on("panmove",(t=>{if(this.disabled)return;const e=Ms(t);this.value=this.percentageToValue(e),this.dispatchEvent(new CustomEvent("current-change",{detail:{value:Math.round(this.value/this.step)*this.step}}))})),this._mc.on("panend",(t=>{if(this.disabled)return;this.controlled=!1;const e=Ms(t);this.value=Math.round(this.percentageToValue(e)/this.step)*this.step,this.dispatchEvent(new CustomEvent("current-change",{detail:{value:void 0}})),this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value}}))})),this._mc.on("singletap",(t=>{if(this.disabled)return;const e=Ms(t);this.value=Math.round(this.percentageToValue(e)/this.step)*this.step,this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value}}))}))}}destroyListeners(){this._mc&&(this._mc.destroy(),this._mc=void 0)}render(){var t;return B`
- ${this.showActive?N`
`:null} - ${this.showIndicator?N`
`:null} + ${this.showActive?B`
`:null} + ${this.showIndicator?B`
`:null}
- `}static get styles(){return d` + `}static get styles(){return h` :host { --main-color: rgba(var(--rgb-secondary-text-color), 1); --bg-gradient: none; @@ -2108,86 +2109,86 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .controlled .slider .slider-track-indicator { transition: none; } - `}};function ms(t){return null!=t.attributes.current_position?Math.round(t.attributes.current_position):void 0}function ps(t){const e=t.state;return"open"===e||"opening"===e?"var(--rgb-state-cover-open)":"closed"===e||"closing"===e?"var(--rgb-state-cover-closed)":"var(--rgb-disabled)"}n([st({type:Boolean})],hs.prototype,"disabled",void 0),n([st({type:Boolean})],hs.prototype,"inactive",void 0),n([st({type:Boolean,attribute:"show-active"})],hs.prototype,"showActive",void 0),n([st({type:Boolean,attribute:"show-indicator"})],hs.prototype,"showIndicator",void 0),n([st({attribute:!1,type:Number,reflect:!0})],hs.prototype,"value",void 0),n([st({type:Number})],hs.prototype,"step",void 0),n([st({type:Number})],hs.prototype,"min",void 0),n([st({type:Number})],hs.prototype,"max",void 0),n([ct()],hs.prototype,"controlled",void 0),n([ht("#slider")],hs.prototype,"slider",void 0),hs=n([at("mushroom-slider")],hs);let fs=class extends ot{onChange(t){const e=t.detail.value;this.hass.callService("cover","set_cover_position",{entity_id:this.entity.entity_id,position:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=ms(this.entity);return N` + `}};function Ls(t){return null!=t.attributes.current_position?Math.round(t.attributes.current_position):void 0}function js(t){const e=t.state;return"open"===e||"opening"===e?"var(--rgb-state-cover-open)":"closed"===e||"closing"===e?"var(--rgb-state-cover-closed)":"var(--rgb-disabled)"}n([ht({type:Boolean})],Ds.prototype,"disabled",void 0),n([ht({type:Boolean})],Ds.prototype,"inactive",void 0),n([ht({type:Boolean,attribute:"show-active"})],Ds.prototype,"showActive",void 0),n([ht({type:Boolean,attribute:"show-indicator"})],Ds.prototype,"showIndicator",void 0),n([ht({attribute:!1,type:Number,reflect:!0})],Ds.prototype,"value",void 0),n([ht({type:Number})],Ds.prototype,"step",void 0),n([ht({type:Number})],Ds.prototype,"min",void 0),n([ht({type:Number})],Ds.prototype,"max",void 0),n([mt()],Ds.prototype,"controlled",void 0),n([gt("#slider")],Ds.prototype,"slider",void 0),Ds=n([dt("mushroom-slider")],Ds);let Ps=class extends st{onChange(t){const e=t.detail.value;this.hass.callService("cover","set_cover_position",{entity_id:this.entity.entity_id,position:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=Ls(this.entity);return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-slider { --main-color: var(--slider-color); --bg-color: var(--slider-bg-color); } - `}};n([st({attribute:!1})],fs.prototype,"hass",void 0),n([st({attribute:!1})],fs.prototype,"entity",void 0),fs=n([at("mushroom-cover-position-control")],fs);const gs=function(t=24,e=.2){const i=[];for(let n=0;n - `}static get styles(){const t=gs.map((([t,e])=>`${e} ${100*t}%`)).join(", ");return d` + `}static get styles(){const t=Ns.map((([t,e])=>`${e} ${100*t}%`)).join(", ");return h` mushroom-slider { --main-color: var(--slider-color); --bg-color: var(--slider-bg-color); - --gradient: -webkit-linear-gradient(left, ${c(t)}); + --gradient: -webkit-linear-gradient(left, ${u(t)}); } - `}};n([st({attribute:!1})],_s.prototype,"hass",void 0),n([st({attribute:!1})],_s.prototype,"entity",void 0),_s=n([at("mushroom-cover-tilt-position-control")],_s);const vs={buttons_control:"mdi:gesture-tap-button",position_control:"mdi:gesture-swipe-horizontal",tilt_position_control:"mdi:rotate-right"};ll({type:"mushroom-cover-card",name:"Mushroom Cover Card",description:"Card for cover entity"});let bs=class extends rl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return Sm})),document.createElement("mushroom-cover-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>ss.includes(t.split(".")[0])));return{type:"custom:mushroom-cover-card",entity:e[0]}}get _nextControl(){var t;if(this._activeControl)return null!==(t=this._controls[this._controls.indexOf(this._activeControl)+1])&&void 0!==t?t:this._controls[0]}_onNextControlTap(t){t.stopPropagation(),this._activeControl=this._nextControl}getCardSize(){return 1}setConfig(t){var e,i,n;this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t);const o=[];(null===(e=this._config)||void 0===e?void 0:e.show_buttons_control)&&o.push("buttons_control"),(null===(i=this._config)||void 0===i?void 0:i.show_position_control)&&o.push("position_control"),(null===(n=this._config)||void 0===n?void 0:n.show_tilt_position_control)&&o.push("tilt_position_control"),this._controls=o,this._activeControl=o[0],this.updatePosition()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updatePosition()}updatePosition(){if(this.position=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.position=ms(e))}onCurrentPositionChange(t){null!=t.detail.value&&(this.position=t.detail.value)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type);let a=Ht(this.hass.localize,e,this.hass.locale);this.position&&(a+=` - ${this.position}%`);const l=pe(this.hass);return N` - - + `}};n([ht({attribute:!1})],Rs.prototype,"hass",void 0),n([ht({attribute:!1})],Rs.prototype,"entity",void 0),Rs=n([dt("mushroom-cover-tilt-position-control")],Rs);const Vs={buttons_control:"mdi:gesture-tap-button",position_control:"mdi:gesture-swipe-horizontal",tilt_position_control:"mdi:rotate-right"};$l({type:"mushroom-cover-card",name:"Mushroom Cover Card",description:"Card for cover entity"});let Fs=class extends kl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return Fm})),document.createElement("mushroom-cover-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ss.includes(t.split(".")[0])));return{type:"custom:mushroom-cover-card",entity:e[0]}}get _nextControl(){var t;if(this._activeControl)return null!==(t=this._controls[this._controls.indexOf(this._activeControl)+1])&&void 0!==t?t:this._controls[0]}_onNextControlTap(t){t.stopPropagation(),this._activeControl=this._nextControl}getCardSize(){return 1}setConfig(t){var e,i,o;this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t);const n=[];(null===(e=this._config)||void 0===e?void 0:e.show_buttons_control)&&n.push("buttons_control"),(null===(i=this._config)||void 0===i?void 0:i.show_position_control)&&n.push("position_control"),(null===(o=this._config)||void 0===o?void 0:o.show_tilt_position_control)&&n.push("tilt_position_control"),this._controls=n,this._activeControl=n[0],this.updatePosition()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updatePosition()}updatePosition(){if(this.position=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.position=Ls(e))}onCurrentPositionChange(t){null!=t.detail.value&&(this.position=t.detail.value)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this.hass||!this._config||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type);let a=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);this.position&&(a+=` - ${this.position}${te(this.hass.locale)}%`);const l=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i,a)}; + ${this.renderStateInfo(e,n,i,a)}; - ${this._controls.length>0?N` + ${this._controls.length>0?B`
- ${this.renderActiveControl(e,o.layout)} + ${this.renderActiveControl(e,n.layout)} ${this.renderNextControlButton()}
`:null}
- `}renderIcon(t,e){const i={},n=Dt(t),o=ps(t);return i["--icon-color"]=`rgb(${o})`,i["--shape-color"]=`rgba(${o}, 0.2)`,N` + `}renderIcon(t,e){const i={},o=Rt(t),n=js(t);return i["--icon-color"]=`rgb(${n})`,i["--shape-color"]=`rgba(${n}, 0.2)`,B` - `}renderNextControlButton(){return this._nextControl&&this._nextControl!=this._activeControl?N` + `}renderNextControlButton(){return this._nextControl&&this._nextControl!=this._activeControl?B` - `:null}renderActiveControl(t,e){switch(this._activeControl){case"buttons_control":return N` + `:null}renderActiveControl(t,e){switch(this._activeControl){case"buttons_control":return B` - `;case"position_control":{const e=ps(t),i={};return i["--slider-color"]=`rgb(${e})`,i["--slider-bg-color"]=`rgba(${e}, 0.2)`,N` + `;case"position_control":{const e=js(t),i={};return i["--slider-color"]=`rgb(${e})`,i["--slider-bg-color"]=`rgba(${e}, 0.2)`,B` - `}case"tilt_position_control":{const e=ps(t),i={};return i["--slider-color"]=`rgb(${e})`,i["--slider-bg-color"]=`rgba(${e}, 0.2)`,N` + `}case"tilt_position_control":{const e=js(t),i={};return i["--slider-color"]=`rgb(${e})`,i["--slider-bg-color"]=`rgba(${e}, 0.2)`,B` - `}default:return null}}static get styles(){return[super.styles,al,d` + `}default:return null}}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2202,29 +2203,29 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-cover-tilt-position-control { flex: 1; } - `]}};n([ct()],bs.prototype,"_config",void 0),n([ct()],bs.prototype,"_activeControl",void 0),n([ct()],bs.prototype,"_controls",void 0),n([ct()],bs.prototype,"position",void 0),bs=n([at("mushroom-cover-card")],bs);ll({type:"mushroom-entity-card",name:"Mushroom Entity Card",description:"Card for all entities"});let ys=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Om})),document.createElement("mushroom-entity-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-entity-card",entity:Object.keys(t.states)[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type),a=pe(this.hass);return N` - - + `]}};n([mt()],Fs.prototype,"_config",void 0),n([mt()],Fs.prototype,"_activeControl",void 0),n([mt()],Fs.prototype,"_controls",void 0),n([mt()],Fs.prototype,"position",void 0),Fs=n([dt("mushroom-cover-card")],Fs);$l({type:"mushroom-entity-card",name:"Mushroom Entity Card",description:"Card for all entities"});let Bs=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return Ym})),document.createElement("mushroom-entity-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-entity-card",entity:Object.keys(t.states)[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type),a=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i)}; + ${this.renderStateInfo(e,n,i)}; - `}renderIcon(t,e){var i;const n=Lt(t),o={},r=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(r){const t=xa(r);o["--icon-color"]=`rgb(${t})`,o["--shape-color"]=`rgba(${t}, 0.2)`}return N` + `}renderIcon(t,e){var i;const o=Nt(t),n={},r=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(r){const t=Ra(r);n["--icon-color"]=`rgb(${t})`,n["--shape-color"]=`rgba(${t}, 0.2)`}return B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2232,14 +2233,14 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --icon-color: rgb(var(--rgb-state-entity)); --shape-color: rgba(var(--rgb-state-entity), 0.2); } - `]}};n([ct()],ys.prototype,"_config",void 0),ys=n([at("mushroom-entity-card")],ys);const xs=["fan"];function ws(t){return null!=t.attributes.percentage?Math.round(t.attributes.percentage):void 0}function ks(t){return null!=t.attributes.oscillating&&Boolean(t.attributes.oscillating)}let Cs=class extends ot{_onTap(t){t.stopPropagation();const e=ks(this.entity);this.hass.callService("fan","oscillate",{entity_id:this.entity.entity_id,oscillating:!e})}render(){const t=ks(this.entity),e=Lt(this.entity);return N` + `]}};n([mt()],Bs.prototype,"_config",void 0),Bs=n([dt("mushroom-entity-card")],Bs);const Us=["fan"];function Hs(t){return null!=t.attributes.percentage?Math.round(t.attributes.percentage):void 0}function Ys(t){return null!=t.attributes.oscillating&&Boolean(t.attributes.oscillating)}let Xs=class extends st{_onTap(t){t.stopPropagation();const e=Ys(this.entity);this.hass.callService("fan","oscillate",{entity_id:this.entity.entity_id,oscillating:!e})}render(){const t=Ys(this.entity),e=Nt(this.entity);return B` - `}static get styles(){return d` + `}static get styles(){return h` :host { display: flex; } @@ -2247,44 +2248,44 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --icon-color: rgb(var(--rgb-state-fan)); --bg-color: rgba(var(--rgb-state-fan), 0.2); } - `}};n([st({attribute:!1})],Cs.prototype,"hass",void 0),n([st({attribute:!1})],Cs.prototype,"entity",void 0),Cs=n([at("mushroom-fan-oscillate-control")],Cs);let $s=class extends ot{onChange(t){const e=t.detail.value;this.hass.callService("fan","set_percentage",{entity_id:this.entity.entity_id,percentage:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=ws(this.entity);return N` + `}};n([ht({attribute:!1})],Xs.prototype,"hass",void 0),n([ht({attribute:!1})],Xs.prototype,"entity",void 0),Xs=n([dt("mushroom-fan-oscillate-control")],Xs);let Ws=class extends st{onChange(t){const e=t.detail.value;this.hass.callService("fan","set_percentage",{entity_id:this.entity.entity_id,percentage:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=Hs(this.entity);return B` - `;var e}static get styles(){return d` + `;var e}static get styles(){return h` mushroom-slider { --main-color: rgb(var(--rgb-state-fan)); --bg-color: rgba(var(--rgb-state-fan), 0.2); } - `}};n([st({attribute:!1})],$s.prototype,"hass",void 0),n([st({attribute:!1})],$s.prototype,"entity",void 0),$s=n([at("mushroom-fan-percentage-control")],$s),ll({type:"mushroom-fan-card",name:"Mushroom Fan Card",description:"Card for fan entity"});let Es=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Pm})),document.createElement("mushroom-fan-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>xs.includes(t.split(".")[0])));return{type:"custom:mushroom-fan-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updatePercentage()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updatePercentage()}updatePercentage(){if(this.percentage=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.percentage=ws(e))}onCurrentPercentageChange(t){null!=t.detail.value&&(this.percentage=Math.round(t.detail.value))}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type);let a=Ht(this.hass.localize,e,this.hass.locale);null!=this.percentage&&(a=`${this.percentage}%`);const l=pe(this.hass),s=(!this._config.collapsible_controls||Lt(e))&&(this._config.show_percentage_control||this._config.show_oscillate_control);return N` - - + `}};n([ht({attribute:!1})],Ws.prototype,"hass",void 0),n([ht({attribute:!1})],Ws.prototype,"entity",void 0),Ws=n([dt("mushroom-fan-percentage-control")],Ws),$l({type:"mushroom-fan-card",name:"Mushroom Fan Card",description:"Card for fan entity"});let qs=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return Gm})),document.createElement("mushroom-fan-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Us.includes(t.split(".")[0])));return{type:"custom:mushroom-fan-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updatePercentage()}updated(t){super.updated(t),this.hass&&t.has("hass")&&this.updatePercentage()}updatePercentage(){if(this.percentage=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.percentage=Hs(e))}onCurrentPercentageChange(t){null!=t.detail.value&&(this.percentage=Math.round(t.detail.value))}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type);let a=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);null!=this.percentage&&(a=`${this.percentage}${te(this.hass.locale)}%`);const l=Ee(this.hass),s=(!this._config.collapsible_controls||Nt(e))&&(this._config.show_percentage_control||this._config.show_oscillate_control);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i,a)}; + ${this.renderStateInfo(e,n,i,a)}; - ${s?N` + ${s?B`
- ${this._config.show_percentage_control?N` + ${this._config.show_percentage_control?B` `:null} - ${this._config.show_oscillate_control?N` + ${this._config.show_oscillate_control?B` - `}renderIcon(t,e){var i;let n={};const o=ws(t),r=Lt(t);if(r)if(o){const t=1.5*(o/100)**.5;n["--animation-duration"]=1/t+"s"}else n["--animation-duration"]="1s";return N` + `}renderIcon(t,e){var i;let o={};const n=Hs(t),r=Nt(t);if(r)if(n){const t=1.5*(n/100)**.5;o["--animation-duration"]=1/t+"s"}else o["--animation-duration"]="1s";return B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2319,34 +2320,34 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-fan-percentage-control { flex: 1; } - `]}};n([ct()],Es.prototype,"_config",void 0),n([ct()],Es.prototype,"percentage",void 0),Es=n([at("mushroom-fan-card")],Es);const As=["humidifier"];let Ss=class extends ot{onChange(t){const e=t.detail.value;this.hass.callService("humidifier","set_humidity",{entity_id:this.entity.entity_id,humidity:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=this.entity.attributes.max_humidity||100,e=this.entity.attributes.min_humidity||0;return N``}static get styles(){return d` + />`}static get styles(){return h` mushroom-slider { --main-color: rgb(var(--rgb-state-humidifier)); --bg-color: rgba(var(--rgb-state-humidifier), 0.2); } - `}};n([st({attribute:!1})],Ss.prototype,"hass",void 0),n([st({attribute:!1})],Ss.prototype,"entity",void 0),n([st({attribute:!1})],Ss.prototype,"color",void 0),Ss=n([at("mushroom-humidifier-humidity-control")],Ss),ll({type:"mushroom-humidifier-card",name:"Mushroom Humidifier Card",description:"Card for humidifier entity"});let Is=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Bm})),document.createElement("mushroom-humidifier-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>As.includes(t.split(".")[0])));return{type:"custom:mushroom-humidifier-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}onCurrentHumidityChange(t){null!=t.detail.value&&(this.humidity=t.detail.value)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type);let a=Ht(this.hass.localize,e,this.hass.locale);this.humidity&&(a=`${this.humidity} %`);const l=pe(this.hass),s=(!this._config.collapsible_controls||Lt(e))&&this._config.show_target_humidity_control;return N` - - + `}};n([ht({attribute:!1})],Gs.prototype,"hass",void 0),n([ht({attribute:!1})],Gs.prototype,"entity",void 0),n([ht({attribute:!1})],Gs.prototype,"color",void 0),Gs=n([dt("mushroom-humidifier-humidity-control")],Gs),$l({type:"mushroom-humidifier-card",name:"Mushroom Humidifier Card",description:"Card for humidifier entity"});let Zs=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return ep})),document.createElement("mushroom-humidifier-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ks.includes(t.split(".")[0])));return{type:"custom:mushroom-humidifier-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}onCurrentHumidityChange(t){null!=t.detail.value&&(this.humidity=t.detail.value)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type);let a=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);this.humidity&&(a=`${this.humidity}${te(this.hass.locale)}%`);const l=Ee(this.hass),s=(!this._config.collapsible_controls||Nt(e))&&this._config.show_target_humidity_control;return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i,a)}; + ${this.renderStateInfo(e,n,i,a)}; - ${s?N` + ${s?B`
- `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2368,16 +2369,92 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-humidifier-humidity-control { flex: 1; } - `]}};n([ct()],Is.prototype,"_config",void 0),n([ct()],Is.prototype,"humidity",void 0),Is=n([at("mushroom-humidifier-card")],Is);const Ts=["light"];let zs=class extends ot{onChange(t){const e=t.detail.value;this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,brightness_pct:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=Bl(this.entity);return N` + `]}};n([mt()],Zs.prototype,"_config",void 0),n([mt()],Zs.prototype,"humidity",void 0),Zs=n([dt("mushroom-humidifier-card")],Zs);const Js=["number","input_number"];let Qs=class extends st{onChange(t){const e=t.detail.value,i=this.entity.entity_id.split(".")[0];this.hass.callService(i,"set_value",{entity_id:this.entity.entity_id,value:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){var t;const e=Number(this.entity.state),i=null!==(t=Jt(this.entity,this.hass.entities[this.entity.entity_id]))&&void 0!==t?t:Qt(this.entity.state);return"buttons"===this.displayMode?B` + + `:B` + + `}static get styles(){return h` + :host { + --slider-color: rgb(var(--rgb-state-number)); + --slider-outline-color: transparent; + --slider-bg-color: rgba(var(--rgb-state-number), 0.2); + } + mushroom-slider { + --main-color: var(--slider-color); + --bg-color: var(--slider-bg-color); + --main-outline-color: var(--slider-outline-color); + } + `}};n([ht({attribute:!1})],Qs.prototype,"hass",void 0),n([ht({attribute:!1})],Qs.prototype,"entity",void 0),n([ht({attribute:!1})],Qs.prototype,"displayMode",void 0),Qs=n([dt("mushroom-number-value-control")],Qs),$l({type:"mushroom-number-card",name:"Mushroom Number Card",description:"Card for number and input number entity"});let tc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return lp})),document.createElement("mushroom-number-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Js.includes(t.split(".")[0])));return{type:"custom:mushroom-number-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}onCurrentValueChange(t){null!=t.detail.value&&(this.value=t.detail.value)}render(){var t,e,i;if(!this._config||!this.hass||!this._config.entity)return B``;const o=this._config.entity,n=this.hass.states[o],r=this._config.name||n.attributes.friendly_name||"",a=this._config.icon||Dl(n),l=ml(this._config),s=qa(n,l.icon_type);let c=ee(this.hass.localize,n,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);if(void 0!==this.value){c=`${Zt(this.value,this.hass.locale,null!==(t=Jt(n,this.hass.entities[n.entity_id]))&&void 0!==t?t:Qt(n.state))} ${null!==(e=n.attributes.unit_of_measurement)&&void 0!==e?e:""}`}const d=Ee(this.hass),u={},h=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(h){const t=Ra(h);u["--slider-color"]=`rgb(${t})`,u["--slider-bg-color"]=`rgba(${t}, 0.2)`}return B` + + + + ${s?this.renderPicture(s):this.renderIcon(n,a)} + ${this.renderBadge(n)} + ${this.renderStateInfo(n,l,r,c)}; + +
+ +
+
+
+ `}renderIcon(t,e){var i;const o=Nt(t),n={},r=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(r){const t=Ra(r);n["--icon-color"]=`rgb(${t})`,n["--shape-color"]=`rgba(${t}, 0.2)`}return B` + + `}static get styles(){return[super.styles,Cl,h` + mushroom-state-item { + cursor: pointer; + } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-number)); + --shape-color: rgba(var(--rgb-state-number), 0.2); + } + mushroom-number-value-control { + flex: 1; + } + `]}};n([mt()],tc.prototype,"_config",void 0),n([mt()],tc.prototype,"value",void 0),tc=n([dt("mushroom-number-card")],tc);const ec=["light"];let ic=class extends st{onChange(t){const e=t.detail.value;this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,brightness_pct:e})}onCurrentChange(t){const e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}render(){const t=ss(this.entity);return B` - `}static get styles(){return d` + `}static get styles(){return h` :host { --slider-color: rgb(var(--rgb-state-light)); --slider-outline-color: transparent; @@ -2388,80 +2465,80 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --bg-color: var(--slider-bg-color); --main-outline-color: var(--slider-outline-color); } - `}};n([st({attribute:!1})],zs.prototype,"hass",void 0),n([st({attribute:!1})],zs.prototype,"entity",void 0),zs=n([at("mushroom-light-brightness-control")],zs);const Os=[[0,"#f00"],[.17,"#ff0"],[.33,"#0f0"],[.5,"#0ff"],[.66,"#00f"],[.83,"#f0f"],[1,"#f00"]];let Ms=class extends ot{constructor(){super(...arguments),this._percent=0}_percentToRGB(t){return ba.hsv(360*t,100,100).rgb().array()}_rgbToPercent(t){return ba.rgb(t).hsv().hue()/360}onChange(t){const e=t.detail.value;this._percent=e;const i=this._percentToRGB(e/100);3===i.length&&this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,rgb_color:i})}render(){const t=this._percent||100*this._rgbToPercent(this.entity.attributes.rgb_color);return N` + `}};n([ht({attribute:!1})],ic.prototype,"hass",void 0),n([ht({attribute:!1})],ic.prototype,"entity",void 0),ic=n([dt("mushroom-light-brightness-control")],ic);const oc=[[0,"#f00"],[.17,"#ff0"],[.33,"#0f0"],[.5,"#0ff"],[.66,"#00f"],[.83,"#f0f"],[1,"#f00"]];let nc=class extends st{constructor(){super(...arguments),this._percent=0}_percentToRGB(t){return Pa.hsv(360*t,100,100).rgb().array()}_rgbToPercent(t){return Pa.rgb(t).hsv().hue()/360}onChange(t){const e=t.detail.value;this._percent=e;const i=this._percentToRGB(e/100);3===i.length&&this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,rgb_color:i})}render(){const t=this._percent||100*this._rgbToPercent(this.entity.attributes.rgb_color);return B` - `}static get styles(){const t=Os.map((([t,e])=>`${e} ${100*t}%`)).join(", ");return d` + `}static get styles(){const t=oc.map((([t,e])=>`${e} ${100*t}%`)).join(", ");return h` mushroom-slider { - --gradient: -webkit-linear-gradient(left, ${c(t)}); + --gradient: -webkit-linear-gradient(left, ${u(t)}); } - `}};n([st({attribute:!1})],Ms.prototype,"hass",void 0),n([st({attribute:!1})],Ms.prototype,"entity",void 0),Ms=n([at("mushroom-light-color-control")],Ms);let Ls=class extends ot{onChange(t){const e=t.detail.value;this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,color_temp:e})}render(){var t,e;const i=null!=(n=this.entity).attributes.color_temp?Math.round(n.attributes.color_temp):void 0;var n;return N` + `}};n([ht({attribute:!1})],nc.prototype,"hass",void 0),n([ht({attribute:!1})],nc.prototype,"entity",void 0),nc=n([dt("mushroom-light-color-control")],nc);let rc=class extends st{onChange(t){const e=t.detail.value;this.hass.callService("light","turn_on",{entity_id:this.entity.entity_id,color_temp:e})}render(){var t,e;const i=null!=(o=this.entity).attributes.color_temp?Math.round(o.attributes.color_temp):void 0;var o;return B` - `}static get styles(){return d` + `}static get styles(){return h` mushroom-slider { --gradient: -webkit-linear-gradient(right, rgb(255, 160, 0) 0%, white 100%); } - `}};n([st({attribute:!1})],Ls.prototype,"hass",void 0),n([st({attribute:!1})],Ls.prototype,"entity",void 0),Ls=n([at("mushroom-light-color-temp-control")],Ls);const Ds={brightness_control:"mdi:brightness-4",color_temp_control:"mdi:thermometer",color_control:"mdi:palette"};ll({type:"mushroom-light-card",name:"Mushroom Light Card",description:"Card for light entity"});let js=class extends rl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return Xh})),document.createElement("mushroom-light-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ts.includes(t.split(".")[0])));return{type:"custom:mushroom-light-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updateControls(),this.updateBrightness()}updated(t){super.updated(t),this.hass&&t.has("hass")&&(this.updateControls(),this.updateBrightness())}updateBrightness(){if(this.brightness=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.brightness=Bl(e))}onCurrentBrightnessChange(t){null!=t.detail.value&&(this.brightness=t.detail.value)}updateControls(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=[];this._config.collapsible_controls&&!Lt(e)||(this._config.show_brightness_control&&Wl(e)&&i.push("brightness_control"),this._config.show_color_temp_control&&function(t){var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>["color_temp"].includes(t)))}(e)&&i.push("color_temp_control"),this._config.show_color_control&&Xl(e)&&i.push("color_control")),this._controls=i;const n=!!this._activeControl&&i.includes(this._activeControl);this._activeControl=n?this._activeControl:i[0]}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type);let a=Ht(this.hass.localize,e,this.hass.locale);null!=this.brightness&&(a=`${this.brightness}%`);const l=pe(this.hass);return N` - - + `}};n([ht({attribute:!1})],rc.prototype,"hass",void 0),n([ht({attribute:!1})],rc.prototype,"entity",void 0),rc=n([dt("mushroom-light-color-temp-control")],rc);const ac={brightness_control:"mdi:brightness-4",color_temp_control:"mdi:thermometer",color_control:"mdi:palette"};$l({type:"mushroom-light-card",name:"Mushroom Light Card",description:"Card for light entity"});let lc=class extends kl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return rm})),document.createElement("mushroom-light-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>ec.includes(t.split(".")[0])));return{type:"custom:mushroom-light-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t),this.updateControls(),this.updateBrightness()}updated(t){super.updated(t),this.hass&&t.has("hass")&&(this.updateControls(),this.updateBrightness())}updateBrightness(){if(this.brightness=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];e&&(this.brightness=ss(e))}onCurrentBrightnessChange(t){null!=t.detail.value&&(this.brightness=t.detail.value)}updateControls(){if(!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=[];this._config.collapsible_controls&&!Nt(e)||(this._config.show_brightness_control&&ms(e)&&i.push("brightness_control"),this._config.show_color_temp_control&&function(t){var e;return null===(e=t.attributes.supported_color_modes)||void 0===e?void 0:e.some((t=>["color_temp"].includes(t)))}(e)&&i.push("color_temp_control"),this._config.show_color_control&&hs(e)&&i.push("color_control")),this._controls=i;const o=!!this._activeControl&&i.includes(this._activeControl);this._activeControl=o?this._activeControl:i[0]}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type);let a=ee(this.hass.localize,e,this.hass.locale,this.hass.entities,this.hass.connection.haVersion);null!=this.brightness&&(a=`${this.brightness}${te(this.hass.locale)}%`);const l=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i,a)}; + ${this.renderStateInfo(e,n,i,a)}; - ${this._controls.length>0?N` + ${this._controls.length>0?B`
${this.renderActiveControl(e)} ${this.renderOtherControls()}
`:null}
- `}renderIcon(t,e){var i;const n=Ul(t),o=Lt(t),r={};if(n&&(null===(i=this._config)||void 0===i?void 0:i.use_light_color)){const t=n.join(",");r["--icon-color"]=`rgb(${t})`,r["--shape-color"]=`rgba(${t}, 0.25)`,Hl(n)&&!this.hass.themes.darkMode&&(r["--shape-outline-color"]="rgba(var(--rgb-primary-text-color), 0.05)",Yl(n)&&(r["--icon-color"]="rgba(var(--rgb-primary-text-color), 0.2)"))}return N` + `}renderIcon(t,e){var i,o;const n=cs(t),r=Nt(t),a={},l=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(n&&(null===(o=this._config)||void 0===o?void 0:o.use_light_color)){const t=n.join(",");a["--icon-color"]=`rgb(${t})`,a["--shape-color"]=`rgba(${t}, 0.25)`,ds(n)&&!this.hass.themes.darkMode&&(a["--shape-outline-color"]="rgba(var(--rgb-primary-text-color), 0.05)",us(n)&&(a["--icon-color"]="rgba(var(--rgb-primary-text-color), 0.2)"))}else if(l){const t=Ra(l);a["--icon-color"]=`rgb(${t})`,a["--shape-color"]=`rgba(${t}, 0.2)`}return B` - `}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return N` - ${t.map((t=>N` + `}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return B` + ${t.map((t=>B` this._onControlTap(t,e)} /> `))} - `}renderActiveControl(t){var e;switch(this._activeControl){case"brightness_control":const i=Ul(t),n={};if(i&&(null===(e=this._config)||void 0===e?void 0:e.use_light_color)){const t=i.join(",");n["--slider-color"]=`rgb(${t})`,n["--slider-bg-color"]=`rgba(${t}, 0.2)`,Hl(i)&&!this.hass.themes.darkMode&&(n["--slider-bg-color"]="rgba(var(--rgb-primary-text-color), 0.05)",n["--slider-color"]="rgba(var(--rgb-primary-text-color), 0.15)")}return N` + `}renderActiveControl(t){var e,i;switch(this._activeControl){case"brightness_control":const o=cs(t),n={},r=null===(e=this._config)||void 0===e?void 0:e.icon_color;if(o&&(null===(i=this._config)||void 0===i?void 0:i.use_light_color)){const t=o.join(",");n["--slider-color"]=`rgb(${t})`,n["--slider-bg-color"]=`rgba(${t}, 0.2)`,ds(o)&&!this.hass.themes.darkMode&&(n["--slider-bg-color"]="rgba(var(--rgb-primary-text-color), 0.05)",n["--slider-color"]="rgba(var(--rgb-primary-text-color), 0.15)")}else if(r){const t=Ra(r);n["--slider-color"]=`rgb(${t})`,n["--slider-bg-color"]=`rgba(${t}, 0.2)`}return B` - `;case"color_temp_control":return N` + `;case"color_temp_control":return B` - `;case"color_control":return N` + `;case"color_control":return B` - `;default:return null}}static get styles(){return[super.styles,al,d` + `;default:return null}}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2474,58 +2551,58 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-light-color-control { flex: 1; } - `]}};n([ct()],js.prototype,"_config",void 0),n([ct()],js.prototype,"_activeControl",void 0),n([ct()],js.prototype,"_controls",void 0),n([ct()],js.prototype,"brightness",void 0),js=n([at("mushroom-light-card")],js);const Ps=["lock"];function Ns(t){return"unlocked"===t.state}function Vs(t){return"locked"===t.state}function Rs(t){switch(t.state){case"locking":case"unlocking":return!0;default:return!1}}const Fs=[{icon:"mdi:lock",title:"lock",serviceName:"lock",isVisible:t=>Ns(t),isDisabled:()=>!1},{icon:"mdi:lock-open",title:"unlock",serviceName:"unlock",isVisible:t=>Vs(t),isDisabled:()=>!1},{icon:"mdi:lock-clock",isVisible:t=>Rs(t),isDisabled:()=>!0},{icon:"mdi:door-open",title:"open",serviceName:"open",isVisible:t=>Nt(t,1)&&Ns(t),isDisabled:t=>Rs(t)}];let Bs=class extends ot{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.entry;this.hass.callService("lock",e.serviceName,{entity_id:this.entity.entity_id})}render(){const t=pe(this.hass),e=Hi(this.hass);return N` + `]}};n([mt()],lc.prototype,"_config",void 0),n([mt()],lc.prototype,"_activeControl",void 0),n([mt()],lc.prototype,"_controls",void 0),n([mt()],lc.prototype,"brightness",void 0),lc=n([dt("mushroom-light-card")],lc);const sc=["lock"];function cc(t){return"unlocked"===t.state}function dc(t){return"locked"===t.state}function uc(t){switch(t.state){case"locking":case"unlocking":return!0;default:return!1}}const hc=[{icon:"mdi:lock",title:"lock",serviceName:"lock",isVisible:t=>cc(t),isDisabled:()=>!1},{icon:"mdi:lock-open",title:"unlock",serviceName:"unlock",isVisible:t=>dc(t),isDisabled:()=>!1},{icon:"mdi:lock-clock",isVisible:t=>uc(t),isDisabled:()=>!0},{icon:"mdi:door-open",title:"open",serviceName:"open",isVisible:t=>Bt(t,1)&&cc(t),isDisabled:t=>uc(t)}];let mc=class extends st{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.entry;this.hass.callService("lock",e.serviceName,{entity_id:this.entity.entity_id})}render(){const t=Ee(this.hass),e=ao(this.hass);return B` ${Fs.filter((t=>t.isVisible(this.entity))).map((t=>N` + >${hc.filter((t=>t.isVisible(this.entity))).map((t=>B` `))} - `}};n([st({attribute:!1})],Bs.prototype,"hass",void 0),n([st({attribute:!1})],Bs.prototype,"entity",void 0),n([st()],Bs.prototype,"fill",void 0),Bs=n([at("mushroom-lock-buttons-control")],Bs),ll({type:"mushroom-lock-card",name:"Mushroom Lock Card",description:"Card for all lock entities"});let Us=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return Xm})),document.createElement("mushroom-lock-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Ps.includes(t.split(".")[0])));return{type:"custom:mushroom-lock-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type),a=pe(this.hass);return N` - - + `}};n([ht({attribute:!1})],mc.prototype,"hass",void 0),n([ht({attribute:!1})],mc.prototype,"entity",void 0),n([ht()],mc.prototype,"fill",void 0),mc=n([dt("mushroom-lock-buttons-control")],mc),$l({type:"mushroom-lock-card",name:"Mushroom Lock Card",description:"Card for all lock entities"});let pc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return up})),document.createElement("mushroom-lock-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>sc.includes(t.split(".")[0])));return{type:"custom:mushroom-lock-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type),a=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i)}; + ${this.renderStateInfo(e,n,i)};
- `}renderIcon(t,e){const i=Dt(t),n={"--icon-color":"rgb(var(--rgb-state-lock))","--shape-color":"rgba(var(--rgb-state-lock), 0.2)"};return Vs(t)?(n["--icon-color"]="rgb(var(--rgb-state-lock-locked))",n["--shape-color"]="rgba(var(--rgb-state-lock-locked), 0.2)"):Ns(t)?(n["--icon-color"]="rgb(var(--rgb-state-lock-unlocked))",n["--shape-color"]="rgba(var(--rgb-state-lock-unlocked), 0.2)"):Rs(t)&&(n["--icon-color"]="rgb(var(--rgb-state-lock-pending))",n["--shape-color"]="rgba(var(--rgb-state-lock-pending), 0.2)"),N` + `}renderIcon(t,e){const i=Rt(t),o={"--icon-color":"rgb(var(--rgb-state-lock))","--shape-color":"rgba(var(--rgb-state-lock), 0.2)"};return dc(t)?(o["--icon-color"]="rgb(var(--rgb-state-lock-locked))",o["--shape-color"]="rgba(var(--rgb-state-lock-locked), 0.2)"):cc(t)?(o["--icon-color"]="rgb(var(--rgb-state-lock-unlocked))",o["--shape-color"]="rgba(var(--rgb-state-lock-unlocked), 0.2)"):uc(t)&&(o["--icon-color"]="rgb(var(--rgb-state-lock-pending))",o["--shape-color"]="rgba(var(--rgb-state-lock-pending), 0.2)"),B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } mushroom-lock-buttons-control { flex: 1; } - `]}};n([ct()],Us.prototype,"_config",void 0),Us=n([at("mushroom-lock-card")],Us);const Hs=["media_player"];function Ys(t){return null!=t.attributes.volume_level?100*t.attributes.volume_level:void 0}const Xs=(t,e)=>{if(!t)return[];const i=t.state;if("off"===i)return Nt(t,128)&&e.includes("on_off")?[{icon:"mdi:power",action:"turn_on"}]:[];const n=[];Nt(t,256)&&e.includes("on_off")&&n.push({icon:"mdi:power",action:"turn_off"});const o=!0===t.attributes.assumed_state,r=t.attributes;return("playing"===i||"paused"===i||o)&&Nt(t,32768)&&e.includes("shuffle")&&n.push({icon:!0===r.shuffle?"mdi:shuffle":"mdi:shuffle-disabled",action:"shuffle_set"}),("playing"===i||"paused"===i||o)&&Nt(t,16)&&e.includes("previous")&&n.push({icon:"mdi:skip-previous",action:"media_previous_track"}),!o&&("playing"===i&&(Nt(t,1)||Nt(t,4096))||("paused"===i||"idle"===i)&&Nt(t,16384)||"on"===i&&(Nt(t,16384)||Nt(t,1)))&&e.includes("play_pause_stop")&&n.push({icon:"on"===i?"mdi:play-pause":"playing"!==i?"mdi:play":Nt(t,1)?"mdi:pause":"mdi:stop",action:"playing"!==i?"media_play":Nt(t,1)?"media_pause":"media_stop"}),o&&Nt(t,16384)&&e.includes("play_pause_stop")&&n.push({icon:"mdi:play",action:"media_play"}),o&&Nt(t,1)&&e.includes("play_pause_stop")&&n.push({icon:"mdi:pause",action:"media_pause"}),o&&Nt(t,4096)&&e.includes("play_pause_stop")&&n.push({icon:"mdi:stop",action:"media_stop"}),("playing"===i||"paused"===i||o)&&Nt(t,32)&&e.includes("next")&&n.push({icon:"mdi:skip-next",action:"media_next_track"}),("playing"===i||"paused"===i||o)&&Nt(t,262144)&&e.includes("repeat")&&n.push({icon:"all"===r.repeat?"mdi:repeat":"one"===r.repeat?"mdi:repeat-once":"mdi:repeat-off",action:"repeat_set"}),n.length>0?n:[]},Ws=(t,e,i)=>{let n={};"shuffle_set"===i?n={shuffle:!e.attributes.shuffle}:"repeat_set"===i?n={repeat:"all"===e.attributes.repeat?"one":"off"===e.attributes.repeat?"all":"off"}:"volume_mute"===i&&(n={is_volume_muted:!e.attributes.is_volume_muted}),t.callService("media_player",i,Object.assign({entity_id:e.entity_id},n))};let qs=class extends ot{constructor(){super(...arguments),this.fill=!1}_handleClick(t){t.stopPropagation();const e=t.target.action;Ws(this.hass,this.entity,e)}render(){const t=pe(this.hass),e=Xs(this.entity,this.controls);return N` + `]}};n([mt()],pc.prototype,"_config",void 0),pc=n([dt("mushroom-lock-card")],pc);const fc=["media_player"];function gc(t){return null!=t.attributes.volume_level?100*t.attributes.volume_level:void 0}const _c=(t,e)=>{if(!t)return[];const i=t.state;if("off"===i)return Bt(t,128)&&e.includes("on_off")?[{icon:"mdi:power",action:"turn_on"}]:[];const o=[];Bt(t,256)&&e.includes("on_off")&&o.push({icon:"mdi:power",action:"turn_off"});const n=!0===t.attributes.assumed_state,r=t.attributes;return("playing"===i||"paused"===i||n)&&Bt(t,32768)&&e.includes("shuffle")&&o.push({icon:!0===r.shuffle?"mdi:shuffle":"mdi:shuffle-disabled",action:"shuffle_set"}),("playing"===i||"paused"===i||n)&&Bt(t,16)&&e.includes("previous")&&o.push({icon:"mdi:skip-previous",action:"media_previous_track"}),!n&&("playing"===i&&(Bt(t,1)||Bt(t,4096))||("paused"===i||"idle"===i)&&Bt(t,16384)||"on"===i&&(Bt(t,16384)||Bt(t,1)))&&e.includes("play_pause_stop")&&o.push({icon:"on"===i?"mdi:play-pause":"playing"!==i?"mdi:play":Bt(t,1)?"mdi:pause":"mdi:stop",action:"playing"!==i?"media_play":Bt(t,1)?"media_pause":"media_stop"}),n&&Bt(t,16384)&&e.includes("play_pause_stop")&&o.push({icon:"mdi:play",action:"media_play"}),n&&Bt(t,1)&&e.includes("play_pause_stop")&&o.push({icon:"mdi:pause",action:"media_pause"}),n&&Bt(t,4096)&&e.includes("play_pause_stop")&&o.push({icon:"mdi:stop",action:"media_stop"}),("playing"===i||"paused"===i||n)&&Bt(t,32)&&e.includes("next")&&o.push({icon:"mdi:skip-next",action:"media_next_track"}),("playing"===i||"paused"===i||n)&&Bt(t,262144)&&e.includes("repeat")&&o.push({icon:"all"===r.repeat?"mdi:repeat":"one"===r.repeat?"mdi:repeat-once":"mdi:repeat-off",action:"repeat_set"}),o.length>0?o:[]},vc=(t,e,i)=>{let o={};"shuffle_set"===i?o={shuffle:!e.attributes.shuffle}:"repeat_set"===i?o={repeat:"all"===e.attributes.repeat?"one":"off"===e.attributes.repeat?"all":"off"}:"volume_mute"===i&&(o={is_volume_muted:!e.attributes.is_volume_muted}),t.callService("media_player",i,Object.assign({entity_id:e.entity_id},o))};let bc=class extends st{constructor(){super(...arguments),this.fill=!1}_handleClick(t){t.stopPropagation();const e=t.target.action;vc(this.hass,this.entity,e)}render(){const t=Ee(this.hass),e=_c(this.entity,this.controls);return B` - ${e.map((t=>N` + ${e.map((t=>B` `))} - `}};n([st({attribute:!1})],qs.prototype,"hass",void 0),n([st({attribute:!1})],qs.prototype,"entity",void 0),n([st({attribute:!1})],qs.prototype,"controls",void 0),n([st()],qs.prototype,"fill",void 0),qs=n([at("mushroom-media-player-media-control")],qs);let Ks=class extends ot{constructor(){super(...arguments),this.fill=!1}handleSliderChange(t){const e=t.detail.value;this.hass.callService("media_player","volume_set",{entity_id:this.entity.entity_id,volume_level:e/100})}handleSliderCurrentChange(t){let e=t.detail.value;this.dispatchEvent(new CustomEvent("current-change",{detail:{value:e}}))}handleClick(t){t.stopPropagation();const e=t.target.action;Ws(this.hass,this.entity,e)}render(){var t,e,i;if(!this.entity)return null;const n=Ys(this.entity),o=pe(this.hass),r=(null===(t=this.controls)||void 0===t?void 0:t.includes("volume_set"))&&Nt(this.entity,4),a=(null===(e=this.controls)||void 0===e?void 0:e.includes("volume_mute"))&&Nt(this.entity,8),l=(null===(i=this.controls)||void 0===i?void 0:i.includes("volume_buttons"))&&Nt(this.entity,1024);return N` - - ${r?N` + ${r?B` `:null} - ${a?N` + ${a?B` `:void 0} - ${l?N` + ${l?B` `:void 0} - ${l?N` + ${l?B` `:void 0} - `}static get styles(){return d` + `}static get styles(){return h` mushroom-slider { flex: 1; --main-color: rgb(var(--rgb-state-media-player)); --bg-color: rgba(var(--rgb-state-media-player), 0.2); } - `}};n([st({attribute:!1})],Ks.prototype,"hass",void 0),n([st({attribute:!1})],Ks.prototype,"entity",void 0),n([st()],Ks.prototype,"fill",void 0),n([st({attribute:!1})],Ks.prototype,"controls",void 0),Ks=n([at("mushroom-media-player-volume-control")],Ks);const Gs={media_control:"mdi:play-pause",volume_control:"mdi:volume-high"};ll({type:"mushroom-media-player-card",name:"Mushroom Media Card",description:"Card for media player entity"});let Zs=class extends rl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return Qm})),document.createElement("mushroom-media-player-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Hs.includes(t.split(".")[0])));return{type:"custom:mushroom-media-player-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t),this.updateControls(),this.updateVolume()}updated(t){super.updated(t),this.hass&&t.has("hass")&&(this.updateControls(),this.updateVolume())}updateVolume(){if(this.volume=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=Ys(e);this.volume=null!=i?Math.round(i):i}onCurrentVolumeChange(t){null!=t.detail.value&&(this.volume=t.detail.value)}updateControls(){var t;if(!this._config||!this.hass||!this._config.entity)return;const e=this._config.entity,i=this.hass.states[e];if(!i)return;const n=[];this._config.collapsible_controls&&!Lt(i)||(((t,e)=>Xs(t,null!=e?e:[]).length>0)(i,null===(t=this._config)||void 0===t?void 0:t.media_controls)&&n.push("media_control"),((t,e)=>(null==e?void 0:e.includes("volume_buttons"))&&Nt(t,1024)||(null==e?void 0:e.includes("volume_mute"))&&Nt(t,8)||(null==e?void 0:e.includes("volume_set"))&&Nt(t,4))(i,this._config.volume_controls)&&n.push("volume_control")),this._controls=n;const o=!!this._activeControl&&n.includes(this._activeControl);this._activeControl=o?this._activeControl:n[0]}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=function(t,e){var i,n=t.icon||pl(e);if(![Tt,zt,Ot].includes(e.state)&&t.use_media_info)switch(null===(i=e.attributes.app_name)||void 0===i?void 0:i.toLowerCase()){case"spotify":return"mdi:spotify";case"google podcasts":return"mdi:google-podcast";case"plex":return"mdi:plex";case"soundcloud":return"mdi:soundcloud";case"youtube":return"mdi:youtube";case"oto music":return"mdi:music-circle";case"netflix":return"mdi:netflix";default:return n}return n}(this._config,e),n=function(t,e){let i=t.name||e.attributes.friendly_name||"";return![Tt,zt,Ot].includes(e.state)&&t.use_media_info&&e.attributes.media_title&&(i=e.attributes.media_title),i}(this._config,e),o=function(t,e,i){let n=Ht(i.localize,e,i.locale);return![Tt,zt,Ot].includes(e.state)&&t.use_media_info&&(t=>{let e;switch(t.attributes.media_content_type){case"music":case"image":e=t.attributes.media_artist;break;case"playlist":e=t.attributes.media_playlist;break;case"tvshow":e=t.attributes.media_series_title,t.attributes.media_season&&(e+=" S"+t.attributes.media_season,t.attributes.media_episode&&(e+="E"+t.attributes.media_episode));break;default:e=t.attributes.app_name||""}return e})(e)||n}(this._config,e,this.hass),r=Ka(this._config),a=Ta(e,r.icon_type),l=null!=this.volume&&this._config.show_volume_level?`${o} - ${this.volume}%`:o,s=pe(this.hass);return N` - + `}};n([ht({attribute:!1})],yc.prototype,"hass",void 0),n([ht({attribute:!1})],yc.prototype,"entity",void 0),n([ht()],yc.prototype,"fill",void 0),n([ht({attribute:!1})],yc.prototype,"controls",void 0),yc=n([dt("mushroom-media-player-volume-control")],yc);const xc={media_control:"mdi:play-pause",volume_control:"mdi:volume-high"};$l({type:"mushroom-media-player-card",name:"Mushroom Media Card",description:"Card for media player entity"});let wc=class extends kl{constructor(){super(...arguments),this._controls=[]}static async getConfigElement(){return await Promise.resolve().then((function(){return vp})),document.createElement("mushroom-media-player-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>fc.includes(t.split(".")[0])));return{type:"custom:mushroom-media-player-card",entity:e[0]}}_onControlTap(t,e){e.stopPropagation(),this._activeControl=t}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t),this.updateControls(),this.updateVolume()}updated(t){super.updated(t),this.hass&&t.has("hass")&&(this.updateControls(),this.updateVolume())}updateVolume(){if(this.volume=void 0,!this._config||!this.hass||!this._config.entity)return;const t=this._config.entity,e=this.hass.states[t];if(!e)return;const i=gc(e);this.volume=null!=i?Math.round(i):i}onCurrentVolumeChange(t){null!=t.detail.value&&(this.volume=t.detail.value)}updateControls(){var t;if(!this._config||!this.hass||!this._config.entity)return;const e=this._config.entity,i=this.hass.states[e];if(!i)return;const o=[];this._config.collapsible_controls&&!Nt(i)||(((t,e)=>_c(t,null!=e?e:[]).length>0)(i,null===(t=this._config)||void 0===t?void 0:t.media_controls)&&o.push("media_control"),((t,e)=>(null==e?void 0:e.includes("volume_buttons"))&&Bt(t,1024)||(null==e?void 0:e.includes("volume_mute"))&&Bt(t,8)||(null==e?void 0:e.includes("volume_set"))&&Bt(t,4))(i,this._config.volume_controls)&&o.push("volume_control")),this._controls=o;const n=!!this._activeControl&&o.includes(this._activeControl);this._activeControl=n?this._activeControl:o[0]}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=function(t,e){var i,o=t.icon||Dl(e);if(![Dt,Lt,jt].includes(e.state)&&t.use_media_info)switch(null===(i=e.attributes.app_name)||void 0===i?void 0:i.toLowerCase()){case"spotify":return"mdi:spotify";case"google podcasts":return"mdi:google-podcast";case"plex":return"mdi:plex";case"soundcloud":return"mdi:soundcloud";case"youtube":return"mdi:youtube";case"oto music":return"mdi:music-circle";case"netflix":return"mdi:netflix";default:return o}return o}(this._config,e),o=function(t,e){let i=t.name||e.attributes.friendly_name||"";return![Dt,Lt,jt].includes(e.state)&&t.use_media_info&&e.attributes.media_title&&(i=e.attributes.media_title),i}(this._config,e),n=function(t,e,i){let o=ee(i.localize,e,i.locale,i.entities,i.connection.haVersion);return![Dt,Lt,jt].includes(e.state)&&t.use_media_info&&(t=>{let e;switch(t.attributes.media_content_type){case"music":case"image":e=t.attributes.media_artist;break;case"playlist":e=t.attributes.media_playlist;break;case"tvshow":e=t.attributes.media_series_title,t.attributes.media_season&&(e+=" S"+t.attributes.media_season,t.attributes.media_episode&&(e+="E"+t.attributes.media_episode));break;default:e=t.attributes.app_name||""}return e})(e)||o}(this._config,e,this.hass),r=ml(this._config),a=qa(e,r.icon_type),l=null!=this.volume&&this._config.show_volume_level?`${n} - ${this.volume}${te(this.hass.locale)}%`:n,s=Ee(this.hass);return B` + ${a?this.renderPicture(a):this.renderIcon(e,i)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,r,n,l)}; + ${this.renderStateInfo(e,r,o,l)}; - ${this._controls.length>0?N` + ${this._controls.length>0?B`
${this.renderActiveControl(e,r.layout)} ${this.renderOtherControls()} @@ -2597,14 +2674,14 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl `:null} - `}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return N` - ${t.map((t=>N` + `}renderOtherControls(){const t=this._controls.filter((t=>t!=this._activeControl));return B` + ${t.map((t=>B` this._onControlTap(t,e)} /> `))} - `}renderActiveControl(t,e){var i,n,o,r;const a=null!==(n=null===(i=this._config)||void 0===i?void 0:i.media_controls)&&void 0!==n?n:[],l=null!==(r=null===(o=this._config)||void 0===o?void 0:o.volume_controls)&&void 0!==r?r:[];switch(this._activeControl){case"media_control":return N` + `}renderActiveControl(t,e){var i,o,n,r;const a=null!==(o=null===(i=this._config)||void 0===i?void 0:i.media_controls)&&void 0!==o?o:[],l=null!==(r=null===(n=this._config)||void 0===n?void 0:n.volume_controls)&&void 0!==r?r:[];switch(this._activeControl){case"media_control":return B` - `;case"volume_control":return N` + `;case"volume_control":return B` - `;default:return null}}static get styles(){return[super.styles,al,d` + `;default:return null}}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2632,69 +2709,128 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-media-player-volume-control { flex: 1; } - `]}};n([ct()],Zs.prototype,"_config",void 0),n([ct()],Zs.prototype,"_activeControl",void 0),n([ct()],Zs.prototype,"_controls",void 0),n([ct()],Zs.prototype,"volume",void 0),Zs=n([at("mushroom-media-player-card")],Zs);const Js=["person","device_tracker"];ll({type:"mushroom-person-card",name:"Mushroom Person Card",description:"Card for person entity"});let Qs=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return op})),document.createElement("mushroom-person-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Js.includes(t.split(".")[0])));return{type:"custom:mushroom-person-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type),a=pe(this.hass);return N` - - + `]}};n([mt()],wc.prototype,"_config",void 0),n([mt()],wc.prototype,"_activeControl",void 0),n([mt()],wc.prototype,"_controls",void 0),n([mt()],wc.prototype,"volume",void 0),wc=n([dt("mushroom-media-player-card")],wc);const kc=["person","device_tracker"];$l({type:"mushroom-person-card",name:"Mushroom Person Card",description:"Card for person entity"});let Cc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return kp})),document.createElement("mushroom-person-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>kc.includes(t.split(".")[0])));return{type:"custom:mushroom-person-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type),a=Ee(this.hass);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i)}; + ${this.renderStateInfo(e,n,i)}; - `}renderStateBadge(t){const e=Object.values(this.hass.states).filter((t=>t.entity_id.startsWith("zone."))),i=function(t,e){const i=t.state;if(i===zt)return"mdi:help";if("not_home"===i)return"mdi:home-export-outline";if("home"===i)return"mdi:home";const n=e.find((t=>i===t.attributes.friendly_name));return n&&n.attributes.icon?n.attributes.icon:"mdi:home"}(t,e),n=function(t,e){const i=t.state;if(i===zt)return"var(--rgb-state-person-unknown)";if("not_home"===i)return"var(--rgb-state-person-not-home)";if("home"===i)return"var(--rgb-state-person-home)";const n=e.some((t=>i===t.attributes.friendly_name));return n?"var(--rgb-state-person-zone)":"var(--rgb-state-person-home)"}(t,e);return N` + `}renderStateBadge(t){const e=Object.values(this.hass.states).filter((t=>t.entity_id.startsWith("zone."))),i=function(t,e){const i=t.state;if(i===Lt)return"mdi:help";if("not_home"===i)return"mdi:home-export-outline";if("home"===i)return"mdi:home";const o=e.find((t=>i===t.attributes.friendly_name));return o&&o.attributes.icon?o.attributes.icon:"mdi:home"}(t,e),o=function(t,e){const i=t.state;if(i===Lt)return"var(--rgb-state-person-unknown)";if("not_home"===i)return"var(--rgb-state-person-not-home)";if("home"===i)return"var(--rgb-state-person-home)";const o=e.some((t=>i===t.attributes.friendly_name));return o?"var(--rgb-state-person-zone)":"var(--rgb-state-person-home)"}(t,e);return B` - `}renderBadge(t){return!Dt(t)?super.renderBadge(t):this.renderStateBadge(t)}static get styles(){return[super.styles,al,d` + `}renderBadge(t){return!Rt(t)?super.renderBadge(t):this.renderStateBadge(t)}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } - `]}};n([ct()],Qs.prototype,"_config",void 0),Qs=n([at("mushroom-person-card")],Qs);ll({type:"mushroom-template-card",name:"Mushroom Template Card",description:"Card for custom rendering with templates"});const tc=["icon","icon_color","badge_color","badge_icon","primary","secondary","picture"];let ec=class extends ol{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return fd})),document.createElement("mushroom-template-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-template-card",primary:"Hello, {{user}}",secondary:"How are you?",icon:"mdi:home"}}getCardSize(){return 1}setConfig(t){tc.forEach((e=>{var i,n;(null===(i=this._config)||void 0===i?void 0:i[e])===t[e]&&(null===(n=this._config)||void 0===n?void 0:n.entity)==t.entity||this._tryDisconnectKey(e)})),this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}render(){if(!this._config||!this.hass)return N``;const t=this.getValue("icon"),e=this.getValue("icon_color"),i=this.getValue("badge_icon"),n=this.getValue("badge_color"),o=this.getValue("primary"),r=this.getValue("secondary"),a=this.getValue("picture"),l=this._config.multiline_secondary,s=pe(this.hass),c=Ka({fill_container:this._config.fill_container,layout:this._config.layout,icon_type:Boolean(a)?"entity-picture":Boolean(t)?"icon":"none",primary_info:Boolean(o)?"name":"none",secondary_info:Boolean(r)?"state":"none"});return N` - + `]}};n([mt()],Cc.prototype,"_config",void 0),Cc=n([dt("mushroom-person-card")],Cc);const $c=["input_select","select"];function Ec(t){return null!=t.state?t.state:void 0}let Ac=class extends st{_selectChanged(t){const e=t.target.value,i=Ec(this.entity);e&&e!==i&&this._setValue(e)}_setValue(t){const e=this.entity.entity_id.split(".")[0];this.hass.callService(e,"select_option",{entity_id:this.entity.entity_id,option:t})}render(){const t=Ec(this.entity),e=this.entity.attributes.options;return B` + t.stopPropagation()} + .value=${null!=t?t:""} + naturalMenuWidth + > + ${e.map((t=>B` ${t} `))} + + `}static get styles(){return h` + mushroom-select { + --select-height: 42px; + width: 100%; + } + `}};n([ht()],Ac.prototype,"hass",void 0),n([ht({attribute:!1})],Ac.prototype,"entity",void 0),Ac=n([dt("mushroom-select-option-control")],Ac),$l({type:"mushroom-select-card",name:"Mushroom Select Card",description:"Card for select and input_select entities"});let Sc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return Sp})),document.createElement("mushroom-select-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>$c.includes(t.split(".")[0])));return{type:"custom:mushroom-select-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){var t;if(!this._config||!this.hass||!this._config.entity)return B``;const e=this._config.entity,i=this.hass.states[e],o=this._config.name||i.attributes.friendly_name||"",n=this._config.icon||Dl(i),r=ml(this._config),a=qa(i,r.icon_type),l=Ee(this.hass),s=null===(t=this._config)||void 0===t?void 0:t.icon_color,c={};if(s){const t=Ra(s);c["--mdc-theme-primary"]=`rgb(${t})`}return B` + + + + ${a?this.renderPicture(a):this.renderIcon(i,n)} + ${this.renderBadge(i)} + ${this.renderStateInfo(i,r,o)}; + +
+ +
+
+
+ `}renderIcon(t,e){var i;const o=Nt(t),n={},r=null===(i=this._config)||void 0===i?void 0:i.icon_color;if(r){const t=Ra(r);n["--icon-color"]=`rgb(${t})`,n["--shape-color"]=`rgba(${t}, 0.2)`}return B` + + `}static get styles(){return[super.styles,Cl,h` + .actions { + overflow: visible; + display: block; + } + mushroom-state-item { + cursor: pointer; + } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-entity)); + --shape-color: rgba(var(--rgb-state-entity), 0.2); + } + mushroom-select-option-control { + flex: 1; + --mdc-theme-primary: rgb(var(--rgb-state-entity)); + } + `]}};n([mt()],Sc.prototype,"_config",void 0),Sc=n([dt("mushroom-select-card")],Sc);$l({type:"mushroom-template-card",name:"Mushroom Template Card",description:"Card for custom rendering with templates"});const Ic=["icon","icon_color","badge_color","badge_icon","primary","secondary","picture"];let Tc=class extends wl{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return Yd})),document.createElement("mushroom-template-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-template-card",primary:"Hello, {{user}}",secondary:"How are you?",icon:"mdi:home"}}getCardSize(){return 1}setConfig(t){Ic.forEach((e=>{var i,o;(null===(i=this._config)||void 0===i?void 0:i[e])===t[e]&&(null===(o=this._config)||void 0===o?void 0:o.entity)==t.entity||this._tryDisconnectKey(e)})),this._config=Object.assign({tap_action:{action:"toggle"},hold_action:{action:"more-info"}},t)}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}render(){if(!this._config||!this.hass)return B``;const t=this.getValue("icon"),e=this.getValue("icon_color"),i=this.getValue("badge_icon"),o=this.getValue("badge_color"),n=this.getValue("primary"),r=this.getValue("secondary"),a=this.getValue("picture"),l=this._config.multiline_secondary,s=Ee(this.hass),c=ml({fill_container:this._config.fill_container,layout:this._config.layout,icon_type:Boolean(a)?"entity-picture":Boolean(t)?"icon":"none",primary_info:Boolean(n)?"name":"none",secondary_info:Boolean(r)?"state":"none"});return B` + ${a?this.renderPicture(a):t?this.renderIcon(t,e):null} - ${(t||a)&&i?this.renderBadgeIcon(i,n):void 0} + ${(t||a)&&i?this.renderBadgeIcon(i,o):void 0} - `}renderPicture(t){return N` + `}renderPicture(t){return B` - `}renderIcon(t,e){const i={};if(e){const t=xa(e);i["--icon-color"]=`rgb(${t})`,i["--shape-color"]=`rgba(${t}, 0.2)`}return N` + `}renderIcon(t,e){const i={};if(e){const t=Ra(e);i["--icon-color"]=`rgb(${t})`,i["--shape-color"]=`rgba(${t}, 0.2)`}return B` - `}renderBadgeIcon(t,e){const i={};if(e){const t=xa(e);i["--main-color"]=`rgba(${t})`}return N` + `}renderBadgeIcon(t,e){const i={};if(e){const t=Ra(e);i["--main-color"]=`rgba(${t})`}return B` - `}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){tc.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=ke(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name,entity:this._config.entity},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const n={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:n}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){tc.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return[super.styles,al,d` + `}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){Ic.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=Le(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name,entity:this._config.entity},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const o={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:o}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){Ic.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2702,34 +2838,77 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl --icon-color: rgb(var(--rgb-disabled)); --shape-color: rgba(var(--rgb-disabled), 0.2); } - `]}};n([ct()],ec.prototype,"_config",void 0),n([ct()],ec.prototype,"_templateResults",void 0),n([ct()],ec.prototype,"_unsubRenderTemplates",void 0),ec=n([at("mushroom-template-card")],ec);ll({type:"mushroom-title-card",name:"Mushroom Title Card",description:"Title and subtitle to separate sections"});const ic=["title","subtitle"];let nc=class extends ol{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return cp})),document.createElement("mushroom-title-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-title-card",title:"Hello, {{ user }} !"}}getCardSize(){return 1}setConfig(t){ic.forEach((e=>{var i;(null===(i=this._config)||void 0===i?void 0:i[e])!==t[e]&&this._tryDisconnectKey(e)})),this._config=t}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}render(){if(!this._config||!this.hass)return N``;const t=this.getValue("title"),e=this.getValue("subtitle");let i="";return this._config.alignment&&(i=`align-${this._config.alignment}`),N` -
- ${t?N`

${t}

`:null} - ${e?N`

${e}

`:null} + `]}};n([mt()],Tc.prototype,"_config",void 0),n([mt()],Tc.prototype,"_templateResults",void 0),n([mt()],Tc.prototype,"_unsubRenderTemplates",void 0),Tc=n([dt("mushroom-template-card")],Tc);$l({type:"mushroom-title-card",name:"Mushroom Title Card",description:"Title and subtitle to separate sections"});const zc=["title","subtitle"];let Oc=class extends wl{constructor(){super(...arguments),this._templateResults={},this._unsubRenderTemplates=new Map}static async getConfigElement(){return await Promise.resolve().then((function(){return Dp})),document.createElement("mushroom-title-card-editor")}static async getStubConfig(t){return{type:"custom:mushroom-title-card",title:"Hello, {{ user }} !"}}getCardSize(){return 1}setConfig(t){zc.forEach((e=>{var i;(null===(i=this._config)||void 0===i?void 0:i[e])!==t[e]&&this._tryDisconnectKey(e)})),this._config=Object.assign({title_tap_action:{action:"none"},subtitle_tap_action:{action:"none"}},t)}connectedCallback(){super.connectedCallback(),this._tryConnect()}disconnectedCallback(){this._tryDisconnect()}isTemplate(t){var e;const i=null===(e=this._config)||void 0===e?void 0:e[t];return null==i?void 0:i.includes("{")}getValue(t){var e,i;return this.isTemplate(t)?null===(e=this._templateResults[t])||void 0===e?void 0:e.result:null===(i=this._config)||void 0===i?void 0:i[t]}_handleTitleAction(t){const e={tap_action:this._config.title_tap_action};He(this,this.hass,e,t.detail.action)}_handleSubtitleAction(t){const e={tap_action:this._config.subtitle_tap_action};He(this,this.hass,e,t.detail.action)}render(){if(!this._config||!this.hass)return B``;const t=this.getValue("title"),e=this.getValue("subtitle");let i="";this._config.alignment&&(i=`align-${this._config.alignment}`);const o=Boolean(this._config.title_tap_action&&"none"!==this._config.title_tap_action.action),n=Boolean(this._config.subtitle_tap_action&&"none"!==this._config.subtitle_tap_action.action),r=Ee(this.hass);return B` +
+ ${t?B` +
+

${t}${this.renderArrow()}

+
+ `:null} + ${e?B` +
+

${e}${this.renderArrow()}

+
+ `:null}
- `}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){ic.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=ke(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const n={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:n}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){ic.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return[super.styles,al,d` + `}renderArrow(){const t=Ee(this.hass);return B` `}updated(t){super.updated(t),this._config&&this.hass&&this._tryConnect()}async _tryConnect(){zc.forEach((t=>{this._tryConnectKey(t)}))}async _tryConnectKey(t){var e,i;if(void 0===this._unsubRenderTemplates.get(t)&&this.hass&&this._config&&this.isTemplate(t))try{const i=Le(this.hass.connection,(e=>{this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:e})}),{template:null!==(e=this._config[t])&&void 0!==e?e:"",entity_ids:this._config.entity_id,variables:{config:this._config,user:this.hass.user.name},strict:!0});this._unsubRenderTemplates.set(t,i),await i}catch(e){const o={result:null!==(i=this._config[t])&&void 0!==i?i:"",listeners:{all:!1,domains:[],entities:[],time:!1}};this._templateResults=Object.assign(Object.assign({},this._templateResults),{[t]:o}),this._unsubRenderTemplates.delete(t)}}async _tryDisconnect(){zc.forEach((t=>{this._tryDisconnectKey(t)}))}async _tryDisconnectKey(t){const e=this._unsubRenderTemplates.get(t);if(e)try{(await e)(),this._unsubRenderTemplates.delete(t)}catch(t){if("not_found"!==t.code&&"template_error"!==t.code)throw t}}static get styles(){return[super.styles,Cl,h` .header { display: block; padding: var(--title-padding); } - .header * { + .header div * { margin: 0; white-space: pre-wrap; } - .header *:not(:last-child) { + .header div:not(:last-child) { margin-bottom: var(--title-spacing); } + .actionable { + cursor: pointer; + } + .header ha-icon { + display: none; + } + .actionable ha-icon { + display: inline-block; + margin-left: 4px; + transition: transform 180ms ease-in-out; + } + .actionable:hover ha-icon { + transform: translateX(4px); + } + [rtl] .actionable ha-icon { + margin-left: initial; + margin-right: 4px; + } + [rtl] .actionable:hover ha-icon { + transform: translateX(-4px); + } .title { color: var(--primary-text-color); font-size: var(--title-font-size); font-weight: var(--title-font-weight); line-height: var(--title-line-height); + --mdc-icon-size: var(--title-font-size); } .subtitle { color: var(--secondary-text-color); font-size: var(--subtitle-font-size); font-weight: var(--subtitle-font-weight); line-height: var(--subtitle-line-height); + --mdc-icon-size: var(--subtitle-font-size); } .align-start { text-align: start; @@ -2743,7 +2922,7 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl .align-justify { text-align: justify; } - `]}};n([ct()],nc.prototype,"_config",void 0),n([ct()],nc.prototype,"_templateResults",void 0),n([ct()],nc.prototype,"_unsubRenderTemplates",void 0),nc=n([at("mushroom-title-card")],nc);const oc=["update"],rc={on:"var(--rgb-state-update-on)",off:"var(--rgb-state-update-off)",installing:"var(--rgb-state-update-installing)"};let ac=class extends ot{constructor(){super(...arguments),this.fill=!1}_handleInstall(){this.hass.callService("update","install",{entity_id:this.entity.entity_id})}_handleSkip(t){t.stopPropagation(),this.hass.callService("update","skip",{entity_id:this.entity.entity_id})}get installDisabled(){if(!Dt(this.entity))return!0;const t=this.entity.attributes.latest_version&&this.entity.attributes.skipped_version===this.entity.attributes.latest_version;return!Lt(this.entity)&&!t||Vt(this.entity)}get skipDisabled(){if(!Dt(this.entity))return!0;return this.entity.attributes.latest_version&&this.entity.attributes.skipped_version===this.entity.attributes.latest_version||!Lt(this.entity)||Vt(this.entity)}render(){const t=pe(this.hass);return N` + `]}};n([mt()],Oc.prototype,"_config",void 0),n([mt()],Oc.prototype,"_templateResults",void 0),n([mt()],Oc.prototype,"_unsubRenderTemplates",void 0),Oc=n([dt("mushroom-title-card")],Oc);const Mc=["update"],Dc={on:"var(--rgb-state-update-on)",off:"var(--rgb-state-update-off)",installing:"var(--rgb-state-update-installing)"};let Lc=class extends st{constructor(){super(...arguments),this.fill=!1}_handleInstall(){this.hass.callService("update","install",{entity_id:this.entity.entity_id})}_handleSkip(t){t.stopPropagation(),this.hass.callService("update","skip",{entity_id:this.entity.entity_id})}get installDisabled(){if(!Rt(this.entity))return!0;const t=this.entity.attributes.latest_version&&this.entity.attributes.skipped_version===this.entity.attributes.latest_version;return!Nt(this.entity)&&!t||Yt(this.entity)}get skipDisabled(){if(!Rt(this.entity))return!0;return this.entity.attributes.latest_version&&this.entity.attributes.skipped_version===this.entity.attributes.latest_version||!Nt(this.entity)||Yt(this.entity)}render(){const t=Ee(this.hass);return B` - `}};n([st({attribute:!1})],ac.prototype,"hass",void 0),n([st({attribute:!1})],ac.prototype,"entity",void 0),n([st()],ac.prototype,"fill",void 0),ac=n([at("mushroom-update-buttons-control")],ac),ll({type:"mushroom-update-card",name:"Mushroom Update Card",description:"Card for update entity"});let lc=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return fp})),document.createElement("mushroom-update-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>oc.includes(t.split(".")[0])));return{type:"custom:mushroom-update-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return N``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",n=this._config.icon||pl(e),o=Ka(this._config),r=Ta(e,o.icon_type),a=pe(this.hass),l=(!this._config.collapsible_controls||Lt(e))&&this._config.show_buttons_control&&Nt(e,1);return N` - - + `}};n([ht({attribute:!1})],Lc.prototype,"hass",void 0),n([ht({attribute:!1})],Lc.prototype,"entity",void 0),n([ht()],Lc.prototype,"fill",void 0),Lc=n([dt("mushroom-update-buttons-control")],Lc),$l({type:"mushroom-update-card",name:"Mushroom Update Card",description:"Card for update entity"});let jc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return Vp})),document.createElement("mushroom-update-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Mc.includes(t.split(".")[0])));return{type:"custom:mushroom-update-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){if(!this._config||!this.hass||!this._config.entity)return B``;const t=this._config.entity,e=this.hass.states[t],i=this._config.name||e.attributes.friendly_name||"",o=this._config.icon||Dl(e),n=ml(this._config),r=qa(e,n.icon_type),a=Ee(this.hass),l=(!this._config.collapsible_controls||Nt(e))&&this._config.show_buttons_control&&Bt(e,1);return B` + + - ${r?this.renderPicture(r):this.renderIcon(e,n)} + ${r?this.renderPicture(r):this.renderIcon(e,o)} ${this.renderBadge(e)} - ${this.renderStateInfo(e,o,i)}; + ${this.renderStateInfo(e,n,i)}; - ${l?N` + ${l?B`
+ .fill=${"horizontal"!==n.layout} + >
`:null}
- `}renderIcon(t,e){const i=Vt(t),n=function(t,e){return e?rc.installing:rc[t]||"var(--rgb-grey)"}(t.state,i),o={"--icon-color":`rgb(${n})`,"--shape-color":`rgba(${n}, 0.2)`};return N` + `}renderIcon(t,e){const i=Yt(t),o=function(t,e){return e?Dc.installing:Dc[t]||"var(--rgb-grey)"}(t.state,i),n={"--icon-color":`rgb(${o})`,"--shape-color":`rgba(${o}, 0.2)`};return B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2802,35 +2981,35 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-update-buttons-control { flex: 1; } - `]}};n([ct()],lc.prototype,"_config",void 0),lc=n([at("mushroom-update-card")],lc);const sc=["vacuum"];function cc(t){switch(t.state){case"cleaning":case"on":return!0;default:return!1}}function dc(t){return"returning"===t.state}const uc=[{icon:"mdi:power",serviceName:"turn_on",isVisible:(t,e)=>Nt(t,1)&&e.includes("on_off")&&!Lt(t),isDisabled:()=>!1},{icon:"mdi:power",serviceName:"turn_off",isVisible:(t,e)=>Nt(t,2)&&e.includes("on_off")&&Lt(t),isDisabled:()=>!1},{icon:"mdi:play",serviceName:"start",isVisible:(t,e)=>Nt(t,8192)&&e.includes("start_pause")&&!cc(t),isDisabled:()=>!1},{icon:"mdi:pause",serviceName:"pause",isVisible:(t,e)=>Nt(t,8192)&&Nt(t,4)&&e.includes("start_pause")&&cc(t),isDisabled:()=>!1},{icon:"mdi:play-pause",serviceName:"start_pause",isVisible:(t,e)=>!Nt(t,8192)&&Nt(t,4)&&e.includes("start_pause"),isDisabled:()=>!1},{icon:"mdi:stop",serviceName:"stop",isVisible:(t,e)=>Nt(t,8)&&e.includes("stop"),isDisabled:t=>function(t){switch(t.state){case"docked":case"off":case"idle":case"returning":return!0;default:return!1}}(t)},{icon:"mdi:target-variant",serviceName:"clean_spot",isVisible:(t,e)=>Nt(t,1024)&&e.includes("clean_spot"),isDisabled:()=>!1},{icon:"mdi:map-marker",serviceName:"locate",isVisible:(t,e)=>Nt(t,512)&&e.includes("locate"),isDisabled:t=>dc(t)},{icon:"mdi:home-map-marker",serviceName:"return_to_base",isVisible:(t,e)=>Nt(t,16)&&e.includes("return_home"),isDisabled:()=>!1}];let hc=class extends ot{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.entry;this.hass.callService("vacuum",e.serviceName,{entity_id:this.entity.entity_id})}render(){const t=pe(this.hass);return N` + `]}};n([mt()],jc.prototype,"_config",void 0),jc=n([dt("mushroom-update-card")],jc);const Pc=["vacuum"];function Nc(t){switch(t.state){case"cleaning":case"on":return!0;default:return!1}}function Rc(t){return"returning"===t.state}const Vc=[{icon:"mdi:power",serviceName:"turn_on",isVisible:(t,e)=>Bt(t,1)&&e.includes("on_off")&&!Nt(t),isDisabled:()=>!1},{icon:"mdi:power",serviceName:"turn_off",isVisible:(t,e)=>Bt(t,2)&&e.includes("on_off")&&Nt(t),isDisabled:()=>!1},{icon:"mdi:play",serviceName:"start",isVisible:(t,e)=>Bt(t,8192)&&e.includes("start_pause")&&!Nc(t),isDisabled:()=>!1},{icon:"mdi:pause",serviceName:"pause",isVisible:(t,e)=>Bt(t,8192)&&Bt(t,4)&&e.includes("start_pause")&&Nc(t),isDisabled:()=>!1},{icon:"mdi:play-pause",serviceName:"start_pause",isVisible:(t,e)=>!Bt(t,8192)&&Bt(t,4)&&e.includes("start_pause"),isDisabled:()=>!1},{icon:"mdi:stop",serviceName:"stop",isVisible:(t,e)=>Bt(t,8)&&e.includes("stop"),isDisabled:t=>function(t){switch(t.state){case"docked":case"off":case"idle":case"returning":return!0;default:return!1}}(t)},{icon:"mdi:target-variant",serviceName:"clean_spot",isVisible:(t,e)=>Bt(t,1024)&&e.includes("clean_spot"),isDisabled:()=>!1},{icon:"mdi:map-marker",serviceName:"locate",isVisible:(t,e)=>Bt(t,512)&&e.includes("locate"),isDisabled:t=>Rc(t)},{icon:"mdi:home-map-marker",serviceName:"return_to_base",isVisible:(t,e)=>Bt(t,16)&&e.includes("return_home"),isDisabled:()=>!1}];let Fc=class extends st{constructor(){super(...arguments),this.fill=!1}callService(t){t.stopPropagation();const e=t.target.entry;this.hass.callService("vacuum",e.serviceName,{entity_id:this.entity.entity_id})}render(){const t=Ee(this.hass);return B` - ${uc.filter((t=>t.isVisible(this.entity,this.commands))).map((t=>N` + ${Vc.filter((t=>t.isVisible(this.entity,this.commands))).map((t=>B` `))} - `}};n([st({attribute:!1})],hc.prototype,"hass",void 0),n([st({attribute:!1})],hc.prototype,"entity",void 0),n([st({attribute:!1})],hc.prototype,"commands",void 0),n([st()],hc.prototype,"fill",void 0),hc=n([at("mushroom-vacuum-commands-control")],hc),ll({type:"mushroom-vacuum-card",name:"Mushroom Vacuum Card",description:"Card for vacuum entity"});let mc=class extends rl{static async getConfigElement(){return await Promise.resolve().then((function(){return xp})),document.createElement("mushroom-vacuum-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>sc.includes(t.split(".")[0])));return{type:"custom:mushroom-vacuum-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){Oe(this,this.hass,this._config,t.detail.action)}render(){var t,e;if(!this._config||!this.hass||!this._config.entity)return N``;const i=this._config.entity,n=this.hass.states[i],o=this._config.name||n.attributes.friendly_name||"",r=this._config.icon||pl(n),a=Ka(this._config),l=Ta(n,a.icon_type),s=pe(this.hass),c=null!==(e=null===(t=this._config)||void 0===t?void 0:t.commands)&&void 0!==e?e:[];return N` - + `}};n([ht({attribute:!1})],Fc.prototype,"hass",void 0),n([ht({attribute:!1})],Fc.prototype,"entity",void 0),n([ht({attribute:!1})],Fc.prototype,"commands",void 0),n([ht()],Fc.prototype,"fill",void 0),Fc=n([dt("mushroom-vacuum-commands-control")],Fc),$l({type:"mushroom-vacuum-card",name:"Mushroom Vacuum Card",description:"Card for vacuum entity"});let Bc=class extends kl{static async getConfigElement(){return await Promise.resolve().then((function(){return Xp})),document.createElement("mushroom-vacuum-card-editor")}static async getStubConfig(t){const e=Object.keys(t.states).filter((t=>Pc.includes(t.split(".")[0])));return{type:"custom:mushroom-vacuum-card",entity:e[0]}}getCardSize(){return 1}setConfig(t){this._config=Object.assign({tap_action:{action:"more-info"},hold_action:{action:"more-info"}},t)}_handleAction(t){He(this,this.hass,this._config,t.detail.action)}render(){var t,e;if(!this._config||!this.hass||!this._config.entity)return B``;const i=this._config.entity,o=this.hass.states[i],n=this._config.name||o.attributes.friendly_name||"",r=this._config.icon||Dl(o),a=ml(this._config),l=qa(o,a.icon_type),s=Ee(this.hass),c=null!==(e=null===(t=this._config)||void 0===t?void 0:t.commands)&&void 0!==e?e:[];return B` + - ${l?this.renderPicture(l):this.renderIcon(n,r)} - ${this.renderBadge(n)} - ${this.renderStateInfo(n,a,o)}; + ${l?this.renderPicture(l):this.renderIcon(o,r)} + ${this.renderBadge(o)} + ${this.renderStateInfo(o,a,n)}; - ${((t,e)=>uc.some((i=>i.isVisible(t,e))))(n,c)?N` + ${((t,e)=>Vc.some((i=>i.isVisible(t,e))))(o,c)?B`
@@ -2839,15 +3018,15 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl `:null} - `}renderIcon(t,e){var i,n;return N` + `}renderIcon(t,e){var i,o;return B` - `}static get styles(){return[super.styles,al,d` + `}static get styles(){return[super.styles,Cl,h` mushroom-state-item { cursor: pointer; } @@ -2864,20 +3043,20 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl mushroom-vacuum-commands-control { flex: 1; } - `]}};n([ct()],mc.prototype,"_config",void 0),mc=n([at("mushroom-vacuum-card")],mc),console.info("%c🍄 Mushroom 🍄 - 2.4.1","color: #ef5350; font-weight: 700;");const pc=ce({tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be)}),fc=(t,e)=>[{name:"tap_action",selector:He(t,2022,11)?{"ui-action":{actions:e}}:{"mush-action":{actions:e}}},{name:"hold_action",selector:He(t,2022,11)?{"ui-action":{actions:e}}:{"mush-action":{actions:e}}},{name:"double_tap_action",selector:He(t,2022,11)?{"ui-action":{actions:e}}:{"mush-action":{actions:e}}}],gc=ce({layout:de(me([le("horizontal"),le("vertical"),le("default")])),fill_container:de(re()),primary_info:de(ae(Aa)),secondary_info:de(ae(Aa)),icon_type:de(ae(Sa))}),_c=[{type:"grid",name:"",schema:[{name:"layout",selector:{"mush-layout":{}}},{name:"fill_container",selector:{boolean:{}}}]},{type:"grid",name:"",schema:[{name:"primary_info",selector:{"mush-info":{}}},{name:"secondary_info",selector:{"mush-info":{}}},{name:"icon_type",selector:{"mush-icon-type":{}}}]}],vc=["icon_color","layout","fill_container","primary_info","secondary_info","icon_type","content_info","use_entity_picture","collapsible_controls","icon_animation"],bc=t=>{var e,i;customElements.get("ha-form")&&(customElements.get("hui-action-editor")||He(t,2022,11))||null===(e=customElements.get("hui-button-card"))||void 0===e||e.getConfigElement(),customElements.get("ha-entity-picker")||null===(i=customElements.get("hui-entities-card"))||void 0===i||i.getConfigElement()},yc=ce({entity:de(ue()),name:de(ue()),icon:de(ue())}),xc=ce({index:de(se()),view_index:de(se()),view_layout:ne(),type:ue()}),wc=te(xc,te(yc,gc,pc),ce({states:de(oe()),show_keypad:de(re())})),kc=["more-info","navigate","url","call-service","none"],Cc=["armed_home","armed_away","armed_night","armed_vacation","armed_custom_bypass"],$c=["show_keypad"],Ec=_t(((t,e,i)=>[{name:"entity",selector:{entity:{domain:fl}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:i}}},..._c,{type:"multi_select",name:"states",options:Cc.map((e=>[e,t(`ui.card.alarm_control_panel.${e.replace("armed","arm")}`)]))},{name:"show_keypad",selector:{boolean:{}}},...fc(e,kc)]));let Ac=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):$c.includes(t.name)?e(`editor.card.alarm_control_panel.${t.name}`):"states"===t.name?this.hass.localize("ui.panel.lovelace.editor.card.alarm-panel.available_states"):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,wc),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Ec(this.hass.localize,this.hass.connection.haVersion,i);return N` + `]}};n([mt()],Bc.prototype,"_config",void 0),Bc=n([dt("mushroom-vacuum-card")],Bc),console.info("%c🍄 Mushroom 🍄 - 2.6.3","color: #ef5350; font-weight: 700;");const Uc=xe({tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti)}),Hc=t=>[{name:"tap_action",selector:{"ui-action":{actions:t}}},{name:"hold_action",selector:{"ui-action":{actions:t}}},{name:"double_tap_action",selector:{"ui-action":{actions:t}}}],Yc=xe({layout:we($e([be("horizontal"),be("vertical"),be("default")])),fill_container:we(_e()),primary_info:we(ve(Ya)),secondary_info:we(ve(Ya)),icon_type:we(ve(Xa))}),Xc=[{type:"grid",name:"",schema:[{name:"layout",selector:{"mush-layout":{}}},{name:"fill_container",selector:{boolean:{}}}]},{type:"grid",name:"",schema:[{name:"primary_info",selector:{"mush-info":{}}},{name:"secondary_info",selector:{"mush-info":{}}},{name:"icon_type",selector:{"mush-icon-type":{}}}]}],Wc=["icon_color","layout","fill_container","primary_info","secondary_info","icon_type","content_info","use_entity_picture","collapsible_controls","icon_animation"],qc=()=>{var t,e;customElements.get("ha-form")||null===(t=customElements.get("hui-button-card"))||void 0===t||t.getConfigElement(),customElements.get("ha-entity-picker")||null===(e=customElements.get("hui-entities-card"))||void 0===e||e.getConfigElement()},Kc=xe({entity:we(ke()),name:we(ke()),icon:we(ke())}),Gc=xe({index:we(ye()),view_index:we(ye()),view_layout:fe(),type:ke()}),Zc=he(Gc,he(Kc,Yc,Uc),xe({states:we(ge()),show_keypad:we(_e())})),Jc=["more-info","navigate","url","call-service","none"],Qc=["armed_home","armed_away","armed_night","armed_vacation","armed_custom_bypass"],td=["show_keypad"],ed=xt(((t,e)=>[{name:"entity",selector:{entity:{domain:Ll}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},...Xc,{type:"multi_select",name:"states",options:Qc.map((e=>[e,t(`ui.card.alarm_control_panel.${e.replace("armed","arm")}`)]))},{name:"show_keypad",selector:{boolean:{}}},...Hc(Jc)]));let id=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):td.includes(t.name)?e(`editor.card.alarm_control_panel.${t.name}`):"states"===t.name?this.hass.localize("ui.panel.lovelace.editor.card.alarm-panel.available_states"):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Zc),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=ed(this.hass.localize,i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Ac.prototype,"_config",void 0),Ac=n([at("mushroom-alarm-control-panel-card-editor")],Ac);var Sc=Object.freeze({__proto__:null,get SwitchCardEditor(){return Ac}}); + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],id.prototype,"_config",void 0),id=n([dt("mushroom-alarm-control-panel-card-editor")],id);var od=Object.freeze({__proto__:null,get SwitchCardEditor(){return id}}); /** * @license * Copyright 2021 Google LLC * SPDX-LIcense-Identifier: Apache-2.0 - */const Ic=d`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px;z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-text-field--filled{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-text-field--filled .mdc-text-field__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-text-field--filled .mdc-text-field__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-text-field--filled.mdc-ripple-upgraded--unbounded .mdc-text-field__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-activation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-deactivation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-text-field__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-text-field{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0;display:inline-flex;align-items:baseline;padding:0 16px;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input{color:rgba(0, 0, 0, 0.87)}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.54)}}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.54)}}.mdc-text-field .mdc-text-field__input{caret-color:#6200ee;caret-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field-character-counter,.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-text-field__input{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);width:100%;min-width:0;border:none;border-radius:0;background:none;appearance:none;padding:0}.mdc-text-field__input::-ms-clear{display:none}.mdc-text-field__input::-webkit-calendar-picker-indicator{display:none}.mdc-text-field__input:focus{outline:none}.mdc-text-field__input:invalid{box-shadow:none}@media all{.mdc-text-field__input::placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mdc-text-field--focused .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}@media all{.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}.mdc-text-field__affix{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0;white-space:nowrap}.mdc-text-field--label-floating .mdc-text-field__affix,.mdc-text-field--no-label .mdc-text-field__affix{opacity:1}@supports(-webkit-hyphens: none){.mdc-text-field--outlined .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field__affix--prefix,.mdc-text-field__affix--prefix[dir=rtl]{padding-left:2px;padding-right:0}.mdc-text-field--end-aligned .mdc-text-field__affix--prefix{padding-left:0;padding-right:12px}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--end-aligned .mdc-text-field__affix--prefix[dir=rtl]{padding-left:12px;padding-right:0}.mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field__affix--suffix,.mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:12px}.mdc-text-field--end-aligned .mdc-text-field__affix--suffix{padding-left:2px;padding-right:0}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--end-aligned .mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:2px}.mdc-text-field--filled{height:56px}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-text-field--filled:hover .mdc-text-field__ripple::before,.mdc-text-field--filled.mdc-ripple-surface--hover .mdc-text-field__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-text-field--filled.mdc-ripple-upgraded--background-focused .mdc-text-field__ripple::before,.mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-text-field--filled::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-text-field--filled:not(.mdc-text-field--disabled){background-color:whitesmoke}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-text-field--filled:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--filled .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-text-field--filled .mdc-floating-label,.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{height:100%}.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label{display:none}.mdc-text-field--filled.mdc-text-field--no-label::before{display:none}@supports(-webkit-hyphens: none){.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field--outlined{height:56px;overflow:visible}.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--outlined .mdc-text-field__input{height:100%}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-text-field--outlined{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined{padding-right:max(16px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-right:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-left:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-right:max(16px, var(--mdc-shape-small, 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-right:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-right:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-text-field--outlined .mdc-text-field__ripple::before,.mdc-text-field--outlined .mdc-text-field__ripple::after{content:none}.mdc-text-field--outlined .mdc-floating-label{left:4px;right:initial}[dir=rtl] .mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-text-field--outlined .mdc-text-field__input{display:flex;border:none !important;background-color:transparent}.mdc-text-field--outlined .mdc-notched-outline{z-index:1}.mdc-text-field--textarea{flex-direction:column;align-items:center;width:auto;height:auto;padding:0;transition:none}.mdc-text-field--textarea .mdc-floating-label{top:19px}.mdc-text-field--textarea .mdc-floating-label:not(.mdc-floating-label--float-above){transform:none}.mdc-text-field--textarea .mdc-text-field__input{flex-grow:1;height:auto;min-height:1.5rem;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;resize:none;padding:0 16px;line-height:1.5rem}.mdc-text-field--textarea.mdc-text-field--filled::before{display:none}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-10.25px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-filled 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-filled{0%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-10.25px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-10.25px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--filled .mdc-text-field__input{margin-top:23px;margin-bottom:9px}.mdc-text-field--textarea.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-27.25px) scale(1)}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-24.75px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-24.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-24.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label{top:18px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field__input{margin-bottom:2px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter{align-self:flex-end;padding:0 16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::after{display:inline-block;width:0;height:16px;content:"";vertical-align:-16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::before{display:none}.mdc-text-field__resizer{align-self:stretch;display:inline-flex;flex-direction:column;flex-grow:1;max-height:100%;max-width:100%;min-height:56px;min-width:fit-content;min-width:-moz-available;min-width:-webkit-fill-available;overflow:hidden;resize:both}.mdc-text-field--filled .mdc-text-field__resizer{transform:translateY(-1px)}.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateY(1px)}.mdc-text-field--outlined .mdc-text-field__resizer{transform:translateX(-1px) translateY(-1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer,.mdc-text-field--outlined .mdc-text-field__resizer[dir=rtl]{transform:translateX(1px) translateY(-1px)}.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateX(1px) translateY(1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input[dir=rtl],.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter[dir=rtl]{transform:translateX(-1px) translateY(1px)}.mdc-text-field--with-leading-icon{padding-left:0;padding-right:16px}[dir=rtl] .mdc-text-field--with-leading-icon,.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:16px;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 48px);left:48px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-text-field--with-leading-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake,.mdc-text-field--with-leading-icon.mdc-text-field--outlined[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--with-trailing-icon{padding-left:16px;padding-right:0}[dir=rtl] .mdc-text-field--with-trailing-icon,.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-trailing-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-text-field-helper-line{display:flex;justify-content:space-between;box-sizing:border-box}.mdc-text-field+.mdc-text-field-helper-line{padding-right:16px;padding-left:16px}.mdc-form-field>.mdc-text-field+label{align-self:flex-start}.mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--focused .mdc-notched-outline__trailing{border-width:2px}.mdc-text-field--focused+.mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg){opacity:1}.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-text-field--focused.mdc-text-field--outlined.mdc-text-field--textarea .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid .mdc-text-field__input{caret-color:#b00020;caret-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{opacity:1}.mdc-text-field--disabled{pointer-events:none}.mdc-text-field--disabled .mdc-text-field__input{color:rgba(0, 0, 0, 0.38)}@media all{.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.38)}}@media all{.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.38)}}.mdc-text-field--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-floating-label{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--leading{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:GrayText}}@media screen and (forced-colors: active){.mdc-text-field--disabled .mdc-text-field__input{background-color:Window}.mdc-text-field--disabled .mdc-floating-label{z-index:1}}.mdc-text-field--disabled .mdc-floating-label{cursor:default}.mdc-text-field--disabled.mdc-text-field--filled{background-color:#fafafa}.mdc-text-field--disabled.mdc-text-field--filled .mdc-text-field__ripple{display:none}.mdc-text-field--disabled .mdc-text-field__input{pointer-events:auto}.mdc-text-field--end-aligned .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--end-aligned .mdc-text-field__input[dir=rtl]{text-align:left}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix{direction:ltr}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--leading,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--leading{order:1}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{order:2}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input{order:3}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{order:4}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--trailing,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--trailing{order:5}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--prefix{padding-right:12px}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--suffix{padding-left:2px}.mdc-text-field-helper-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;opacity:0;will-change:opacity;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-text-field-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-text-field-helper-text--persistent{transition:none;opacity:1;will-change:initial}.mdc-text-field-character-counter{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-left:auto;margin-right:0;padding-left:16px;padding-right:0;white-space:nowrap}.mdc-text-field-character-counter::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field__icon{align-self:center;cursor:pointer}.mdc-text-field__icon:not([tabindex]),.mdc-text-field__icon[tabindex="-1"]{cursor:default;pointer-events:none}.mdc-text-field__icon svg{display:block}.mdc-text-field__icon--leading{margin-left:16px;margin-right:8px}[dir=rtl] .mdc-text-field__icon--leading,.mdc-text-field__icon--leading[dir=rtl]{margin-left:8px;margin-right:16px}.mdc-text-field__icon--trailing{padding:12px;margin-left:0px;margin-right:0px}[dir=rtl] .mdc-text-field__icon--trailing,.mdc-text-field__icon--trailing[dir=rtl]{margin-left:0px;margin-right:0px}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-flex;flex-direction:column;outline:none}.mdc-text-field{width:100%}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42))}.mdc-text-field:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-text-field-hover-line-color, rgba(0, 0, 0, 0.87))}.mdc-text-field.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06);border-bottom-color:var(--mdc-text-field-disabled-line-color, rgba(0, 0, 0, 0.06))}.mdc-text-field.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field__input{direction:inherit}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-idle-border-color, rgba(0, 0, 0, 0.38) )}:host(:not([disabled]):hover) :not(.mdc-text-field--invalid):not(.mdc-text-field--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-error-color, var(--mdc-theme-error, #b00020) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-character-counter,:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid .mdc-text-field__icon{color:var(--mdc-text-field-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input{color:var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg),:host(:not([disabled])) .mdc-text-field-helper-line:not(.mdc-text-field--invalid) .mdc-text-field-character-counter{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-text-field.mdc-text-field--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field .mdc-text-field__input,:host([disabled]) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-helper-text,:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-character-counter{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}` + */const nd=h`.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);position:absolute;left:0;-webkit-transform-origin:left top;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform;transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-floating-label,.mdc-floating-label[dir=rtl]{right:0;left:auto;-webkit-transform-origin:right top;transform-origin:right top;text-align:right}.mdc-floating-label--float-above{cursor:auto}.mdc-floating-label--required::after{margin-left:1px;margin-right:0px;content:"*"}[dir=rtl] .mdc-floating-label--required::after,.mdc-floating-label--required[dir=rtl]::after{margin-left:0;margin-right:1px}.mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-standard 250ms 1}@keyframes mdc-floating-label-shake-float-above-standard{0%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-106%) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-106%) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-106%) scale(0.75)}}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{border-bottom-width:1px}.mdc-line-ripple::before{z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-text-field--filled{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-text-field--filled .mdc-text-field__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-text-field--filled .mdc-text-field__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-text-field--filled.mdc-ripple-upgraded--unbounded .mdc-text-field__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-activation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-text-field--filled.mdc-ripple-upgraded--foreground-deactivation .mdc-text-field__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-text-field--filled.mdc-ripple-upgraded .mdc-text-field__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-text-field__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-text-field{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0;display:inline-flex;align-items:baseline;padding:0 16px;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input{color:rgba(0, 0, 0, 0.87)}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.54)}}@media all{.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.54)}}.mdc-text-field .mdc-text-field__input{caret-color:#6200ee;caret-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field-character-counter,.mdc-text-field:not(.mdc-text-field--disabled)+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.54)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.6)}.mdc-text-field .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-text-field__input{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);width:100%;min-width:0;border:none;border-radius:0;background:none;appearance:none;padding:0}.mdc-text-field__input::-ms-clear{display:none}.mdc-text-field__input::-webkit-calendar-picker-indicator{display:none}.mdc-text-field__input:focus{outline:none}.mdc-text-field__input:invalid{box-shadow:none}@media all{.mdc-text-field__input::placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0}}@media all{.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mdc-text-field--focused .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}@media all{.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}}.mdc-text-field__affix{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);height:28px;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0;white-space:nowrap}.mdc-text-field--label-floating .mdc-text-field__affix,.mdc-text-field--no-label .mdc-text-field__affix{opacity:1}@supports(-webkit-hyphens: none){.mdc-text-field--outlined .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field__affix--prefix,.mdc-text-field__affix--prefix[dir=rtl]{padding-left:2px;padding-right:0}.mdc-text-field--end-aligned .mdc-text-field__affix--prefix{padding-left:0;padding-right:12px}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--end-aligned .mdc-text-field__affix--prefix[dir=rtl]{padding-left:12px;padding-right:0}.mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field__affix--suffix,.mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:12px}.mdc-text-field--end-aligned .mdc-text-field__affix--suffix{padding-left:2px;padding-right:0}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--end-aligned .mdc-text-field__affix--suffix[dir=rtl]{padding-left:0;padding-right:2px}.mdc-text-field--filled{height:56px}.mdc-text-field--filled .mdc-text-field__ripple::before,.mdc-text-field--filled .mdc-text-field__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-text-field--filled:hover .mdc-text-field__ripple::before,.mdc-text-field--filled.mdc-ripple-surface--hover .mdc-text-field__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-text-field--filled.mdc-ripple-upgraded--background-focused .mdc-text-field__ripple::before,.mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-text-field--filled::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-text-field--filled:not(.mdc-text-field--disabled){background-color:whitesmoke}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-text-field--filled:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--filled .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-text-field--filled .mdc-floating-label,.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{height:100%}.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label{display:none}.mdc-text-field--filled.mdc-text-field--no-label::before{display:none}@supports(-webkit-hyphens: none){.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__affix{align-items:center;align-self:center;display:inline-flex;height:100%}}.mdc-text-field--outlined{height:56px;overflow:visible}.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--outlined .mdc-text-field__input{height:100%}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-text-field--outlined{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined{padding-right:max(16px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-text-field--outlined+.mdc-text-field-helper-line{padding-right:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-left:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-leading-icon{padding-right:max(16px, var(--mdc-shape-small, 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-right:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-leading-icon,.mdc-text-field--outlined.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:max(16px, var(--mdc-shape-small, 4px))}}.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-right:0}@supports(top: max(0%)){.mdc-text-field--outlined.mdc-text-field--with-trailing-icon{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-text-field--outlined.mdc-text-field--with-trailing-icon,.mdc-text-field--outlined.mdc-text-field--with-trailing-icon[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-text-field--outlined.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-text-field--outlined .mdc-text-field__ripple::before,.mdc-text-field--outlined .mdc-text-field__ripple::after{background-color:transparent;background-color:var(--mdc-ripple-color, transparent)}.mdc-text-field--outlined .mdc-floating-label{left:4px;right:initial}[dir=rtl] .mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-text-field--outlined .mdc-text-field__input{display:flex;border:none !important;background-color:transparent}.mdc-text-field--outlined .mdc-notched-outline{z-index:1}.mdc-text-field--textarea{flex-direction:column;align-items:center;width:auto;height:auto;padding:0;transition:none}.mdc-text-field--textarea .mdc-floating-label{top:19px}.mdc-text-field--textarea .mdc-floating-label:not(.mdc-floating-label--float-above){transform:none}.mdc-text-field--textarea .mdc-text-field__input{flex-grow:1;height:auto;min-height:1.5rem;overflow-x:hidden;overflow-y:auto;box-sizing:border-box;resize:none;padding:0 16px;line-height:1.5rem}.mdc-text-field--textarea.mdc-text-field--filled::before{display:none}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-10.25px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--filled .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-filled 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-filled{0%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-10.25px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-10.25px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-10.25px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--filled .mdc-text-field__input{margin-top:23px;margin-bottom:9px}.mdc-text-field--textarea.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-27.25px) scale(1)}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-24.75px) scale(0.75)}.mdc-text-field--textarea.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--textarea.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-textarea-outlined 250ms 1}@keyframes mdc-floating-label-shake-float-above-textarea-outlined{0%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-24.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-24.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-24.75px) scale(0.75)}}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-text-field__input{margin-top:16px;margin-bottom:16px}.mdc-text-field--textarea.mdc-text-field--outlined .mdc-floating-label{top:18px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field__input{margin-bottom:2px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter{align-self:flex-end;padding:0 16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::after{display:inline-block;width:0;height:16px;content:"";vertical-align:-16px}.mdc-text-field--textarea.mdc-text-field--with-internal-counter .mdc-text-field-character-counter::before{display:none}.mdc-text-field__resizer{align-self:stretch;display:inline-flex;flex-direction:column;flex-grow:1;max-height:100%;max-width:100%;min-height:56px;min-width:fit-content;min-width:-moz-available;min-width:-webkit-fill-available;overflow:hidden;resize:both}.mdc-text-field--filled .mdc-text-field__resizer{transform:translateY(-1px)}.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--filled .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateY(1px)}.mdc-text-field--outlined .mdc-text-field__resizer{transform:translateX(-1px) translateY(-1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer,.mdc-text-field--outlined .mdc-text-field__resizer[dir=rtl]{transform:translateX(1px) translateY(-1px)}.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter{transform:translateX(1px) translateY(1px)}[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input,[dir=rtl] .mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter,.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field__input[dir=rtl],.mdc-text-field--outlined .mdc-text-field__resizer .mdc-text-field-character-counter[dir=rtl]{transform:translateX(-1px) translateY(1px)}.mdc-text-field--with-leading-icon{padding-left:0;padding-right:16px}[dir=rtl] .mdc-text-field--with-leading-icon,.mdc-text-field--with-leading-icon[dir=rtl]{padding-left:16px;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 48px);left:48px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-text-field--with-leading-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-text-field--with-leading-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--float-above{font-size:.75rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-text-field--with-leading-icon.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-text-field--with-leading-icon.mdc-text-field--outlined .mdc-floating-label--shake,.mdc-text-field--with-leading-icon.mdc-text-field--outlined[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-text-field-outlined-leading-icon 250ms 1}@keyframes mdc-floating-label-shake-float-above-text-field-outlined-leading-icon-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-text-field--with-trailing-icon{padding-left:16px;padding-right:0}[dir=rtl] .mdc-text-field--with-trailing-icon,.mdc-text-field--with-trailing-icon[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-text-field--with-trailing-icon.mdc-text-field--outlined :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon{padding-left:0;padding-right:0}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon.mdc-text-field--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-text-field-helper-line{display:flex;justify-content:space-between;box-sizing:border-box}.mdc-text-field+.mdc-text-field-helper-line{padding-right:16px;padding-left:16px}.mdc-form-field>.mdc-text-field+label{align-self:flex-start}.mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--focused .mdc-notched-outline__trailing{border-width:2px}.mdc-text-field--focused+.mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg){opacity:1}.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-text-field--focused.mdc-text-field--outlined.mdc-text-field--textarea .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0}.mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid .mdc-text-field__input{caret-color:#b00020;caret-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__leading,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__notch,.mdc-text-field--invalid:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-helper-text--validation-msg{opacity:1}.mdc-text-field--disabled{pointer-events:none}.mdc-text-field--disabled .mdc-text-field__input{color:rgba(0, 0, 0, 0.38)}@media all{.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:rgba(0, 0, 0, 0.38)}}@media all{.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:rgba(0, 0, 0, 0.38)}}.mdc-text-field--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__icon--leading{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:rgba(0, 0, 0, 0.3)}.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:rgba(0, 0, 0, 0.38)}.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input::placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__input:-ms-input-placeholder{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-floating-label{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-helper-text{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field-character-counter,.mdc-text-field--disabled+.mdc-text-field-helper-line .mdc-text-field-character-counter{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--leading{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__icon--trailing{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--prefix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-text-field__affix--suffix{color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-text-field--disabled .mdc-notched-outline__leading,.mdc-text-field--disabled .mdc-notched-outline__notch,.mdc-text-field--disabled .mdc-notched-outline__trailing{border-color:GrayText}}@media screen and (forced-colors: active){.mdc-text-field--disabled .mdc-text-field__input{background-color:Window}.mdc-text-field--disabled .mdc-floating-label{z-index:1}}.mdc-text-field--disabled .mdc-floating-label{cursor:default}.mdc-text-field--disabled.mdc-text-field--filled{background-color:#fafafa}.mdc-text-field--disabled.mdc-text-field--filled .mdc-text-field__ripple{display:none}.mdc-text-field--disabled .mdc-text-field__input{pointer-events:auto}.mdc-text-field--end-aligned .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--end-aligned .mdc-text-field__input[dir=rtl]{text-align:left}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix{direction:ltr}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{padding-left:0;padding-right:2px}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{padding-left:12px;padding-right:0}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--leading,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--leading{order:1}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--suffix{order:2}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__input,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__input{order:3}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__affix--prefix{order:4}[dir=rtl] .mdc-text-field--ltr-text .mdc-text-field__icon--trailing,.mdc-text-field--ltr-text[dir=rtl] .mdc-text-field__icon--trailing{order:5}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__input,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__input{text-align:right}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--prefix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--prefix{padding-right:12px}[dir=rtl] .mdc-text-field--ltr-text.mdc-text-field--end-aligned .mdc-text-field__affix--suffix,.mdc-text-field--ltr-text.mdc-text-field--end-aligned[dir=rtl] .mdc-text-field__affix--suffix{padding-left:2px}.mdc-text-field-helper-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin:0;opacity:0;will-change:opacity;transition:opacity 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-text-field-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-text-field-helper-text--persistent{transition:none;opacity:1;will-change:initial}.mdc-text-field-character-counter{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal;margin-left:auto;margin-right:0;padding-left:16px;padding-right:0;white-space:nowrap}.mdc-text-field-character-counter::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-text-field-character-counter,.mdc-text-field-character-counter[dir=rtl]{padding-left:0;padding-right:16px}.mdc-text-field__icon{align-self:center;cursor:pointer}.mdc-text-field__icon:not([tabindex]),.mdc-text-field__icon[tabindex="-1"]{cursor:default;pointer-events:none}.mdc-text-field__icon svg{display:block}.mdc-text-field__icon--leading{margin-left:16px;margin-right:8px}[dir=rtl] .mdc-text-field__icon--leading,.mdc-text-field__icon--leading[dir=rtl]{margin-left:8px;margin-right:16px}.mdc-text-field__icon--trailing{padding:12px;margin-left:0px;margin-right:0px}[dir=rtl] .mdc-text-field__icon--trailing,.mdc-text-field__icon--trailing[dir=rtl]{margin-left:0px;margin-right:0px}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-flex;flex-direction:column;outline:none}.mdc-text-field{width:100%}.mdc-text-field:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42))}.mdc-text-field:not(.mdc-text-field--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-text-field-hover-line-color, rgba(0, 0, 0, 0.87))}.mdc-text-field.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06);border-bottom-color:var(--mdc-text-field-disabled-line-color, rgba(0, 0, 0, 0.06))}.mdc-text-field.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-text-field__input{direction:inherit}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-idle-border-color, rgba(0, 0, 0, 0.38) )}:host(:not([disabled]):hover) :not(.mdc-text-field--invalid):not(.mdc-text-field--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-error-color, var(--mdc-theme-error, #b00020) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid+.mdc-text-field-helper-line .mdc-text-field-character-counter,:host(:not([disabled])) .mdc-text-field.mdc-text-field--invalid .mdc-text-field__icon{color:var(--mdc-text-field-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input{color:var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-text-field-helper-line .mdc-text-field-helper-text:not(.mdc-text-field-helper-text--validation-msg),:host(:not([disabled])) .mdc-text-field-helper-line:not(.mdc-text-field--invalid) .mdc-text-field-character-counter{color:var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]) .mdc-text-field:not(.mdc-text-field--outlined){background-color:var(--mdc-text-field-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-text-field.mdc-text-field--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-text-field-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label,:host([disabled]) .mdc-text-field:not(.mdc-text-field--invalid):not(.mdc-text-field--focused) .mdc-floating-label::after{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field .mdc-text-field__input,:host([disabled]) .mdc-text-field .mdc-text-field__input::placeholder{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-helper-text,:host([disabled]) .mdc-text-field-helper-line .mdc-text-field-character-counter{color:var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38))}` /** * @license * Copyright 2016 Google Inc. @@ -2899,7 +3078,7 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */;var Tc=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),zc={ARIA_CONTROLS:"aria-controls",ARIA_DESCRIBEDBY:"aria-describedby",INPUT_SELECTOR:".mdc-text-field__input",LABEL_SELECTOR:".mdc-floating-label",LEADING_ICON_SELECTOR:".mdc-text-field__icon--leading",LINE_RIPPLE_SELECTOR:".mdc-line-ripple",OUTLINE_SELECTOR:".mdc-notched-outline",PREFIX_SELECTOR:".mdc-text-field__affix--prefix",SUFFIX_SELECTOR:".mdc-text-field__affix--suffix",TRAILING_ICON_SELECTOR:".mdc-text-field__icon--trailing"},Oc={DISABLED:"mdc-text-field--disabled",FOCUSED:"mdc-text-field--focused",HELPER_LINE:"mdc-text-field-helper-line",INVALID:"mdc-text-field--invalid",LABEL_FLOATING:"mdc-text-field--label-floating",NO_LABEL:"mdc-text-field--no-label",OUTLINED:"mdc-text-field--outlined",ROOT:"mdc-text-field",TEXTAREA:"mdc-text-field--textarea",WITH_LEADING_ICON:"mdc-text-field--with-leading-icon",WITH_TRAILING_ICON:"mdc-text-field--with-trailing-icon",WITH_INTERNAL_COUNTER:"mdc-text-field--with-internal-counter"},Mc={LABEL_SCALE:.75},Lc=["pattern","min","max","required","step","minlength","maxlength"],Dc=["color","date","datetime-local","month","range","time","week"],jc=["mousedown","touchstart"],Pc=["click","keydown"],Nc=function(t){function n(e,o){void 0===o&&(o={});var r=t.call(this,i(i({},n.defaultAdapter),e))||this;return r.isFocused=!1,r.receivedUserInput=!1,r.valid=!0,r.useNativeValidation=!0,r.validateOnValueChange=!0,r.helperText=o.helperText,r.characterCounter=o.characterCounter,r.leadingIcon=o.leadingIcon,r.trailingIcon=o.trailingIcon,r.inputFocusHandler=function(){r.activateFocus()},r.inputBlurHandler=function(){r.deactivateFocus()},r.inputInputHandler=function(){r.handleInput()},r.setPointerXOffset=function(t){r.setTransformOrigin(t)},r.textFieldInteractionHandler=function(){r.handleTextFieldInteraction()},r.validationAttributeChangeHandler=function(t){r.handleValidationAttributeChange(t)},r}return e(n,t),Object.defineProperty(n,"cssClasses",{get:function(){return Oc},enumerable:!1,configurable:!0}),Object.defineProperty(n,"strings",{get:function(){return zc},enumerable:!1,configurable:!0}),Object.defineProperty(n,"numbers",{get:function(){return Mc},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldAlwaysFloat",{get:function(){var t=this.getNativeInput().type;return Dc.indexOf(t)>=0},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldFloat",{get:function(){return this.shouldAlwaysFloat||this.isFocused||!!this.getValue()||this.isBadInput()},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"shouldShake",{get:function(){return!this.isFocused&&!this.isValid()&&!!this.getValue()},enumerable:!1,configurable:!0}),Object.defineProperty(n,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!0},setInputAttr:function(){},removeInputAttr:function(){},registerTextFieldInteractionHandler:function(){},deregisterTextFieldInteractionHandler:function(){},registerInputInteractionHandler:function(){},deregisterInputInteractionHandler:function(){},registerValidationAttributeChangeHandler:function(){return new MutationObserver((function(){}))},deregisterValidationAttributeChangeHandler:function(){},getNativeInput:function(){return null},isFocused:function(){return!1},activateLineRipple:function(){},deactivateLineRipple:function(){},setLineRippleTransformOrigin:function(){},shakeLabel:function(){},floatLabel:function(){},setLabelRequired:function(){},hasLabel:function(){return!1},getLabelWidth:function(){return 0},hasOutline:function(){return!1},notchOutline:function(){},closeOutline:function(){}}},enumerable:!1,configurable:!0}),n.prototype.init=function(){var t,e,i,n;this.adapter.hasLabel()&&this.getNativeInput().required&&this.adapter.setLabelRequired(!0),this.adapter.isFocused()?this.inputFocusHandler():this.adapter.hasLabel()&&this.shouldFloat&&(this.notchOutline(!0),this.adapter.floatLabel(!0),this.styleFloating(!0)),this.adapter.registerInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.registerInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.registerInputInteractionHandler("input",this.inputInputHandler);try{for(var r=o(jc),a=r.next();!a.done;a=r.next()){var l=a.value;this.adapter.registerInputInteractionHandler(l,this.setPointerXOffset)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=r.return)&&e.call(r)}finally{if(t)throw t.error}}try{for(var s=o(Pc),c=s.next();!c.done;c=s.next()){l=c.value;this.adapter.registerTextFieldInteractionHandler(l,this.textFieldInteractionHandler)}}catch(t){i={error:t}}finally{try{c&&!c.done&&(n=s.return)&&n.call(s)}finally{if(i)throw i.error}}this.validationObserver=this.adapter.registerValidationAttributeChangeHandler(this.validationAttributeChangeHandler),this.setcharacterCounter(this.getValue().length)},n.prototype.destroy=function(){var t,e,i,n;this.adapter.deregisterInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.deregisterInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.deregisterInputInteractionHandler("input",this.inputInputHandler);try{for(var r=o(jc),a=r.next();!a.done;a=r.next()){var l=a.value;this.adapter.deregisterInputInteractionHandler(l,this.setPointerXOffset)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=r.return)&&e.call(r)}finally{if(t)throw t.error}}try{for(var s=o(Pc),c=s.next();!c.done;c=s.next()){l=c.value;this.adapter.deregisterTextFieldInteractionHandler(l,this.textFieldInteractionHandler)}}catch(t){i={error:t}}finally{try{c&&!c.done&&(n=s.return)&&n.call(s)}finally{if(i)throw i.error}}this.adapter.deregisterValidationAttributeChangeHandler(this.validationObserver)},n.prototype.handleTextFieldInteraction=function(){var t=this.adapter.getNativeInput();t&&t.disabled||(this.receivedUserInput=!0)},n.prototype.handleValidationAttributeChange=function(t){var e=this;t.some((function(t){return Lc.indexOf(t)>-1&&(e.styleValidity(!0),e.adapter.setLabelRequired(e.getNativeInput().required),!0)})),t.indexOf("maxlength")>-1&&this.setcharacterCounter(this.getValue().length)},n.prototype.notchOutline=function(t){if(this.adapter.hasOutline()&&this.adapter.hasLabel())if(t){var e=this.adapter.getLabelWidth()*Mc.LABEL_SCALE;this.adapter.notchOutline(e)}else this.adapter.closeOutline()},n.prototype.activateFocus=function(){this.isFocused=!0,this.styleFocused(this.isFocused),this.adapter.activateLineRipple(),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),!this.helperText||!this.helperText.isPersistent()&&this.helperText.isValidation()&&this.valid||this.helperText.showToScreenReader()},n.prototype.setTransformOrigin=function(t){if(!this.isDisabled()&&!this.adapter.hasOutline()){var e=t.touches,i=e?e[0]:t,n=i.target.getBoundingClientRect(),o=i.clientX-n.left;this.adapter.setLineRippleTransformOrigin(o)}},n.prototype.handleInput=function(){this.autoCompleteFocus(),this.setcharacterCounter(this.getValue().length)},n.prototype.autoCompleteFocus=function(){this.receivedUserInput||this.activateFocus()},n.prototype.deactivateFocus=function(){this.isFocused=!1,this.adapter.deactivateLineRipple();var t=this.isValid();this.styleValidity(t),this.styleFocused(this.isFocused),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),this.shouldFloat||(this.receivedUserInput=!1)},n.prototype.getValue=function(){return this.getNativeInput().value},n.prototype.setValue=function(t){if(this.getValue()!==t&&(this.getNativeInput().value=t),this.setcharacterCounter(t.length),this.validateOnValueChange){var e=this.isValid();this.styleValidity(e)}this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.validateOnValueChange&&this.adapter.shakeLabel(this.shouldShake))},n.prototype.isValid=function(){return this.useNativeValidation?this.isNativeInputValid():this.valid},n.prototype.setValid=function(t){this.valid=t,this.styleValidity(t);var e=!t&&!this.isFocused&&!!this.getValue();this.adapter.hasLabel()&&this.adapter.shakeLabel(e)},n.prototype.setValidateOnValueChange=function(t){this.validateOnValueChange=t},n.prototype.getValidateOnValueChange=function(){return this.validateOnValueChange},n.prototype.setUseNativeValidation=function(t){this.useNativeValidation=t},n.prototype.isDisabled=function(){return this.getNativeInput().disabled},n.prototype.setDisabled=function(t){this.getNativeInput().disabled=t,this.styleDisabled(t)},n.prototype.setHelperTextContent=function(t){this.helperText&&this.helperText.setContent(t)},n.prototype.setLeadingIconAriaLabel=function(t){this.leadingIcon&&this.leadingIcon.setAriaLabel(t)},n.prototype.setLeadingIconContent=function(t){this.leadingIcon&&this.leadingIcon.setContent(t)},n.prototype.setTrailingIconAriaLabel=function(t){this.trailingIcon&&this.trailingIcon.setAriaLabel(t)},n.prototype.setTrailingIconContent=function(t){this.trailingIcon&&this.trailingIcon.setContent(t)},n.prototype.setcharacterCounter=function(t){if(this.characterCounter){var e=this.getNativeInput().maxLength;if(-1===e)throw new Error("MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.");this.characterCounter.setCounterValue(t,e)}},n.prototype.isBadInput=function(){return this.getNativeInput().validity.badInput||!1},n.prototype.isNativeInputValid=function(){return this.getNativeInput().validity.valid},n.prototype.styleValidity=function(t){var e=n.cssClasses.INVALID;if(t?this.adapter.removeClass(e):this.adapter.addClass(e),this.helperText){if(this.helperText.setValidity(t),!this.helperText.isValidation())return;var i=this.helperText.isVisible(),o=this.helperText.getId();i&&o?this.adapter.setInputAttr(zc.ARIA_DESCRIBEDBY,o):this.adapter.removeInputAttr(zc.ARIA_DESCRIBEDBY)}},n.prototype.styleFocused=function(t){var e=n.cssClasses.FOCUSED;t?this.adapter.addClass(e):this.adapter.removeClass(e)},n.prototype.styleDisabled=function(t){var e=n.cssClasses,i=e.DISABLED,o=e.INVALID;t?(this.adapter.addClass(i),this.adapter.removeClass(o)):this.adapter.removeClass(i),this.leadingIcon&&this.leadingIcon.setDisabled(t),this.trailingIcon&&this.trailingIcon.setDisabled(t)},n.prototype.styleFloating=function(t){var e=n.cssClasses.LABEL_FLOATING;t?this.adapter.addClass(e):this.adapter.removeClass(e)},n.prototype.getNativeInput=function(){return(this.adapter?this.adapter.getNativeInput():null)||{disabled:!1,maxLength:-1,required:!1,type:"input",validity:{badInput:!1,valid:!0},value:""}},n}(Tc); + */;var rd=function(){function t(t){void 0===t&&(t={}),this.adapter=t}return Object.defineProperty(t,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),t.prototype.init=function(){},t.prototype.destroy=function(){},t}(),ad={ARIA_CONTROLS:"aria-controls",ARIA_DESCRIBEDBY:"aria-describedby",INPUT_SELECTOR:".mdc-text-field__input",LABEL_SELECTOR:".mdc-floating-label",LEADING_ICON_SELECTOR:".mdc-text-field__icon--leading",LINE_RIPPLE_SELECTOR:".mdc-line-ripple",OUTLINE_SELECTOR:".mdc-notched-outline",PREFIX_SELECTOR:".mdc-text-field__affix--prefix",SUFFIX_SELECTOR:".mdc-text-field__affix--suffix",TRAILING_ICON_SELECTOR:".mdc-text-field__icon--trailing"},ld={DISABLED:"mdc-text-field--disabled",FOCUSED:"mdc-text-field--focused",HELPER_LINE:"mdc-text-field-helper-line",INVALID:"mdc-text-field--invalid",LABEL_FLOATING:"mdc-text-field--label-floating",NO_LABEL:"mdc-text-field--no-label",OUTLINED:"mdc-text-field--outlined",ROOT:"mdc-text-field",TEXTAREA:"mdc-text-field--textarea",WITH_LEADING_ICON:"mdc-text-field--with-leading-icon",WITH_TRAILING_ICON:"mdc-text-field--with-trailing-icon",WITH_INTERNAL_COUNTER:"mdc-text-field--with-internal-counter"},sd={LABEL_SCALE:.75},cd=["pattern","min","max","required","step","minlength","maxlength"],dd=["color","date","datetime-local","month","range","time","week"],ud=["mousedown","touchstart"],hd=["click","keydown"],md=function(t){function e(i,n){void 0===n&&(n={});var r=t.call(this,o(o({},e.defaultAdapter),i))||this;return r.isFocused=!1,r.receivedUserInput=!1,r.valid=!0,r.useNativeValidation=!0,r.validateOnValueChange=!0,r.helperText=n.helperText,r.characterCounter=n.characterCounter,r.leadingIcon=n.leadingIcon,r.trailingIcon=n.trailingIcon,r.inputFocusHandler=function(){r.activateFocus()},r.inputBlurHandler=function(){r.deactivateFocus()},r.inputInputHandler=function(){r.handleInput()},r.setPointerXOffset=function(t){r.setTransformOrigin(t)},r.textFieldInteractionHandler=function(){r.handleTextFieldInteraction()},r.validationAttributeChangeHandler=function(t){r.handleValidationAttributeChange(t)},r}return i(e,t),Object.defineProperty(e,"cssClasses",{get:function(){return ld},enumerable:!1,configurable:!0}),Object.defineProperty(e,"strings",{get:function(){return ad},enumerable:!1,configurable:!0}),Object.defineProperty(e,"numbers",{get:function(){return sd},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"shouldAlwaysFloat",{get:function(){var t=this.getNativeInput().type;return dd.indexOf(t)>=0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"shouldFloat",{get:function(){return this.shouldAlwaysFloat||this.isFocused||!!this.getValue()||this.isBadInput()},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"shouldShake",{get:function(){return!this.isFocused&&!this.isValid()&&!!this.getValue()},enumerable:!1,configurable:!0}),Object.defineProperty(e,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!0},setInputAttr:function(){},removeInputAttr:function(){},registerTextFieldInteractionHandler:function(){},deregisterTextFieldInteractionHandler:function(){},registerInputInteractionHandler:function(){},deregisterInputInteractionHandler:function(){},registerValidationAttributeChangeHandler:function(){return new MutationObserver((function(){}))},deregisterValidationAttributeChangeHandler:function(){},getNativeInput:function(){return null},isFocused:function(){return!1},activateLineRipple:function(){},deactivateLineRipple:function(){},setLineRippleTransformOrigin:function(){},shakeLabel:function(){},floatLabel:function(){},setLabelRequired:function(){},hasLabel:function(){return!1},getLabelWidth:function(){return 0},hasOutline:function(){return!1},notchOutline:function(){},closeOutline:function(){}}},enumerable:!1,configurable:!0}),e.prototype.init=function(){var t,e,i,o;this.adapter.hasLabel()&&this.getNativeInput().required&&this.adapter.setLabelRequired(!0),this.adapter.isFocused()?this.inputFocusHandler():this.adapter.hasLabel()&&this.shouldFloat&&(this.notchOutline(!0),this.adapter.floatLabel(!0),this.styleFloating(!0)),this.adapter.registerInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.registerInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.registerInputInteractionHandler("input",this.inputInputHandler);try{for(var n=r(ud),a=n.next();!a.done;a=n.next()){var l=a.value;this.adapter.registerInputInteractionHandler(l,this.setPointerXOffset)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=n.return)&&e.call(n)}finally{if(t)throw t.error}}try{for(var s=r(hd),c=s.next();!c.done;c=s.next()){l=c.value;this.adapter.registerTextFieldInteractionHandler(l,this.textFieldInteractionHandler)}}catch(t){i={error:t}}finally{try{c&&!c.done&&(o=s.return)&&o.call(s)}finally{if(i)throw i.error}}this.validationObserver=this.adapter.registerValidationAttributeChangeHandler(this.validationAttributeChangeHandler),this.setcharacterCounter(this.getValue().length)},e.prototype.destroy=function(){var t,e,i,o;this.adapter.deregisterInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.deregisterInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.deregisterInputInteractionHandler("input",this.inputInputHandler);try{for(var n=r(ud),a=n.next();!a.done;a=n.next()){var l=a.value;this.adapter.deregisterInputInteractionHandler(l,this.setPointerXOffset)}}catch(e){t={error:e}}finally{try{a&&!a.done&&(e=n.return)&&e.call(n)}finally{if(t)throw t.error}}try{for(var s=r(hd),c=s.next();!c.done;c=s.next()){l=c.value;this.adapter.deregisterTextFieldInteractionHandler(l,this.textFieldInteractionHandler)}}catch(t){i={error:t}}finally{try{c&&!c.done&&(o=s.return)&&o.call(s)}finally{if(i)throw i.error}}this.adapter.deregisterValidationAttributeChangeHandler(this.validationObserver)},e.prototype.handleTextFieldInteraction=function(){var t=this.adapter.getNativeInput();t&&t.disabled||(this.receivedUserInput=!0)},e.prototype.handleValidationAttributeChange=function(t){var e=this;t.some((function(t){return cd.indexOf(t)>-1&&(e.styleValidity(!0),e.adapter.setLabelRequired(e.getNativeInput().required),!0)})),t.indexOf("maxlength")>-1&&this.setcharacterCounter(this.getValue().length)},e.prototype.notchOutline=function(t){if(this.adapter.hasOutline()&&this.adapter.hasLabel())if(t){var e=this.adapter.getLabelWidth()*sd.LABEL_SCALE;this.adapter.notchOutline(e)}else this.adapter.closeOutline()},e.prototype.activateFocus=function(){this.isFocused=!0,this.styleFocused(this.isFocused),this.adapter.activateLineRipple(),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),!this.helperText||!this.helperText.isPersistent()&&this.helperText.isValidation()&&this.valid||this.helperText.showToScreenReader()},e.prototype.setTransformOrigin=function(t){if(!this.isDisabled()&&!this.adapter.hasOutline()){var e=t.touches,i=e?e[0]:t,o=i.target.getBoundingClientRect(),n=i.clientX-o.left;this.adapter.setLineRippleTransformOrigin(n)}},e.prototype.handleInput=function(){this.autoCompleteFocus(),this.setcharacterCounter(this.getValue().length)},e.prototype.autoCompleteFocus=function(){this.receivedUserInput||this.activateFocus()},e.prototype.deactivateFocus=function(){this.isFocused=!1,this.adapter.deactivateLineRipple();var t=this.isValid();this.styleValidity(t),this.styleFocused(this.isFocused),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),this.shouldFloat||(this.receivedUserInput=!1)},e.prototype.getValue=function(){return this.getNativeInput().value},e.prototype.setValue=function(t){if(this.getValue()!==t&&(this.getNativeInput().value=t),this.setcharacterCounter(t.length),this.validateOnValueChange){var e=this.isValid();this.styleValidity(e)}this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.validateOnValueChange&&this.adapter.shakeLabel(this.shouldShake))},e.prototype.isValid=function(){return this.useNativeValidation?this.isNativeInputValid():this.valid},e.prototype.setValid=function(t){this.valid=t,this.styleValidity(t);var e=!t&&!this.isFocused&&!!this.getValue();this.adapter.hasLabel()&&this.adapter.shakeLabel(e)},e.prototype.setValidateOnValueChange=function(t){this.validateOnValueChange=t},e.prototype.getValidateOnValueChange=function(){return this.validateOnValueChange},e.prototype.setUseNativeValidation=function(t){this.useNativeValidation=t},e.prototype.isDisabled=function(){return this.getNativeInput().disabled},e.prototype.setDisabled=function(t){this.getNativeInput().disabled=t,this.styleDisabled(t)},e.prototype.setHelperTextContent=function(t){this.helperText&&this.helperText.setContent(t)},e.prototype.setLeadingIconAriaLabel=function(t){this.leadingIcon&&this.leadingIcon.setAriaLabel(t)},e.prototype.setLeadingIconContent=function(t){this.leadingIcon&&this.leadingIcon.setContent(t)},e.prototype.setTrailingIconAriaLabel=function(t){this.trailingIcon&&this.trailingIcon.setAriaLabel(t)},e.prototype.setTrailingIconContent=function(t){this.trailingIcon&&this.trailingIcon.setContent(t)},e.prototype.setcharacterCounter=function(t){if(this.characterCounter){var e=this.getNativeInput().maxLength;if(-1===e)throw new Error("MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.");this.characterCounter.setCounterValue(t,e)}},e.prototype.isBadInput=function(){return this.getNativeInput().validity.badInput||!1},e.prototype.isNativeInputValid=function(){return this.getNativeInput().validity.valid},e.prototype.styleValidity=function(t){var i=e.cssClasses.INVALID;if(t?this.adapter.removeClass(i):this.adapter.addClass(i),this.helperText){if(this.helperText.setValidity(t),!this.helperText.isValidation())return;var o=this.helperText.isVisible(),n=this.helperText.getId();o&&n?this.adapter.setInputAttr(ad.ARIA_DESCRIBEDBY,n):this.adapter.removeInputAttr(ad.ARIA_DESCRIBEDBY)}},e.prototype.styleFocused=function(t){var i=e.cssClasses.FOCUSED;t?this.adapter.addClass(i):this.adapter.removeClass(i)},e.prototype.styleDisabled=function(t){var i=e.cssClasses,o=i.DISABLED,n=i.INVALID;t?(this.adapter.addClass(o),this.adapter.removeClass(n)):this.adapter.removeClass(o),this.leadingIcon&&this.leadingIcon.setDisabled(t),this.trailingIcon&&this.trailingIcon.setDisabled(t)},e.prototype.styleFloating=function(t){var i=e.cssClasses.LABEL_FLOATING;t?this.adapter.addClass(i):this.adapter.removeClass(i)},e.prototype.getNativeInput=function(){return(this.adapter?this.adapter.getNativeInput():null)||{disabled:!1,maxLength:-1,required:!1,type:"input",validity:{badInput:!1,valid:!0},value:""}},e}(rd),pd=md; /** * @license * Copyright 2016 Google Inc. @@ -2927,13 +3106,13 @@ const Lr=Ae(class extends Se{constructor(t){var e;if(super(t),t.type!==Ce||"styl * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.type!==Ce&&t.type!==Ee)throw Error("The `live` directive is not allowed on child or event bindings");if(!(t=>void 0===t.strings)(t))throw Error("`live` bindings can only contain a single expression")}render(t){return t}update(t,[e]){if(e===R||e===F)return e;const i=t.element,n=t.name;if(t.type===$e){if(e===i[n])return R}else if(t.type===Ee){if(!!e===i.hasAttribute(n))return R}else if(t.type===Ce&&i.getAttribute(n)===e+"")return R;return((t,e=Vc)=>{t._$AH=e; +const fd={},gd=Re(class extends Ve{constructor(t){if(super(t),t.type!==Pe&&t.type!==je&&t.type!==Ne)throw Error("The `live` directive is not allowed on child or event bindings");if(!(t=>void 0===t.strings)(t))throw Error("`live` bindings can only contain a single expression")}render(t){return t}update(t,[e]){if(e===H||e===Y)return e;const i=t.element,o=t.name;if(t.type===Pe){if(e===i[o])return H}else if(t.type===Ne){if(!!e===i.hasAttribute(o))return H}else if(t.type===je&&i.getAttribute(o)===e+"")return H;return((t,e=fd)=>{t._$AH=e; /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */})(t),e}}),Fc=["touchstart","touchmove","scroll","mousewheel"],Bc=(t={})=>{const e={};for(const i in t)e[i]=t[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},e)};class Uc extends So{constructor(){super(...arguments),this.mdcFoundationClass=Nc,this.value="",this.type="text",this.placeholder="",this.label="",this.icon="",this.iconTrailing="",this.disabled=!1,this.required=!1,this.minLength=-1,this.maxLength=-1,this.outlined=!1,this.helper="",this.validateOnInitialRender=!1,this.validationMessage="",this.autoValidate=!1,this.pattern="",this.min="",this.max="",this.step=null,this.size=null,this.helperPersistent=!1,this.charCounter=!1,this.endAligned=!1,this.prefix="",this.suffix="",this.name="",this.readOnly=!1,this.autocapitalize="",this.outlineOpen=!1,this.outlineWidth=0,this.isUiValid=!0,this.focused=!1,this._validity=Bc(),this.validityTransform=null}get validity(){return this._checkValidity(this.value),this._validity}get willValidate(){return this.formElement.willValidate}get selectionStart(){return this.formElement.selectionStart}get selectionEnd(){return this.formElement.selectionEnd}focus(){const t=new CustomEvent("focus");this.formElement.dispatchEvent(t),this.formElement.focus()}blur(){const t=new CustomEvent("blur");this.formElement.dispatchEvent(t),this.formElement.blur()}select(){this.formElement.select()}setSelectionRange(t,e,i){this.formElement.setSelectionRange(t,e,i)}update(t){t.has("autoValidate")&&this.mdcFoundation&&this.mdcFoundation.setValidateOnValueChange(this.autoValidate),t.has("value")&&"string"!=typeof this.value&&(this.value=`${this.value}`),super.update(t)}setFormData(t){this.name&&t.append(this.name,this.value)}render(){const t=this.charCounter&&-1!==this.maxLength,e=!!this.helper||!!this.validationMessage||t,i={"mdc-text-field--disabled":this.disabled,"mdc-text-field--no-label":!this.label,"mdc-text-field--filled":!this.outlined,"mdc-text-field--outlined":this.outlined,"mdc-text-field--with-leading-icon":this.icon,"mdc-text-field--with-trailing-icon":this.iconTrailing,"mdc-text-field--end-aligned":this.endAligned};return N` -
`:""}renderCharCounter(t){const e=Math.min(this.value.length,this.maxLength);return t?B` ${e} / ${this.maxLength}`:""}onInputFocus(){this.focused=!0}onInputBlur(){this.focused=!1,this.reportValidity()}checkValidity(){const t=this._checkValidity(this.value);if(!t){const t=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(t)}return t}reportValidity(){const t=this.checkValidity();return this.mdcFoundation.setValid(t),this.isUiValid=t,t}_checkValidity(t){const e=this.formElement.validity;let i=Bc(e);if(this.validityTransform){const e=this.validityTransform(t,i);i=Object.assign(Object.assign({},i),e),this.mdcFoundation.setUseNativeValidation(!1)}else this.mdcFoundation.setUseNativeValidation(!0);return this._validity=i,this._validity.valid}setCustomValidity(t){this.validationMessage=t,this.formElement.setCustomValidity(t)}handleInputChange(){this.value=this.formElement.value}createAdapter(){return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},this.getRootAdapterMethods()),this.getInputAdapterMethods()),this.getLabelAdapterMethods()),this.getLineRippleAdapterMethods()),this.getOutlineAdapterMethods())}getRootAdapterMethods(){return Object.assign({registerTextFieldInteractionHandler:(t,e)=>this.addEventListener(t,e),deregisterTextFieldInteractionHandler:(t,e)=>this.removeEventListener(t,e),registerValidationAttributeChangeHandler:t=>{const e=new MutationObserver((e=>{t((t=>t.map((t=>t.attributeName)).filter((t=>t)))(e))}));return e.observe(this.formElement,{attributes:!0}),e},deregisterValidationAttributeChangeHandler:t=>t.disconnect()},xo(this.mdcRoot))}getInputAdapterMethods(){return{getNativeInput:()=>this.formElement,setInputAttr:()=>{},removeInputAttr:()=>{},isFocused:()=>!!this.shadowRoot&&this.shadowRoot.activeElement===this.formElement,registerInputInteractionHandler:(t,e)=>this.formElement.addEventListener(t,e,{passive:t in Fc}),deregisterInputInteractionHandler:(t,e)=>this.formElement.removeEventListener(t,e)}}getLabelAdapterMethods(){return{floatLabel:t=>this.labelElement&&this.labelElement.floatingLabelFoundation.float(t),getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,hasLabel:()=>Boolean(this.labelElement),shakeLabel:t=>this.labelElement&&this.labelElement.floatingLabelFoundation.shake(t),setLabelRequired:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(t)}}}getLineRippleAdapterMethods(){return{activateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},setLineRippleTransformOrigin:t=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.setRippleCenter(t)}}}async getUpdateComplete(){var t;const e=await super.getUpdateComplete();return await(null===(t=this.outlineElement)||void 0===t?void 0:t.updateComplete),e}firstUpdated(){var t;super.firstUpdated(),this.mdcFoundation.setValidateOnValueChange(this.autoValidate),this.validateOnInitialRender&&this.reportValidity(),null===(t=this.outlineElement)||void 0===t||t.updateComplete.then((()=>{var t;this.outlineWidth=(null===(t=this.labelElement)||void 0===t?void 0:t.floatingLabelFoundation.getWidth())||0}))}getOutlineAdapterMethods(){return{closeOutline:()=>this.outlineElement&&(this.outlineOpen=!1),hasOutline:()=>Boolean(this.outlineElement),notchOutline:t=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=t,this.outlineOpen=!0)}}}async layout(){await this.updateComplete;const t=this.labelElement;if(!t)return void(this.outlineOpen=!1);const e=!!this.label&&!!this.value;if(t.floatingLabelFoundation.float(e),!this.outlined)return;this.outlineOpen=e,await this.updateComplete;const i=t.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=i,await this.updateComplete)}}n([ht(".mdc-text-field")],Uc.prototype,"mdcRoot",void 0),n([ht("input")],Uc.prototype,"formElement",void 0),n([ht(".mdc-floating-label")],Uc.prototype,"labelElement",void 0),n([ht(".mdc-line-ripple")],Uc.prototype,"lineRippleElement",void 0),n([ht("mwc-notched-outline")],Uc.prototype,"outlineElement",void 0),n([ht(".mdc-notched-outline__notch")],Uc.prototype,"notchElement",void 0),n([st({type:String})],Uc.prototype,"value",void 0),n([st({type:String})],Uc.prototype,"type",void 0),n([st({type:String})],Uc.prototype,"placeholder",void 0),n([st({type:String}),Io((function(t,e){void 0!==e&&this.label!==e&&this.layout()}))],Uc.prototype,"label",void 0),n([st({type:String})],Uc.prototype,"icon",void 0),n([st({type:String})],Uc.prototype,"iconTrailing",void 0),n([st({type:Boolean,reflect:!0})],Uc.prototype,"disabled",void 0),n([st({type:Boolean})],Uc.prototype,"required",void 0),n([st({type:Number})],Uc.prototype,"minLength",void 0),n([st({type:Number})],Uc.prototype,"maxLength",void 0),n([st({type:Boolean,reflect:!0}),Io((function(t,e){void 0!==e&&this.outlined!==e&&this.layout()}))],Uc.prototype,"outlined",void 0),n([st({type:String})],Uc.prototype,"helper",void 0),n([st({type:Boolean})],Uc.prototype,"validateOnInitialRender",void 0),n([st({type:String})],Uc.prototype,"validationMessage",void 0),n([st({type:Boolean})],Uc.prototype,"autoValidate",void 0),n([st({type:String})],Uc.prototype,"pattern",void 0),n([st({type:String})],Uc.prototype,"min",void 0),n([st({type:String})],Uc.prototype,"max",void 0),n([st({type:String})],Uc.prototype,"step",void 0),n([st({type:Number})],Uc.prototype,"size",void 0),n([st({type:Boolean})],Uc.prototype,"helperPersistent",void 0),n([st({type:Boolean})],Uc.prototype,"charCounter",void 0),n([st({type:Boolean})],Uc.prototype,"endAligned",void 0),n([st({type:String})],Uc.prototype,"prefix",void 0),n([st({type:String})],Uc.prototype,"suffix",void 0),n([st({type:String})],Uc.prototype,"name",void 0),n([st({type:String})],Uc.prototype,"inputMode",void 0),n([st({type:Boolean})],Uc.prototype,"readOnly",void 0),n([st({type:String})],Uc.prototype,"autocapitalize",void 0),n([ct()],Uc.prototype,"outlineOpen",void 0),n([ct()],Uc.prototype,"outlineWidth",void 0),n([ct()],Uc.prototype,"isUiValid",void 0),n([ct()],Uc.prototype,"focused",void 0),n([ut({passive:!0})],Uc.prototype,"handleInputChange",null);class Hc extends Uc{updated(t){super.updated(t),(t.has("invalid")&&(this.invalid||void 0!==t.get("invalid"))||t.has("errorMessage"))&&(this.setCustomValidity(this.invalid?this.errorMessage||"Invalid":""),this.reportValidity())}renderOutline(){return""}renderIcon(t,e=!1){const i=e?"trailing":"leading";return N` + >${e} / ${this.maxLength}`:""}onInputFocus(){this.focused=!0}onInputBlur(){this.focused=!1,this.reportValidity()}checkValidity(){const t=this._checkValidity(this.value);if(!t){const t=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(t)}return t}reportValidity(){const t=this.checkValidity();return this.mdcFoundation.setValid(t),this.isUiValid=t,t}_checkValidity(t){const e=this.formElement.validity;let i=vd(e);if(this.validityTransform){const e=this.validityTransform(t,i);i=Object.assign(Object.assign({},i),e),this.mdcFoundation.setUseNativeValidation(!1)}else this.mdcFoundation.setUseNativeValidation(!0);return this._validity=i,this._validity.valid}setCustomValidity(t){this.validationMessage=t,this.formElement.setCustomValidity(t)}handleInputChange(){this.value=this.formElement.value}createAdapter(){return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},this.getRootAdapterMethods()),this.getInputAdapterMethods()),this.getLabelAdapterMethods()),this.getLineRippleAdapterMethods()),this.getOutlineAdapterMethods())}getRootAdapterMethods(){return Object.assign({registerTextFieldInteractionHandler:(t,e)=>this.addEventListener(t,e),deregisterTextFieldInteractionHandler:(t,e)=>this.removeEventListener(t,e),registerValidationAttributeChangeHandler:t=>{const e=new MutationObserver((e=>{t((t=>t.map((t=>t.attributeName)).filter((t=>t)))(e))}));return e.observe(this.formElement,{attributes:!0}),e},deregisterValidationAttributeChangeHandler:t=>t.disconnect()},Pn(this.mdcRoot))}getInputAdapterMethods(){return{getNativeInput:()=>this.formElement,setInputAttr:()=>{},removeInputAttr:()=>{},isFocused:()=>!!this.shadowRoot&&this.shadowRoot.activeElement===this.formElement,registerInputInteractionHandler:(t,e)=>this.formElement.addEventListener(t,e,{passive:t in _d}),deregisterInputInteractionHandler:(t,e)=>this.formElement.removeEventListener(t,e)}}getLabelAdapterMethods(){return{floatLabel:t=>this.labelElement&&this.labelElement.floatingLabelFoundation.float(t),getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,hasLabel:()=>Boolean(this.labelElement),shakeLabel:t=>this.labelElement&&this.labelElement.floatingLabelFoundation.shake(t),setLabelRequired:t=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(t)}}}getLineRippleAdapterMethods(){return{activateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},setLineRippleTransformOrigin:t=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.setRippleCenter(t)}}}async getUpdateComplete(){var t;const e=await super.getUpdateComplete();return await(null===(t=this.outlineElement)||void 0===t?void 0:t.updateComplete),e}firstUpdated(){var t;super.firstUpdated(),this.mdcFoundation.setValidateOnValueChange(this.autoValidate),this.validateOnInitialRender&&this.reportValidity(),null===(t=this.outlineElement)||void 0===t||t.updateComplete.then((()=>{var t;this.outlineWidth=(null===(t=this.labelElement)||void 0===t?void 0:t.floatingLabelFoundation.getWidth())||0}))}getOutlineAdapterMethods(){return{closeOutline:()=>this.outlineElement&&(this.outlineOpen=!1),hasOutline:()=>Boolean(this.outlineElement),notchOutline:t=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=t,this.outlineOpen=!0)}}}async layout(){await this.updateComplete;const t=this.labelElement;if(!t)return void(this.outlineOpen=!1);const e=!!this.label&&!!this.value;if(t.floatingLabelFoundation.float(e),!this.outlined)return;this.outlineOpen=e,await this.updateComplete;const i=t.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=i,await this.updateComplete)}}n([gt(".mdc-text-field")],bd.prototype,"mdcRoot",void 0),n([gt("input")],bd.prototype,"formElement",void 0),n([gt(".mdc-floating-label")],bd.prototype,"labelElement",void 0),n([gt(".mdc-line-ripple")],bd.prototype,"lineRippleElement",void 0),n([gt("mwc-notched-outline")],bd.prototype,"outlineElement",void 0),n([gt(".mdc-notched-outline__notch")],bd.prototype,"notchElement",void 0),n([ht({type:String})],bd.prototype,"value",void 0),n([ht({type:String})],bd.prototype,"type",void 0),n([ht({type:String})],bd.prototype,"placeholder",void 0),n([ht({type:String}),Yn((function(t,e){void 0!==e&&this.label!==e&&this.layout()}))],bd.prototype,"label",void 0),n([ht({type:String})],bd.prototype,"icon",void 0),n([ht({type:String})],bd.prototype,"iconTrailing",void 0),n([ht({type:Boolean,reflect:!0})],bd.prototype,"disabled",void 0),n([ht({type:Boolean})],bd.prototype,"required",void 0),n([ht({type:Number})],bd.prototype,"minLength",void 0),n([ht({type:Number})],bd.prototype,"maxLength",void 0),n([ht({type:Boolean,reflect:!0}),Yn((function(t,e){void 0!==e&&this.outlined!==e&&this.layout()}))],bd.prototype,"outlined",void 0),n([ht({type:String})],bd.prototype,"helper",void 0),n([ht({type:Boolean})],bd.prototype,"validateOnInitialRender",void 0),n([ht({type:String})],bd.prototype,"validationMessage",void 0),n([ht({type:Boolean})],bd.prototype,"autoValidate",void 0),n([ht({type:String})],bd.prototype,"pattern",void 0),n([ht({type:String})],bd.prototype,"min",void 0),n([ht({type:String})],bd.prototype,"max",void 0),n([ht({type:String})],bd.prototype,"step",void 0),n([ht({type:Number})],bd.prototype,"size",void 0),n([ht({type:Boolean})],bd.prototype,"helperPersistent",void 0),n([ht({type:Boolean})],bd.prototype,"charCounter",void 0),n([ht({type:Boolean})],bd.prototype,"endAligned",void 0),n([ht({type:String})],bd.prototype,"prefix",void 0),n([ht({type:String})],bd.prototype,"suffix",void 0),n([ht({type:String})],bd.prototype,"name",void 0),n([ht({type:String})],bd.prototype,"inputMode",void 0),n([ht({type:Boolean})],bd.prototype,"readOnly",void 0),n([ht({type:String})],bd.prototype,"autocapitalize",void 0),n([mt()],bd.prototype,"outlineOpen",void 0),n([mt()],bd.prototype,"outlineWidth",void 0),n([mt()],bd.prototype,"isUiValid",void 0),n([mt()],bd.prototype,"focused",void 0),n([ft({passive:!0})],bd.prototype,"handleInputChange",null);class yd extends bd{updated(t){super.updated(t),(t.has("invalid")&&(this.invalid||void 0!==t.get("invalid"))||t.has("errorMessage"))&&(this.setCustomValidity(this.invalid?this.errorMessage||"Invalid":""),this.reportValidity())}renderOutline(){return""}renderIcon(t,e=!1){const i=e?"trailing":"leading";return B` - `}}Hc.styles=[Ic,d` + `}}yd.styles=[nd,h` .mdc-text-field__input { width: var(--ha-textfield-input-width, 100%); } @@ -3034,15 +3213,15 @@ const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.typ .mdc-text-field { overflow: var(--text-field-overflow); } - `],n([st({type:Boolean})],Hc.prototype,"invalid",void 0),n([st({attribute:"error-message"})],Hc.prototype,"errorMessage",void 0),customElements.define("mushroom-textfield",Hc);var Yc=Object.freeze({__proto__:null});const Xc=_t(((t,e)=>[{name:"entity",selector:{entity:{}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"icon_color",selector:{"mush-color":{}}}]},{name:"use_entity_picture",selector:{boolean:{}}},...fc(t)]));let Wc=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Xc(this.hass.connection.haVersion,i);return N` + `],n([ht({type:Boolean})],yd.prototype,"invalid",void 0),n([ht({attribute:"error-message"})],yd.prototype,"errorMessage",void 0),customElements.define("mushroom-textfield",yd);var xd=Object.freeze({__proto__:null});const wd=xt((t=>[{name:"entity",selector:{entity:{}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_color",selector:{"mush-color":{}}}]},{name:"use_entity_picture",selector:{boolean:{}}},...Hc()]));let kd=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=wd(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],Wc.prototype,"hass",void 0),n([ct()],Wc.prototype,"_config",void 0),Wc=n([at(El("entity"))],Wc);var qc=Object.freeze({__proto__:null,get EntityChipEditor(){return Wc}});const Kc=["weather"],Gc=["show_conditions","show_temperature"],Zc=["more-info","navigate","url","call-service","none"],Jc=_t((t=>[{name:"entity",selector:{entity:{domain:Kc}}},{type:"grid",name:"",schema:[{name:"show_conditions",selector:{boolean:{}}},{name:"show_temperature",selector:{boolean:{}}}]},...fc(t,Zc)]));let Qc=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Gc.includes(t.name)?e(`editor.card.weather.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=Jc(this.hass.connection.haVersion);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],kd.prototype,"hass",void 0),n([mt()],kd.prototype,"_config",void 0),kd=n([dt(Xl("entity"))],kd);var Cd=Object.freeze({__proto__:null,get EntityChipEditor(){return kd}});const $d=["weather"],Ed=["show_conditions","show_temperature"],Ad=["more-info","navigate","url","call-service","none"],Sd=xt((()=>[{name:"entity",selector:{entity:{domain:$d}}},{type:"grid",name:"",schema:[{name:"show_conditions",selector:{boolean:{}}},{name:"show_temperature",selector:{boolean:{}}}]},...Hc(Ad)]));let Id=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Ed.includes(t.name)?e(`editor.card.weather.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=Sd();return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],Qc.prototype,"hass",void 0),n([ct()],Qc.prototype,"_config",void 0),Qc=n([at(El("weather"))],Qc);var td=Object.freeze({__proto__:null,get WeatherChipEditor(){return Qc}});const ed=_t((t=>[{name:"icon",selector:{icon:{placeholder:t}}}]));let id=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:arrow-left",e=ed(t);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],Id.prototype,"hass",void 0),n([mt()],Id.prototype,"_config",void 0),Id=n([dt(Xl("weather"))],Id);var Td=Object.freeze({__proto__:null,get WeatherChipEditor(){return Id}});const zd=xt((t=>[{name:"icon",selector:{icon:{placeholder:t}}}]));let Od=class extends st{constructor(){super(...arguments),this._computeLabel=t=>this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:arrow-left",e=zd(t);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],id.prototype,"hass",void 0),n([ct()],id.prototype,"_config",void 0),id=n([at(El("back"))],id);var nd=Object.freeze({__proto__:null,get BackChipEditor(){return id}});const od=["navigate","url","call-service","none"],rd=_t(((t,e)=>[{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"icon_color",selector:{"mush-color":{}}}]},...fc(t,od)]));let ad=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:flash",e=rd(this.hass.connection.haVersion,t);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],Od.prototype,"hass",void 0),n([mt()],Od.prototype,"_config",void 0),Od=n([dt(Xl("back"))],Od);var Md=Object.freeze({__proto__:null,get BackChipEditor(){return Od}});const Dd=["navigate","url","call-service","none"],Ld=xt((t=>[{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_color",selector:{"mush-color":{}}}]},...Hc(Dd)]));let jd=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:flash",e=Ld(t);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],ad.prototype,"hass",void 0),n([ct()],ad.prototype,"_config",void 0),ad=n([at(El("action"))],ad);var ld=Object.freeze({__proto__:null,get EntityChipEditor(){return ad}});const sd=_t((t=>[{name:"icon",selector:{icon:{placeholder:t}}}]));let cd=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.icon||"mdi:menu",e=sd(t);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],jd.prototype,"hass",void 0),n([mt()],jd.prototype,"_config",void 0),jd=n([dt(Xl("action"))],jd);var Pd=Object.freeze({__proto__:null,get EntityChipEditor(){return jd}});const Nd=xt((t=>[{name:"icon",selector:{icon:{placeholder:t}}}]));let Rd=class extends st{constructor(){super(...arguments),this._computeLabel=t=>this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.icon||"mdi:menu",e=Nd(t);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],cd.prototype,"hass",void 0),n([ct()],cd.prototype,"_config",void 0),cd=n([at(El("menu"))],cd);var dd=Object.freeze({__proto__:null,get MenuChipEditor(){return cd}});const ud=te(xc,te(gc,pc),ce({entity:de(ue()),icon:de(ue()),icon_color:de(ue()),primary:de(ue()),secondary:de(ue()),badge_icon:de(ue()),badge_color:de(ue()),picture:de(ue()),multiline_secondary:de(re()),entity_id:de(me([ue(),oe(ue())]))})),hd=["badge_icon","badge_color","content","primary","secondary","multiline_secondary","picture"],md=_t((t=>[{name:"entity",selector:{entity:{}}},{name:"icon",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"icon_color",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"primary",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"secondary",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"badge_icon",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"badge_color",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"picture",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{type:"grid",name:"",schema:[{name:"layout",selector:{"mush-layout":{}}},{name:"fill_container",selector:{boolean:{}}},{name:"multiline_secondary",selector:{boolean:{}}}]},...fc(t)]));let pd=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return"entity"===t.name?`${this.hass.localize("ui.panel.lovelace.editor.card.generic.entity")} (${e("editor.card.template.entity_extra")})`:vc.includes(t.name)?e(`editor.card.generic.${t.name}`):hd.includes(t.name)?e(`editor.card.template.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,ud),this._config=t}render(){return this.hass&&this._config?N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],Rd.prototype,"hass",void 0),n([mt()],Rd.prototype,"_config",void 0),Rd=n([dt(Xl("menu"))],Rd);var Vd=Object.freeze({__proto__:null,get MenuChipEditor(){return Rd}});const Fd=he(Gc,he(Yc,Uc),xe({entity:we(ke()),icon:we(ke()),icon_color:we(ke()),primary:we(ke()),secondary:we(ke()),badge_icon:we(ke()),badge_color:we(ke()),picture:we(ke()),multiline_secondary:we(_e()),entity_id:we($e([ke(),ge(ke())]))})),Bd=["badge_icon","badge_color","content","primary","secondary","multiline_secondary","picture"],Ud=xt((()=>[{name:"entity",selector:{entity:{}}},{name:"icon",selector:{template:{}}},{name:"icon_color",selector:{template:{}}},{name:"primary",selector:{template:{}}},{name:"secondary",selector:{template:{}}},{name:"badge_icon",selector:{template:{}}},{name:"badge_color",selector:{template:{}}},{name:"picture",selector:{template:{}}},{type:"grid",name:"",schema:[{name:"layout",selector:{"mush-layout":{}}},{name:"fill_container",selector:{boolean:{}}},{name:"multiline_secondary",selector:{boolean:{}}}]},...Hc()]));let Hd=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return"entity"===t.name?`${this.hass.localize("ui.panel.lovelace.editor.card.generic.entity")} (${e("editor.card.template.entity_extra")})`:Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Bd.includes(t.name)?e(`editor.card.template.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Fd),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=Ud();return B` - `:N``}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],pd.prototype,"_config",void 0),pd=n([at("mushroom-template-card-editor")],pd);var fd=Object.freeze({__proto__:null,TEMPLATE_LABELS:hd,get TemplateCardEditor(){return pd}});const gd=_t((t=>[{name:"entity",selector:{entity:{}}},{name:"icon",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"icon_color",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"picture",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"content",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},...fc(t)]));let _d=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return"entity"===t.name?`${this.hass.localize("ui.panel.lovelace.editor.card.generic.entity")} (${e("editor.card.template.entity_extra")})`:vc.includes(t.name)?e(`editor.card.generic.${t.name}`):hd.includes(t.name)?e(`editor.card.template.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){return this.hass&&this._config?N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Hd.prototype,"_config",void 0),Hd=n([dt("mushroom-template-card-editor")],Hd);var Yd=Object.freeze({__proto__:null,TEMPLATE_LABELS:Bd,get TemplateCardEditor(){return Hd}});const Xd=xt((()=>[{name:"entity",selector:{entity:{}}},{name:"icon",selector:{template:{}}},{name:"icon_color",selector:{template:{}}},{name:"picture",selector:{template:{}}},{name:"content",selector:{template:{}}},...Hc()]));let Wd=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return"entity"===t.name?`${this.hass.localize("ui.panel.lovelace.editor.card.generic.entity")} (${e("editor.card.template.entity_extra")})`:Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Bd.includes(t.name)?e(`editor.card.template.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=Xd();return B` - `:N``}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],_d.prototype,"hass",void 0),n([ct()],_d.prototype,"_config",void 0),_d=n([at(El("template"))],_d);var vd=Object.freeze({__proto__:null,get EntityChipEditor(){return _d}}),bd={},yd={};function xd(t){return null==t}function wd(t,e){var i="",n=t.reason||"(unknown reason)";return t.mark?(t.mark.name&&(i+='in "'+t.mark.name+'" '),i+="("+(t.mark.line+1)+":"+(t.mark.column+1)+")",!e&&t.mark.snippet&&(i+="\n\n"+t.mark.snippet),n+" "+i):n}function kd(t,e){Error.call(this),this.name="YAMLException",this.reason=t,this.mark=e,this.message=wd(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}yd.isNothing=xd,yd.isObject=function(t){return"object"==typeof t&&null!==t},yd.toArray=function(t){return Array.isArray(t)?t:xd(t)?[]:[t]},yd.repeat=function(t,e){var i,n="";for(i=0;il&&(e=n-l+(r=" ... ").length),i-n>l&&(i=n+l-(a=" ...").length),{str:r+t.slice(e,i).replace(/\t/g,"→")+a,pos:n-e+r.length}}function Ad(t,e){return $d.repeat(" ",e-t.length)+t}var Sd=function(t,e){if(e=Object.create(e||null),!t.buffer)return null;e.maxLength||(e.maxLength=79),"number"!=typeof e.indent&&(e.indent=1),"number"!=typeof e.linesBefore&&(e.linesBefore=3),"number"!=typeof e.linesAfter&&(e.linesAfter=2);for(var i,n=/\r?\n|\r|\0/g,o=[0],r=[],a=-1;i=n.exec(t.buffer);)r.push(i.index),o.push(i.index+i[0].length),t.position<=i.index&&a<0&&(a=o.length-2);a<0&&(a=o.length-1);var l,s,c="",d=Math.min(t.line+e.linesAfter,r.length).toString().length,u=e.maxLength-(e.indent+d+3);for(l=1;l<=e.linesBefore&&!(a-l<0);l++)s=Ed(t.buffer,o[a-l],r[a-l],t.position-(o[a]-o[a-l]),u),c=$d.repeat(" ",e.indent)+Ad((t.line-l+1).toString(),d)+" | "+s.str+"\n"+c;for(s=Ed(t.buffer,o[a],r[a],t.position,u),c+=$d.repeat(" ",e.indent)+Ad((t.line+1).toString(),d)+" | "+s.str+"\n",c+=$d.repeat("-",e.indent+d+3+s.pos)+"^\n",l=1;l<=e.linesAfter&&!(a+l>=r.length);l++)s=Ed(t.buffer,o[a+l],r[a+l],t.position-(o[a]-o[a+l]),u),c+=$d.repeat(" ",e.indent)+Ad((t.line+l+1).toString(),d)+" | "+s.str+"\n";return c.replace(/\n$/,"")},Id={exports:{}},Td=Cd,zd=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],Od=["scalar","sequence","mapping"];var Md=function(t,e){if(e=e||{},Object.keys(e).forEach((function(e){if(-1===zd.indexOf(e))throw new Td('Unknown option "'+e+'" is met in definition of "'+t+'" YAML type.')})),this.options=e,this.tag=t,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(t){return t},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.representName=e.representName||null,this.defaultStyle=e.defaultStyle||null,this.multi=e.multi||!1,this.styleAliases=function(t){var e={};return null!==t&&Object.keys(t).forEach((function(i){t[i].forEach((function(t){e[String(t)]=i}))})),e}(e.styleAliases||null),-1===Od.indexOf(this.kind))throw new Td('Unknown kind "'+this.kind+'" is specified for "'+t+'" YAML type.')},Ld=Cd,Dd=Md;function jd(t,e){var i=[];return t[e].forEach((function(t){var e=i.length;i.forEach((function(i,n){i.tag===t.tag&&i.kind===t.kind&&i.multi===t.multi&&(e=n)})),i[e]=t})),i}function Pd(t){return this.extend(t)}Pd.prototype.extend=function(t){var e=[],i=[];if(t instanceof Dd)i.push(t);else if(Array.isArray(t))i=i.concat(t);else{if(!t||!Array.isArray(t.implicit)&&!Array.isArray(t.explicit))throw new Ld("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");t.implicit&&(e=e.concat(t.implicit)),t.explicit&&(i=i.concat(t.explicit))}e.forEach((function(t){if(!(t instanceof Dd))throw new Ld("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(t.loadKind&&"scalar"!==t.loadKind)throw new Ld("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(t.multi)throw new Ld("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),i.forEach((function(t){if(!(t instanceof Dd))throw new Ld("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var n=Object.create(Pd.prototype);return n.implicit=(this.implicit||[]).concat(e),n.explicit=(this.explicit||[]).concat(i),n.compiledImplicit=jd(n,"implicit"),n.compiledExplicit=jd(n,"explicit"),n.compiledTypeMap=function(){var t,e,i={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function n(t){t.multi?(i.multi[t.kind].push(t),i.multi.fallback.push(t)):i[t.kind][t.tag]=i.fallback[t.tag]=t}for(t=0,e=arguments.length;t=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},octal:function(t){return t>=0?"0o"+t.toString(8):"-0o"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),Xd=yd,Wd=Md,qd=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");var Kd=/^[-+]?[0-9]+e/;var Gd=new Wd("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(t){return null!==t&&!(!qd.test(t)||"_"===t[t.length-1])},construct:function(t){var e,i;return i="-"===(e=t.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(e[0])>=0&&(e=e.slice(1)),".inf"===e?1===i?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===e?NaN:i*parseFloat(e,10)},predicate:function(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||Xd.isNegativeZero(t))},represent:function(t,e){var i;if(isNaN(t))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Xd.isNegativeZero(t))return"-0.0";return i=t.toString(10),Kd.test(i)?i.replace("e",".e"):i},defaultStyle:"lowercase"}),Zd=Nd.extend({implicit:[Vd,Rd,Yd,Gd]});Id.exports=Zd;var Jd=Md,Qd=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),tu=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");var eu=new Jd("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(t){return null!==t&&(null!==Qd.exec(t)||null!==tu.exec(t))},construct:function(t){var e,i,n,o,r,a,l,s,c=0,d=null;if(null===(e=Qd.exec(t))&&(e=tu.exec(t)),null===e)throw new Error("Date resolve error");if(i=+e[1],n=+e[2]-1,o=+e[3],!e[4])return new Date(Date.UTC(i,n,o));if(r=+e[4],a=+e[5],l=+e[6],e[7]){for(c=e[7].slice(0,3);c.length<3;)c+="0";c=+c}return e[9]&&(d=6e4*(60*+e[10]+ +(e[11]||0)),"-"===e[9]&&(d=-d)),s=new Date(Date.UTC(i,n,o,r,a,l,c)),d&&s.setTime(s.getTime()-d),s},instanceOf:Date,represent:function(t){return t.toISOString()}});var iu=new Md("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(t){return"<<"===t||null===t}}),nu="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";var ou=new Md("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(t){if(null===t)return!1;var e,i,n=0,o=t.length,r=nu;for(i=0;i64)){if(e<0)return!1;n+=6}return n%8==0},construct:function(t){var e,i,n=t.replace(/[\r\n=]/g,""),o=n.length,r=nu,a=0,l=[];for(e=0;e>16&255),l.push(a>>8&255),l.push(255&a)),a=a<<6|r.indexOf(n.charAt(e));return 0===(i=o%4*6)?(l.push(a>>16&255),l.push(a>>8&255),l.push(255&a)):18===i?(l.push(a>>10&255),l.push(a>>2&255)):12===i&&l.push(a>>4&255),new Uint8Array(l)},predicate:function(t){return"[object Uint8Array]"===Object.prototype.toString.call(t)},represent:function(t){var e,i,n="",o=0,r=t.length,a=nu;for(e=0;e>18&63],n+=a[o>>12&63],n+=a[o>>6&63],n+=a[63&o]),o=(o<<8)+t[e];return 0===(i=r%3)?(n+=a[o>>18&63],n+=a[o>>12&63],n+=a[o>>6&63],n+=a[63&o]):2===i?(n+=a[o>>10&63],n+=a[o>>4&63],n+=a[o<<2&63],n+=a[64]):1===i&&(n+=a[o>>2&63],n+=a[o<<4&63],n+=a[64],n+=a[64]),n}}),ru=Md,au=Object.prototype.hasOwnProperty,lu=Object.prototype.toString;var su=new ru("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(t){if(null===t)return!0;var e,i,n,o,r,a=[],l=t;for(e=0,i=l.length;e>10),56320+(t-65536&1023))}for(var ju=new Array(256),Pu=new Array(256),Nu=0;Nu<256;Nu++)ju[Nu]=Lu(Nu)?1:0,Pu[Nu]=Lu(Nu);function Vu(t,e){this.input=t,this.filename=e.filename||null,this.schema=e.schema||bu,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function Ru(t,e){var i={name:t.filename,buffer:t.input.slice(0,-1),position:t.position,line:t.line,column:t.position-t.lineStart};return i.snippet=vu(i),new _u(e,i)}function Fu(t,e){throw Ru(t,e)}function Bu(t,e){t.onWarning&&t.onWarning.call(null,Ru(t,e))}var Uu={YAML:function(t,e,i){var n,o,r;null!==t.version&&Fu(t,"duplication of %YAML directive"),1!==i.length&&Fu(t,"YAML directive accepts exactly one argument"),null===(n=/^([0-9]+)\.([0-9]+)$/.exec(i[0]))&&Fu(t,"ill-formed argument of the YAML directive"),o=parseInt(n[1],10),r=parseInt(n[2],10),1!==o&&Fu(t,"unacceptable YAML version of the document"),t.version=i[0],t.checkLineBreaks=r<2,1!==r&&2!==r&&Bu(t,"unsupported YAML version of the document")},TAG:function(t,e,i){var n,o;2!==i.length&&Fu(t,"TAG directive accepts exactly two arguments"),n=i[0],o=i[1],Cu.test(n)||Fu(t,"ill-formed tag handle (first argument) of the TAG directive"),yu.call(t.tagMap,n)&&Fu(t,'there is a previously declared suffix for "'+n+'" tag handle'),$u.test(o)||Fu(t,"ill-formed tag prefix (second argument) of the TAG directive");try{o=decodeURIComponent(o)}catch(e){Fu(t,"tag prefix is malformed: "+o)}t.tagMap[n]=o}};function Hu(t,e,i,n){var o,r,a,l;if(e1&&(t.result+=gu.repeat("\n",e-1))}function Zu(t,e){var i,n,o=t.tag,r=t.anchor,a=[],l=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=a),n=t.input.charCodeAt(t.position);0!==n&&(-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,Fu(t,"tab characters must not be used in indentation")),45===n)&&Iu(t.input.charCodeAt(t.position+1));)if(l=!0,t.position++,qu(t,!0,-1)&&t.lineIndent<=e)a.push(null),n=t.input.charCodeAt(t.position);else if(i=t.line,th(t,e,3,!1,!0),a.push(t.result),qu(t,!0,-1),n=t.input.charCodeAt(t.position),(t.line===i||t.lineIndent>e)&&0!==n)Fu(t,"bad indentation of a sequence entry");else if(t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndente)&&(_&&(a=t.line,l=t.lineStart,s=t.position),th(t,e,4,!0,o)&&(_?f=t.result:g=t.result),_||(Xu(t,h,m,p,f,g,a,l,s),p=f=g=null),qu(t,!0,-1),c=t.input.charCodeAt(t.position)),(t.line===r||t.lineIndent>e)&&0!==c)Fu(t,"bad indentation of a mapping entry");else if(t.lineIndent=0))break;0===o?Fu(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):s?Fu(t,"repeat of an indentation width identifier"):(c=e+o-1,s=!0)}if(Su(r)){do{r=t.input.charCodeAt(++t.position)}while(Su(r));if(35===r)do{r=t.input.charCodeAt(++t.position)}while(!Au(r)&&0!==r)}for(;0!==r;){for(Wu(t),t.lineIndent=0,r=t.input.charCodeAt(t.position);(!s||t.lineIndentc&&(c=t.lineIndent),Au(r))d++;else{if(t.lineIndent0){for(o=a,r=0;o>0;o--)(a=zu(l=t.input.charCodeAt(++t.position)))>=0?r=(r<<4)+a:Fu(t,"expected hexadecimal character");t.result+=Du(r),t.position++}else Fu(t,"unknown escape sequence");i=n=t.position}else Au(l)?(Hu(t,i,n,!0),Gu(t,qu(t,!1,e)),i=n=t.position):t.position===t.lineStart&&Ku(t)?Fu(t,"unexpected end of the document within a double quoted scalar"):(t.position++,n=t.position)}Fu(t,"unexpected end of the stream within a double quoted scalar")}(t,h)?g=!0:!function(t){var e,i,n;if(42!==(n=t.input.charCodeAt(t.position)))return!1;for(n=t.input.charCodeAt(++t.position),e=t.position;0!==n&&!Iu(n)&&!Tu(n);)n=t.input.charCodeAt(++t.position);return t.position===e&&Fu(t,"name of an alias node must contain at least one character"),i=t.input.slice(e,t.position),yu.call(t.anchorMap,i)||Fu(t,'unidentified alias "'+i+'"'),t.result=t.anchorMap[i],qu(t,!0,-1),!0}(t)?function(t,e,i){var n,o,r,a,l,s,c,d,u=t.kind,h=t.result;if(Iu(d=t.input.charCodeAt(t.position))||Tu(d)||35===d||38===d||42===d||33===d||124===d||62===d||39===d||34===d||37===d||64===d||96===d)return!1;if((63===d||45===d)&&(Iu(n=t.input.charCodeAt(t.position+1))||i&&Tu(n)))return!1;for(t.kind="scalar",t.result="",o=r=t.position,a=!1;0!==d;){if(58===d){if(Iu(n=t.input.charCodeAt(t.position+1))||i&&Tu(n))break}else if(35===d){if(Iu(t.input.charCodeAt(t.position-1)))break}else{if(t.position===t.lineStart&&Ku(t)||i&&Tu(d))break;if(Au(d)){if(l=t.line,s=t.lineStart,c=t.lineIndent,qu(t,!1,-1),t.lineIndent>=e){a=!0,d=t.input.charCodeAt(t.position);continue}t.position=r,t.line=l,t.lineStart=s,t.lineIndent=c;break}}a&&(Hu(t,o,r,!1),Gu(t,t.line-l),o=r=t.position,a=!1),Su(d)||(r=t.position+1),d=t.input.charCodeAt(++t.position)}return Hu(t,o,r,!1),!!t.result||(t.kind=u,t.result=h,!1)}(t,h,1===i)&&(g=!0,null===t.tag&&(t.tag="?")):(g=!0,null===t.tag&&null===t.anchor||Fu(t,"alias node should not have any properties")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===p&&(g=l&&Zu(t,m))),null===t.tag)null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);else if("?"===t.tag){for(null!==t.result&&"scalar"!==t.kind&&Fu(t,'unacceptable node kind for ! tag; it should be "scalar", not "'+t.kind+'"'),s=0,c=t.implicitTypes.length;s"),null!==t.result&&u.kind!==t.kind&&Fu(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+u.kind+'", not "'+t.kind+'"'),u.resolve(t.result,t.tag)?(t.result=u.construct(t.result,t.tag),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):Fu(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")}return null!==t.listener&&t.listener("close",t),null!==t.tag||null!==t.anchor||g}function eh(t){var e,i,n,o,r=t.position,a=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);0!==(o=t.input.charCodeAt(t.position))&&(qu(t,!0,-1),o=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==o));){for(a=!0,o=t.input.charCodeAt(++t.position),e=t.position;0!==o&&!Iu(o);)o=t.input.charCodeAt(++t.position);for(n=[],(i=t.input.slice(e,t.position)).length<1&&Fu(t,"directive name must not be less than one character in length");0!==o;){for(;Su(o);)o=t.input.charCodeAt(++t.position);if(35===o){do{o=t.input.charCodeAt(++t.position)}while(0!==o&&!Au(o));break}if(Au(o))break;for(e=t.position;0!==o&&!Iu(o);)o=t.input.charCodeAt(++t.position);n.push(t.input.slice(e,t.position))}0!==o&&Wu(t),yu.call(Uu,i)?Uu[i](t,i,n):Bu(t,'unknown document directive "'+i+'"')}qu(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,qu(t,!0,-1)):a&&Fu(t,"directives end mark is expected"),th(t,t.lineIndent-1,4,!1,!0),qu(t,!0,-1),t.checkLineBreaks&&wu.test(t.input.slice(r,t.position))&&Bu(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&Ku(t)?46===t.input.charCodeAt(t.position)&&(t.position+=3,qu(t,!0,-1)):t.position=55296&&n<=56319&&e+1=56320&&i<=57343?1024*(n-55296)+i-56320+65536:n}function xh(t){return/^\n* /.test(t)}function wh(t,e,i,n,o,r,a,l){var s,c=0,d=null,u=!1,h=!1,m=-1!==n,p=-1,f=function(t){return _h(t)&&65279!==t&&!gh(t)&&45!==t&&63!==t&&58!==t&&44!==t&&91!==t&&93!==t&&123!==t&&125!==t&&35!==t&&38!==t&&42!==t&&33!==t&&124!==t&&61!==t&&62!==t&&39!==t&&34!==t&&37!==t&&64!==t&&96!==t}(yh(t,0))&&function(t){return!gh(t)&&58!==t}(yh(t,t.length-1));if(e||a)for(s=0;s=65536?s+=2:s++){if(!_h(c=yh(t,s)))return 5;f=f&&bh(c,d,l),d=c}else{for(s=0;s=65536?s+=2:s++){if(10===(c=yh(t,s)))u=!0,m&&(h=h||s-p-1>n&&" "!==t[p+1],p=s);else if(!_h(c))return 5;f=f&&bh(c,d,l),d=c}h=h||m&&s-p-1>n&&" "!==t[p+1]}return u||h?i>9&&xh(t)?5:a?2===r?5:2:h?4:3:!f||a||o(t)?2===r?5:2:1}function kh(t,e,i,n,o){t.dump=function(){if(0===e.length)return 2===t.quotingType?'""':"''";if(!t.noCompatMode&&(-1!==dh.indexOf(e)||uh.test(e)))return 2===t.quotingType?'"'+e+'"':"'"+e+"'";var r=t.indent*Math.max(1,i),a=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-r),l=n||t.flowLevel>-1&&i>=t.flowLevel;switch(wh(e,l,t.indent,a,(function(e){return function(t,e){var i,n;for(i=0,n=t.implicitTypes.length;i"+Ch(e,t.indent)+$h(ph(function(t,e){var i,n,o=/(\n+)([^\n]*)/g,r=(l=t.indexOf("\n"),l=-1!==l?l:t.length,o.lastIndex=l,Eh(t.slice(0,l),e)),a="\n"===t[0]||" "===t[0];var l;for(;n=o.exec(t);){var s=n[1],c=n[2];i=" "===c[0],r+=s+(a||i||""===c?"":"\n")+Eh(c,e),a=i}return r}(e,a),r));case 5:return'"'+function(t){for(var e,i="",n=0,o=0;o=65536?o+=2:o++)n=yh(t,o),!(e=ch[n])&&_h(n)?(i+=t[o],n>=65536&&(i+=t[o+1])):i+=e||hh(n);return i}(e)+'"';default:throw new rh("impossible error: invalid scalar style")}}()}function Ch(t,e){var i=xh(t)?String(e):"",n="\n"===t[t.length-1];return i+(n&&("\n"===t[t.length-2]||"\n"===t)?"+":n?"":"-")+"\n"}function $h(t){return"\n"===t[t.length-1]?t.slice(0,-1):t}function Eh(t,e){if(""===t||" "===t[0])return t;for(var i,n,o=/ [^ ]/g,r=0,a=0,l=0,s="";i=o.exec(t);)(l=i.index)-r>e&&(n=a>r?a:l,s+="\n"+t.slice(r,n),r=n+1),a=l;return s+="\n",t.length-r>e&&a>r?s+=t.slice(r,a)+"\n"+t.slice(a+1):s+=t.slice(r),s.slice(1)}function Ah(t,e,i,n){var o,r,a,l="",s=t.tag;for(o=0,r=i.length;o tag resolver accepts not "'+s+'" style');n=l.represent[s](e,s)}t.dump=n}return!0}return!1}function Ih(t,e,i,n,o,r,a){t.tag=null,t.dump=i,Sh(t,i,!1)||Sh(t,i,!0);var l,s=lh.call(t.dump),c=n;n&&(n=t.flowLevel<0||t.flowLevel>e);var d,u,h="[object Object]"===s||"[object Array]"===s;if(h&&(u=-1!==(d=t.duplicates.indexOf(i))),(null!==t.tag&&"?"!==t.tag||u||2!==t.indent&&e>0)&&(o=!1),u&&t.usedDuplicates[d])t.dump="*ref_"+d;else{if(h&&u&&!t.usedDuplicates[d]&&(t.usedDuplicates[d]=!0),"[object Object]"===s)n&&0!==Object.keys(t.dump).length?(!function(t,e,i,n){var o,r,a,l,s,c,d="",u=t.tag,h=Object.keys(i);if(!0===t.sortKeys)h.sort();else if("function"==typeof t.sortKeys)h.sort(t.sortKeys);else if(t.sortKeys)throw new rh("sortKeys must be a boolean or a function");for(o=0,r=h.length;o1024)&&(t.dump&&10===t.dump.charCodeAt(0)?c+="?":c+="? "),c+=t.dump,s&&(c+=fh(t,e)),Ih(t,e+1,l,!0,s)&&(t.dump&&10===t.dump.charCodeAt(0)?c+=":":c+=": ",d+=c+=t.dump));t.tag=u,t.dump=d||"{}"}(t,e,t.dump,o),u&&(t.dump="&ref_"+d+t.dump)):(!function(t,e,i){var n,o,r,a,l,s="",c=t.tag,d=Object.keys(i);for(n=0,o=d.length;n1024&&(l+="? "),l+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),Ih(t,e,a,!1,!1)&&(s+=l+=t.dump));t.tag=c,t.dump="{"+s+"}"}(t,e,t.dump),u&&(t.dump="&ref_"+d+" "+t.dump));else if("[object Array]"===s)n&&0!==t.dump.length?(t.noArrayIndent&&!a&&e>0?Ah(t,e-1,t.dump,o):Ah(t,e,t.dump,o),u&&(t.dump="&ref_"+d+t.dump)):(!function(t,e,i){var n,o,r,a="",l=t.tag;for(n=0,o=i.length;n",t.dump=l+" "+t.dump)}return!0}function Th(t,e){var i,n,o=[],r=[];for(zh(t,o,r),i=0,n=r.length;i0}get hasError(){return void 0!==this._errors&&this._errors.length>0}get GUImode(){return this._guiMode}set GUImode(t){this._guiMode=t,At(this,"GUImode-changed",{guiMode:t,guiModeAvailable:!(this.hasWarning||this.hasError||!1===this._guiSupported)})}toggleMode(){this.GUImode=!this.GUImode}focusYamlEditor(){var t,e;(null===(t=this._configElement)||void 0===t?void 0:t.focusYamlEditor)&&this._configElement.focusYamlEditor(),(null===(e=this._yamlEditor)||void 0===e?void 0:e.codemirror)&&this._yamlEditor.codemirror.focus()}async getConfigElement(){}get configElementType(){return this.value?this.value.type:void 0}render(){return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],Wd.prototype,"hass",void 0),n([mt()],Wd.prototype,"_config",void 0),Wd=n([dt(Xl("template"))],Wd);var qd=Object.freeze({__proto__:null,get EntityChipEditor(){return Wd}}); +/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */function Kd(t){return null==t}var Gd=function(t,e){var i,o="";for(i=0;il&&(e=o-l+(r=" ... ").length),i-o>l&&(i=o+l-(a=" ...").length),{str:r+t.slice(e,i).replace(/\t/g,"→")+a,pos:o-e+r.length}}function nu(t,e){return Qd.repeat(" ",e-t.length)+t}var ru=function(t,e){if(e=Object.create(e||null),!t.buffer)return null;e.maxLength||(e.maxLength=79),"number"!=typeof e.indent&&(e.indent=1),"number"!=typeof e.linesBefore&&(e.linesBefore=3),"number"!=typeof e.linesAfter&&(e.linesAfter=2);for(var i,o=/\r?\n|\r|\0/g,n=[0],r=[],a=-1;i=o.exec(t.buffer);)r.push(i.index),n.push(i.index+i[0].length),t.position<=i.index&&a<0&&(a=n.length-2);a<0&&(a=n.length-1);var l,s,c="",d=Math.min(t.line+e.linesAfter,r.length).toString().length,u=e.maxLength-(e.indent+d+3);for(l=1;l<=e.linesBefore&&!(a-l<0);l++)s=ou(t.buffer,n[a-l],r[a-l],t.position-(n[a]-n[a-l]),u),c=Qd.repeat(" ",e.indent)+nu((t.line-l+1).toString(),d)+" | "+s.str+"\n"+c;for(s=ou(t.buffer,n[a],r[a],t.position,u),c+=Qd.repeat(" ",e.indent)+nu((t.line+1).toString(),d)+" | "+s.str+"\n",c+=Qd.repeat("-",e.indent+d+3+s.pos)+"^\n",l=1;l<=e.linesAfter&&!(a+l>=r.length);l++)s=ou(t.buffer,n[a+l],r[a+l],t.position-(n[a]-n[a+l]),u),c+=Qd.repeat(" ",e.indent)+nu((t.line+l+1).toString(),d)+" | "+s.str+"\n";return c.replace(/\n$/,"")},au=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],lu=["scalar","sequence","mapping"];var su=function(t,e){if(e=e||{},Object.keys(e).forEach((function(e){if(-1===au.indexOf(e))throw new iu('Unknown option "'+e+'" is met in definition of "'+t+'" YAML type.')})),this.options=e,this.tag=t,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(t){return t},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.representName=e.representName||null,this.defaultStyle=e.defaultStyle||null,this.multi=e.multi||!1,this.styleAliases=function(t){var e={};return null!==t&&Object.keys(t).forEach((function(i){t[i].forEach((function(t){e[String(t)]=i}))})),e}(e.styleAliases||null),-1===lu.indexOf(this.kind))throw new iu('Unknown kind "'+this.kind+'" is specified for "'+t+'" YAML type.')};function cu(t,e){var i=[];return t[e].forEach((function(t){var e=i.length;i.forEach((function(i,o){i.tag===t.tag&&i.kind===t.kind&&i.multi===t.multi&&(e=o)})),i[e]=t})),i}function du(t){return this.extend(t)}du.prototype.extend=function(t){var e=[],i=[];if(t instanceof su)i.push(t);else if(Array.isArray(t))i=i.concat(t);else{if(!t||!Array.isArray(t.implicit)&&!Array.isArray(t.explicit))throw new iu("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");t.implicit&&(e=e.concat(t.implicit)),t.explicit&&(i=i.concat(t.explicit))}e.forEach((function(t){if(!(t instanceof su))throw new iu("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(t.loadKind&&"scalar"!==t.loadKind)throw new iu("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(t.multi)throw new iu("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),i.forEach((function(t){if(!(t instanceof su))throw new iu("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var o=Object.create(du.prototype);return o.implicit=(this.implicit||[]).concat(e),o.explicit=(this.explicit||[]).concat(i),o.compiledImplicit=cu(o,"implicit"),o.compiledExplicit=cu(o,"explicit"),o.compiledTypeMap=function(){var t,e,i={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function o(t){t.multi?(i.multi[t.kind].push(t),i.multi.fallback.push(t)):i[t.kind][t.tag]=i.fallback[t.tag]=t}for(t=0,e=arguments.length;t=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},octal:function(t){return t>=0?"0o"+t.toString(8):"-0o"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),vu=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");var bu=/^[-+]?[0-9]+e/;var yu=new su("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(t){return null!==t&&!(!vu.test(t)||"_"===t[t.length-1])},construct:function(t){var e,i;return i="-"===(e=t.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(e[0])>=0&&(e=e.slice(1)),".inf"===e?1===i?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===e?NaN:i*parseFloat(e,10)},predicate:function(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||Qd.isNegativeZero(t))},represent:function(t,e){var i;if(isNaN(t))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Qd.isNegativeZero(t))return"-0.0";return i=t.toString(10),bu.test(i)?i.replace("e",".e"):i},defaultStyle:"lowercase"}),xu=uu.extend({implicit:[hu,mu,_u,yu]}),wu=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),ku=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");var Cu=new su("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(t){return null!==t&&(null!==wu.exec(t)||null!==ku.exec(t))},construct:function(t){var e,i,o,n,r,a,l,s,c=0,d=null;if(null===(e=wu.exec(t))&&(e=ku.exec(t)),null===e)throw new Error("Date resolve error");if(i=+e[1],o=+e[2]-1,n=+e[3],!e[4])return new Date(Date.UTC(i,o,n));if(r=+e[4],a=+e[5],l=+e[6],e[7]){for(c=e[7].slice(0,3);c.length<3;)c+="0";c=+c}return e[9]&&(d=6e4*(60*+e[10]+ +(e[11]||0)),"-"===e[9]&&(d=-d)),s=new Date(Date.UTC(i,o,n,r,a,l,c)),d&&s.setTime(s.getTime()-d),s},instanceOf:Date,represent:function(t){return t.toISOString()}});var $u=new su("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(t){return"<<"===t||null===t}}),Eu="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";var Au=new su("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(t){if(null===t)return!1;var e,i,o=0,n=t.length,r=Eu;for(i=0;i64)){if(e<0)return!1;o+=6}return o%8==0},construct:function(t){var e,i,o=t.replace(/[\r\n=]/g,""),n=o.length,r=Eu,a=0,l=[];for(e=0;e>16&255),l.push(a>>8&255),l.push(255&a)),a=a<<6|r.indexOf(o.charAt(e));return 0===(i=n%4*6)?(l.push(a>>16&255),l.push(a>>8&255),l.push(255&a)):18===i?(l.push(a>>10&255),l.push(a>>2&255)):12===i&&l.push(a>>4&255),new Uint8Array(l)},predicate:function(t){return"[object Uint8Array]"===Object.prototype.toString.call(t)},represent:function(t){var e,i,o="",n=0,r=t.length,a=Eu;for(e=0;e>18&63],o+=a[n>>12&63],o+=a[n>>6&63],o+=a[63&n]),n=(n<<8)+t[e];return 0===(i=r%3)?(o+=a[n>>18&63],o+=a[n>>12&63],o+=a[n>>6&63],o+=a[63&n]):2===i?(o+=a[n>>10&63],o+=a[n>>4&63],o+=a[n<<2&63],o+=a[64]):1===i&&(o+=a[n>>2&63],o+=a[n<<4&63],o+=a[64],o+=a[64]),o}}),Su=Object.prototype.hasOwnProperty,Iu=Object.prototype.toString;var Tu=new su("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(t){if(null===t)return!0;var e,i,o,n,r,a=[],l=t;for(e=0,i=l.length;e>10),56320+(t-65536&1023))}for(var Ju=new Array(256),Qu=new Array(256),th=0;th<256;th++)Ju[th]=Gu(th)?1:0,Qu[th]=Gu(th);function eh(t,e){this.input=t,this.filename=e.filename||null,this.schema=e.schema||Lu,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function ih(t,e){var i={name:t.filename,buffer:t.input.slice(0,-1),position:t.position,line:t.line,column:t.position-t.lineStart};return i.snippet=ru(i),new iu(e,i)}function oh(t,e){throw ih(t,e)}function nh(t,e){t.onWarning&&t.onWarning.call(null,ih(t,e))}var rh={YAML:function(t,e,i){var o,n,r;null!==t.version&&oh(t,"duplication of %YAML directive"),1!==i.length&&oh(t,"YAML directive accepts exactly one argument"),null===(o=/^([0-9]+)\.([0-9]+)$/.exec(i[0]))&&oh(t,"ill-formed argument of the YAML directive"),n=parseInt(o[1],10),r=parseInt(o[2],10),1!==n&&oh(t,"unacceptable YAML version of the document"),t.version=i[0],t.checkLineBreaks=r<2,1!==r&&2!==r&&nh(t,"unsupported YAML version of the document")},TAG:function(t,e,i){var o,n;2!==i.length&&oh(t,"TAG directive accepts exactly two arguments"),o=i[0],n=i[1],Vu.test(o)||oh(t,"ill-formed tag handle (first argument) of the TAG directive"),ju.call(t.tagMap,o)&&oh(t,'there is a previously declared suffix for "'+o+'" tag handle'),Fu.test(n)||oh(t,"ill-formed tag prefix (second argument) of the TAG directive");try{n=decodeURIComponent(n)}catch(e){oh(t,"tag prefix is malformed: "+n)}t.tagMap[o]=n}};function ah(t,e,i,o){var n,r,a,l;if(e1&&(t.result+=Qd.repeat("\n",e-1))}function mh(t,e){var i,o,n=t.tag,r=t.anchor,a=[],l=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=a),o=t.input.charCodeAt(t.position);0!==o&&(-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,oh(t,"tab characters must not be used in indentation")),45===o)&&Yu(t.input.charCodeAt(t.position+1));)if(l=!0,t.position++,dh(t,!0,-1)&&t.lineIndent<=e)a.push(null),o=t.input.charCodeAt(t.position);else if(i=t.line,gh(t,e,3,!1,!0),a.push(t.result),dh(t,!0,-1),o=t.input.charCodeAt(t.position),(t.line===i||t.lineIndent>e)&&0!==o)oh(t,"bad indentation of a sequence entry");else if(t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndente)&&(_&&(a=t.line,l=t.lineStart,s=t.position),gh(t,e,4,!0,n)&&(_?f=t.result:g=t.result),_||(sh(t,h,m,p,f,g,a,l,s),p=f=g=null),dh(t,!0,-1),c=t.input.charCodeAt(t.position)),(t.line===r||t.lineIndent>e)&&0!==c)oh(t,"bad indentation of a mapping entry");else if(t.lineIndent=0))break;0===n?oh(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):s?oh(t,"repeat of an indentation width identifier"):(c=e+n-1,s=!0)}if(Hu(r)){do{r=t.input.charCodeAt(++t.position)}while(Hu(r));if(35===r)do{r=t.input.charCodeAt(++t.position)}while(!Uu(r)&&0!==r)}for(;0!==r;){for(ch(t),t.lineIndent=0,r=t.input.charCodeAt(t.position);(!s||t.lineIndentc&&(c=t.lineIndent),Uu(r))d++;else{if(t.lineIndent0){for(n=a,r=0;n>0;n--)(a=Wu(l=t.input.charCodeAt(++t.position)))>=0?r=(r<<4)+a:oh(t,"expected hexadecimal character");t.result+=Zu(r),t.position++}else oh(t,"unknown escape sequence");i=o=t.position}else Uu(l)?(ah(t,i,o,!0),hh(t,dh(t,!1,e)),i=o=t.position):t.position===t.lineStart&&uh(t)?oh(t,"unexpected end of the document within a double quoted scalar"):(t.position++,o=t.position)}oh(t,"unexpected end of the stream within a double quoted scalar")}(t,h)?g=!0:!function(t){var e,i,o;if(42!==(o=t.input.charCodeAt(t.position)))return!1;for(o=t.input.charCodeAt(++t.position),e=t.position;0!==o&&!Yu(o)&&!Xu(o);)o=t.input.charCodeAt(++t.position);return t.position===e&&oh(t,"name of an alias node must contain at least one character"),i=t.input.slice(e,t.position),ju.call(t.anchorMap,i)||oh(t,'unidentified alias "'+i+'"'),t.result=t.anchorMap[i],dh(t,!0,-1),!0}(t)?function(t,e,i){var o,n,r,a,l,s,c,d,u=t.kind,h=t.result;if(Yu(d=t.input.charCodeAt(t.position))||Xu(d)||35===d||38===d||42===d||33===d||124===d||62===d||39===d||34===d||37===d||64===d||96===d)return!1;if((63===d||45===d)&&(Yu(o=t.input.charCodeAt(t.position+1))||i&&Xu(o)))return!1;for(t.kind="scalar",t.result="",n=r=t.position,a=!1;0!==d;){if(58===d){if(Yu(o=t.input.charCodeAt(t.position+1))||i&&Xu(o))break}else if(35===d){if(Yu(t.input.charCodeAt(t.position-1)))break}else{if(t.position===t.lineStart&&uh(t)||i&&Xu(d))break;if(Uu(d)){if(l=t.line,s=t.lineStart,c=t.lineIndent,dh(t,!1,-1),t.lineIndent>=e){a=!0,d=t.input.charCodeAt(t.position);continue}t.position=r,t.line=l,t.lineStart=s,t.lineIndent=c;break}}a&&(ah(t,n,r,!1),hh(t,t.line-l),n=r=t.position,a=!1),Hu(d)||(r=t.position+1),d=t.input.charCodeAt(++t.position)}return ah(t,n,r,!1),!!t.result||(t.kind=u,t.result=h,!1)}(t,h,1===i)&&(g=!0,null===t.tag&&(t.tag="?")):(g=!0,null===t.tag&&null===t.anchor||oh(t,"alias node should not have any properties")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===p&&(g=l&&mh(t,m))),null===t.tag)null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);else if("?"===t.tag){for(null!==t.result&&"scalar"!==t.kind&&oh(t,'unacceptable node kind for ! tag; it should be "scalar", not "'+t.kind+'"'),s=0,c=t.implicitTypes.length;s"),null!==t.result&&u.kind!==t.kind&&oh(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+u.kind+'", not "'+t.kind+'"'),u.resolve(t.result,t.tag)?(t.result=u.construct(t.result,t.tag),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):oh(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")}return null!==t.listener&&t.listener("close",t),null!==t.tag||null!==t.anchor||g}function _h(t){var e,i,o,n,r=t.position,a=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);0!==(n=t.input.charCodeAt(t.position))&&(dh(t,!0,-1),n=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==n));){for(a=!0,n=t.input.charCodeAt(++t.position),e=t.position;0!==n&&!Yu(n);)n=t.input.charCodeAt(++t.position);for(o=[],(i=t.input.slice(e,t.position)).length<1&&oh(t,"directive name must not be less than one character in length");0!==n;){for(;Hu(n);)n=t.input.charCodeAt(++t.position);if(35===n){do{n=t.input.charCodeAt(++t.position)}while(0!==n&&!Uu(n));break}if(Uu(n))break;for(e=t.position;0!==n&&!Yu(n);)n=t.input.charCodeAt(++t.position);o.push(t.input.slice(e,t.position))}0!==n&&ch(t),ju.call(rh,i)?rh[i](t,i,o):nh(t,'unknown document directive "'+i+'"')}dh(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,dh(t,!0,-1)):a&&oh(t,"directives end mark is expected"),gh(t,t.lineIndent-1,4,!1,!0),dh(t,!0,-1),t.checkLineBreaks&&Nu.test(t.input.slice(r,t.position))&&nh(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&uh(t)?46===t.input.charCodeAt(t.position)&&(t.position+=3,dh(t,!0,-1)):t.position=55296&&o<=56319&&e+1=56320&&i<=57343?1024*(o-55296)+i-56320+65536:o}function Lh(t){return/^\n* /.test(t)}function jh(t,e,i,o,n,r,a,l){var s,c=0,d=null,u=!1,h=!1,m=-1!==o,p=-1,f=function(t){return zh(t)&&65279!==t&&!Th(t)&&45!==t&&63!==t&&58!==t&&44!==t&&91!==t&&93!==t&&123!==t&&125!==t&&35!==t&&38!==t&&42!==t&&33!==t&&124!==t&&61!==t&&62!==t&&39!==t&&34!==t&&37!==t&&64!==t&&96!==t}(Dh(t,0))&&function(t){return!Th(t)&&58!==t}(Dh(t,t.length-1));if(e||a)for(s=0;s=65536?s+=2:s++){if(!zh(c=Dh(t,s)))return 5;f=f&&Mh(c,d,l),d=c}else{for(s=0;s=65536?s+=2:s++){if(10===(c=Dh(t,s)))u=!0,m&&(h=h||s-p-1>o&&" "!==t[p+1],p=s);else if(!zh(c))return 5;f=f&&Mh(c,d,l),d=c}h=h||m&&s-p-1>o&&" "!==t[p+1]}return u||h?i>9&&Lh(t)?5:a?2===r?5:2:h?4:3:!f||a||n(t)?2===r?5:2:1}function Ph(t,e,i,o,n){t.dump=function(){if(0===e.length)return 2===t.quotingType?'""':"''";if(!t.noCompatMode&&(-1!==Ch.indexOf(e)||$h.test(e)))return 2===t.quotingType?'"'+e+'"':"'"+e+"'";var r=t.indent*Math.max(1,i),a=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-r),l=o||t.flowLevel>-1&&i>=t.flowLevel;switch(jh(e,l,t.indent,a,(function(e){return function(t,e){var i,o;for(i=0,o=t.implicitTypes.length;i"+Nh(e,t.indent)+Rh(Sh(function(t,e){var i,o,n=/(\n+)([^\n]*)/g,r=(l=t.indexOf("\n"),l=-1!==l?l:t.length,n.lastIndex=l,Vh(t.slice(0,l),e)),a="\n"===t[0]||" "===t[0];var l;for(;o=n.exec(t);){var s=o[1],c=o[2];i=" "===c[0],r+=s+(a||i||""===c?"":"\n")+Vh(c,e),a=i}return r}(e,a),r));case 5:return'"'+function(t){for(var e,i="",o=0,n=0;n=65536?n+=2:n++)o=Dh(t,n),!(e=kh[o])&&zh(o)?(i+=t[n],o>=65536&&(i+=t[n+1])):i+=e||Eh(o);return i}(e)+'"';default:throw new iu("impossible error: invalid scalar style")}}()}function Nh(t,e){var i=Lh(t)?String(e):"",o="\n"===t[t.length-1];return i+(o&&("\n"===t[t.length-2]||"\n"===t)?"+":o?"":"-")+"\n"}function Rh(t){return"\n"===t[t.length-1]?t.slice(0,-1):t}function Vh(t,e){if(""===t||" "===t[0])return t;for(var i,o,n=/ [^ ]/g,r=0,a=0,l=0,s="";i=n.exec(t);)(l=i.index)-r>e&&(o=a>r?a:l,s+="\n"+t.slice(r,o),r=o+1),a=l;return s+="\n",t.length-r>e&&a>r?s+=t.slice(r,a)+"\n"+t.slice(a+1):s+=t.slice(r),s.slice(1)}function Fh(t,e,i,o){var n,r,a,l="",s=t.tag;for(n=0,r=i.length;n tag resolver accepts not "'+s+'" style');o=l.represent[s](e,s)}t.dump=o}return!0}return!1}function Uh(t,e,i,o,n,r,a){t.tag=null,t.dump=i,Bh(t,i,!1)||Bh(t,i,!0);var l,s=xh.call(t.dump),c=o;o&&(o=t.flowLevel<0||t.flowLevel>e);var d,u,h="[object Object]"===s||"[object Array]"===s;if(h&&(u=-1!==(d=t.duplicates.indexOf(i))),(null!==t.tag&&"?"!==t.tag||u||2!==t.indent&&e>0)&&(n=!1),u&&t.usedDuplicates[d])t.dump="*ref_"+d;else{if(h&&u&&!t.usedDuplicates[d]&&(t.usedDuplicates[d]=!0),"[object Object]"===s)o&&0!==Object.keys(t.dump).length?(!function(t,e,i,o){var n,r,a,l,s,c,d="",u=t.tag,h=Object.keys(i);if(!0===t.sortKeys)h.sort();else if("function"==typeof t.sortKeys)h.sort(t.sortKeys);else if(t.sortKeys)throw new iu("sortKeys must be a boolean or a function");for(n=0,r=h.length;n1024)&&(t.dump&&10===t.dump.charCodeAt(0)?c+="?":c+="? "),c+=t.dump,s&&(c+=Ih(t,e)),Uh(t,e+1,l,!0,s)&&(t.dump&&10===t.dump.charCodeAt(0)?c+=":":c+=": ",d+=c+=t.dump));t.tag=u,t.dump=d||"{}"}(t,e,t.dump,n),u&&(t.dump="&ref_"+d+t.dump)):(!function(t,e,i){var o,n,r,a,l,s="",c=t.tag,d=Object.keys(i);for(o=0,n=d.length;o1024&&(l+="? "),l+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),Uh(t,e,a,!1,!1)&&(s+=l+=t.dump));t.tag=c,t.dump="{"+s+"}"}(t,e,t.dump),u&&(t.dump="&ref_"+d+" "+t.dump));else if("[object Array]"===s)o&&0!==t.dump.length?(t.noArrayIndent&&!a&&e>0?Fh(t,e-1,t.dump,n):Fh(t,e,t.dump,n),u&&(t.dump="&ref_"+d+t.dump)):(!function(t,e,i){var o,n,r,a="",l=t.tag;for(o=0,n=i.length;o",t.dump=l+" "+t.dump)}return!0}function Hh(t,e){var i,o,n=[],r=[];for(Yh(t,n,r),i=0,o=r.length;i0}get hasError(){return void 0!==this._errors&&this._errors.length>0}get GUImode(){return this._guiMode}set GUImode(t){this._guiMode=t,zt(this,"GUImode-changed",{guiMode:t,guiModeAvailable:!(this.hasWarning||this.hasError||!1===this._guiSupported)})}toggleMode(){this.GUImode=!this.GUImode}focusYamlEditor(){var t,e;(null===(t=this._configElement)||void 0===t?void 0:t.focusYamlEditor)&&this._configElement.focusYamlEditor(),(null===(e=this._yamlEditor)||void 0===e?void 0:e.codemirror)&&this._yamlEditor.codemirror.focus()}async getConfigElement(){}get configElementType(){return this.value?this.value.type:void 0}render(){return B`
- ${this.GUImode?N` + ${this.GUImode?B`
- ${this._loading?N` + ${this._loading?B` `:this._configElement}
- `:N` + `:B`
`} - ${!1===this._guiSupported&&this.configElementType?N` + ${!1===this._guiSupported&&this.configElementType?B`
${this.hass.localize("ui.errors.config.editor_not_available","type",this.configElementType)}
`:""} - ${this.hasError?N` + ${this.hasError?B`
${this.hass.localize("ui.errors.config.error_detected")}:
    - ${this._errors.map((t=>N`
  • ${t}
  • `))} + ${this._errors.map((t=>B`
  • ${t}
  • `))}
`:""} - ${this.hasWarning?N` + ${this.hasWarning?B` - ${this._warnings.length>0&&void 0!==this._warnings[0]?N` + ${this._warnings.length>0&&void 0!==this._warnings[0]?B`
    - ${this._warnings.map((t=>N`
  • ${t}
  • `))} + ${this._warnings.map((t=>B`
  • ${t}
  • `))}
`:void 0} ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
`:""}
- `}updated(t){super.updated(t),this._configElement&&t.has("hass")&&(this._configElement.hass=this.hass),this._configElement&&"lovelace"in this._configElement&&t.has("lovelace")&&(this._configElement.lovelace=this.lovelace)}_handleUIConfigChanged(t){t.stopPropagation();const e=t.detail.config;this.value=e}_handleYAMLChanged(t){t.stopPropagation();const e=t.detail.value;e!==this.yaml&&(this.yaml=e)}async _updateConfigElement(){var t;if(!this.value)return;let e;try{if(this._errors=void 0,this._warnings=void 0,this._configElementType!==this.configElementType){if(this._guiSupported=void 0,this._configElement=void 0,!this.configElementType)throw new Error(this.hass.localize("ui.errors.config.no_type_provided"));this._configElementType=this.configElementType,this._loading=!0,e=await this.getConfigElement(),e&&(e.hass=this.hass,"lovelace"in e&&(e.lovelace=this.lovelace),e.addEventListener("config-changed",(t=>this._handleUIConfigChanged(t))),this._configElement=e,this._guiSupported=!0)}if(this._configElement)try{this._configElement.setConfig(this.value)}catch(t){const e=((t,e)=>{if(!(e instanceof Yt))return{warnings:[e.message],errors:void 0};const i=[],n=[];for(const o of e.failures())if(void 0===o.value)i.push(t.localize("ui.errors.config.key_missing","key",o.path.join(".")));else if("never"===o.type)n.push(t.localize("ui.errors.config.key_not_expected","key",o.path.join(".")));else{if("union"===o.type)continue;"enums"===o.type?n.push(t.localize("ui.errors.config.key_wrong_type","key",o.path.join("."),"type_correct",o.message.replace("Expected ","").split(", ")[0],"type_wrong",JSON.stringify(o.value))):n.push(t.localize("ui.errors.config.key_wrong_type","key",o.path.join("."),"type_correct",o.refinement||o.type,"type_wrong",JSON.stringify(o.value)))}return{warnings:n,errors:i}})(this.hass,t);throw new Dh("Config is not supported",e.warnings,e.errors)}else this.GUImode=!1}catch(e){e instanceof Dh?(this._warnings=null!==(t=e.warnings)&&void 0!==t?t:[e.message],this._errors=e.errors||void 0):this._errors=[e.message],this.GUImode=!1}finally{this._loading=!1}}_ignoreKeydown(t){t.stopPropagation()}static get styles(){return d` + `}updated(t){super.updated(t),this._configElement&&t.has("hass")&&(this._configElement.hass=this.hass),this._configElement&&"lovelace"in this._configElement&&t.has("lovelace")&&(this._configElement.lovelace=this.lovelace)}_handleUIConfigChanged(t){t.stopPropagation();const e=t.detail.config;this.value=e}_handleYAMLChanged(t){t.stopPropagation();const e=t.detail.value;e!==this.yaml&&(this.yaml=e)}async _updateConfigElement(){var t;if(!this.value)return;let e;try{if(this._errors=void 0,this._warnings=void 0,this._configElementType!==this.configElementType){if(this._guiSupported=void 0,this._configElement=void 0,!this.configElementType)throw new Error(this.hass.localize("ui.errors.config.no_type_provided"));this._configElementType=this.configElementType,this._loading=!0,e=await this.getConfigElement(),e&&(e.hass=this.hass,"lovelace"in e&&(e.lovelace=this.lovelace),e.addEventListener("config-changed",(t=>this._handleUIConfigChanged(t))),this._configElement=e,this._guiSupported=!0)}if(this._configElement)try{this._configElement.setConfig(this.value)}catch(t){const e=((t,e)=>{if(!(e instanceof oe))return{warnings:[e.message],errors:void 0};const i=[],o=[];for(const n of e.failures())if(void 0===n.value)i.push(t.localize("ui.errors.config.key_missing","key",n.path.join(".")));else if("never"===n.type)o.push(t.localize("ui.errors.config.key_not_expected","key",n.path.join(".")));else{if("union"===n.type)continue;"enums"===n.type?o.push(t.localize("ui.errors.config.key_wrong_type","key",n.path.join("."),"type_correct",n.message.replace("Expected ","").split(", ")[0],"type_wrong",JSON.stringify(n.value))):o.push(t.localize("ui.errors.config.key_wrong_type","key",n.path.join("."),"type_correct",n.refinement||n.type,"type_wrong",JSON.stringify(n.value)))}return{warnings:o,errors:i}})(this.hass,t);throw new qh("Config is not supported",e.warnings,e.errors)}else this.GUImode=!1}catch(e){e instanceof qh?(this._warnings=null!==(t=e.warnings)&&void 0!==t?t:[e.message],this._errors=e.errors||void 0):this._errors=[e.message],this.GUImode=!1}finally{this._loading=!1}}_ignoreKeydown(t){t.stopPropagation()}static get styles(){return h` :host { display: flex; } @@ -3181,7 +3361,7 @@ const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.typ display: block; margin: auto; } - `}}n([st({attribute:!1})],jh.prototype,"hass",void 0),n([st({attribute:!1})],jh.prototype,"lovelace",void 0),n([ct()],jh.prototype,"_yaml",void 0),n([ct()],jh.prototype,"_config",void 0),n([ct()],jh.prototype,"_configElement",void 0),n([ct()],jh.prototype,"_configElementType",void 0),n([ct()],jh.prototype,"_guiMode",void 0),n([ct()],jh.prototype,"_errors",void 0),n([ct()],jh.prototype,"_warnings",void 0),n([ct()],jh.prototype,"_guiSupported",void 0),n([ct()],jh.prototype,"_loading",void 0),n([ht("ha-code-editor")],jh.prototype,"_yamlEditor",void 0);let Ph=class extends jh{get configElementType(){var t;return null===(t=this.value)||void 0===t?void 0:t.type}async getConfigElement(){const t=await Nh(this.configElementType);if(t&&t.getConfigElement)return t.getConfigElement()}};Ph=n([at("mushroom-chip-element-editor")],Ph);const Nh=t=>customElements.get($l(t)),Vh=["action","alarm-control-panel","back","conditional","entity","light","menu","template","weather"];let Rh=class extends ot{constructor(){super(...arguments),this._GUImode=!0,this._guiModeAvailable=!0,this._cardTab=!1}setConfig(t){this._config=t}focusYamlEditor(){var t;null===(t=this._cardEditorEl)||void 0===t||t.focusYamlEditor()}render(){var t;if(!this.hass||!this._config)return N``;const e=Hi(this.hass),i=pe(this.hass);return N` + `}}n([ht({attribute:!1})],Kh.prototype,"hass",void 0),n([ht({attribute:!1})],Kh.prototype,"lovelace",void 0),n([mt()],Kh.prototype,"_yaml",void 0),n([mt()],Kh.prototype,"_config",void 0),n([mt()],Kh.prototype,"_configElement",void 0),n([mt()],Kh.prototype,"_configElementType",void 0),n([mt()],Kh.prototype,"_guiMode",void 0),n([mt()],Kh.prototype,"_errors",void 0),n([mt()],Kh.prototype,"_warnings",void 0),n([mt()],Kh.prototype,"_guiSupported",void 0),n([mt()],Kh.prototype,"_loading",void 0),n([gt("ha-code-editor")],Kh.prototype,"_yamlEditor",void 0);let Gh=class extends Kh{get configElementType(){var t;return null===(t=this.value)||void 0===t?void 0:t.type}async getConfigElement(){const t=await Zh(this.configElementType);if(t&&t.getConfigElement)return t.getConfigElement()}};Gh=n([dt("mushroom-chip-element-editor")],Gh);const Zh=t=>customElements.get(Yl(t)),Jh=["action","alarm-control-panel","back","conditional","entity","light","menu","template","weather"];let Qh=class extends st{constructor(){super(...arguments),this._GUImode=!0,this._guiModeAvailable=!0,this._cardTab=!1}setConfig(t){this._config=t}focusYamlEditor(){var t;null===(t=this._cardEditorEl)||void 0===t||t.focusYamlEditor()}render(){var t;if(!this.hass||!this._config)return B``;const e=ao(this.hass),i=Ee(this.hass);return B` - ${this._cardTab?N` + ${this._cardTab?B`
- ${void 0!==(null===(t=this._config.chip)||void 0===t?void 0:t.type)?N` + ${void 0!==(null===(t=this._config.chip)||void 0===t?void 0:t.type)?B`
- `:N` + `:B` - ${Vh.map((t=>N` + ${Jh.map((t=>B` ${e(`editor.chip.chip-picker.types.${t}`)} @@ -3229,10 +3409,10 @@ const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.typ `}
- `:N` + `:B`
${this.hass.localize("ui.panel.lovelace.editor.card.conditional.condition_explanation")} - ${this._config.conditions.map(((t,e)=>{var n;return N` + ${this._config.conditions.map(((t,e)=>{var o;return B`
`} - `}_selectTab(t){this._cardTab=1===t.detail.index}_toggleMode(){var t;null===(t=this._cardEditorEl)||void 0===t||t.toggleMode()}_setMode(t){this._GUImode=t,this._cardEditorEl&&(this._cardEditorEl.GUImode=t)}_handleGUIModeChanged(t){t.stopPropagation(),this._GUImode=t.detail.guiMode,this._guiModeAvailable=t.detail.guiModeAvailable}async _handleChipPicked(t){const e=t.target.value;if(""===e)return;let i;const n=Nh(e);i=n&&n.getStubConfig?await n.getStubConfig(this.hass):{type:e},t.target.value="",t.stopPropagation(),this._config&&(this._setMode(!0),this._guiModeAvailable=!0,this._config=Object.assign(Object.assign({},this._config),{chip:i}),At(this,"config-changed",{config:this._config}))}_handleChipChanged(t){t.stopPropagation(),this._config&&(this._config=Object.assign(Object.assign({},this._config),{chip:t.detail.config}),this._guiModeAvailable=t.detail.guiModeAvailable,At(this,"config-changed",{config:this._config}))}_handleReplaceChip(){this._config&&(this._config=Object.assign(Object.assign({},this._config),{chip:void 0}),At(this,"config-changed",{config:this._config}))}_addCondition(t){const e=t.target;if(""===e.value||!this._config)return;const i=[...this._config.conditions];i.push({entity:e.value,state:""}),this._config=Object.assign(Object.assign({},this._config),{conditions:i}),e.value="",At(this,"config-changed",{config:this._config})}_changeCondition(t){const e=t.target;if(!this._config||!e)return;const i=[...this._config.conditions];if("entity"!==e.configValue||e.value){const t=Object.assign({},i[e.idx]);"entity"===e.configValue?t.entity=e.value:"state"===e.configValue?void 0!==t.state_not?t.state_not=e.value:t.state=e.value:"invert"===e.configValue&&("true"===e.value?t.state&&(t.state_not=t.state,delete t.state):t.state_not&&(t.state=t.state_not,delete t.state_not)),i[e.idx]=t}else i.splice(e.idx,1);this._config=Object.assign(Object.assign({},this._config),{conditions:i}),At(this,"config-changed",{config:this._config})}static get styles(){return d` + `}_selectTab(t){this._cardTab=1===t.detail.index}_toggleMode(){var t;null===(t=this._cardEditorEl)||void 0===t||t.toggleMode()}_setMode(t){this._GUImode=t,this._cardEditorEl&&(this._cardEditorEl.GUImode=t)}_handleGUIModeChanged(t){t.stopPropagation(),this._GUImode=t.detail.guiMode,this._guiModeAvailable=t.detail.guiModeAvailable}async _handleChipPicked(t){const e=t.target.value;if(""===e)return;let i;const o=Zh(e);i=o&&o.getStubConfig?await o.getStubConfig(this.hass):{type:e},t.target.value="",t.stopPropagation(),this._config&&(this._setMode(!0),this._guiModeAvailable=!0,this._config=Object.assign(Object.assign({},this._config),{chip:i}),zt(this,"config-changed",{config:this._config}))}_handleChipChanged(t){t.stopPropagation(),this._config&&(this._config=Object.assign(Object.assign({},this._config),{chip:t.detail.config}),this._guiModeAvailable=t.detail.guiModeAvailable,zt(this,"config-changed",{config:this._config}))}_handleReplaceChip(){this._config&&(this._config=Object.assign(Object.assign({},this._config),{chip:void 0}),zt(this,"config-changed",{config:this._config}))}_addCondition(t){const e=t.target;if(""===e.value||!this._config)return;const i=[...this._config.conditions];i.push({entity:e.value,state:""}),this._config=Object.assign(Object.assign({},this._config),{conditions:i}),e.value="",zt(this,"config-changed",{config:this._config})}_changeCondition(t){const e=t.target;if(!this._config||!e)return;const i=[...this._config.conditions];if("entity"!==e.configValue||e.value){const t=Object.assign({},i[e.idx]);"entity"===e.configValue?t.entity=e.value:"state"===e.configValue?void 0!==t.state_not?t.state_not=e.value:t.state=e.value:"invert"===e.configValue&&("true"===e.value?t.state&&(t.state_not=t.state,delete t.state):t.state_not&&(t.state=t.state_not,delete t.state_not)),i[e.idx]=t}else i.splice(e.idx,1);this._config=Object.assign(Object.assign({},this._config),{conditions:i}),zt(this,"config-changed",{config:this._config})}static get styles(){return h` mwc-tab-bar { border-bottom: 1px solid var(--divider-color); } @@ -3326,31 +3506,31 @@ const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.typ .gui-mode-button { margin-right: auto; } - `}};n([st({attribute:!1})],Rh.prototype,"hass",void 0),n([st({attribute:!1})],Rh.prototype,"lovelace",void 0),n([ct()],Rh.prototype,"_config",void 0),n([ct()],Rh.prototype,"_GUImode",void 0),n([ct()],Rh.prototype,"_guiModeAvailable",void 0),n([ct()],Rh.prototype,"_cardTab",void 0),n([ht("mushroom-chip-element-editor")],Rh.prototype,"_cardEditorEl",void 0),Rh=n([at(El("conditional"))],Rh);var Fh=Object.freeze({__proto__:null,get ConditionalChipEditor(){return Rh}});const Bh=te(xc,te(yc,gc,pc),ce({show_brightness_control:de(re()),show_color_temp_control:de(re()),show_color_control:de(re()),collapsible_controls:de(re()),use_light_color:de(re())})),Uh=["show_brightness_control","use_light_color","show_color_temp_control","show_color_control"],Hh=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:Ts}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,{type:"grid",name:"",schema:[{name:"use_light_color",selector:{boolean:{}}},{name:"show_brightness_control",selector:{boolean:{}}},{name:"show_color_temp_control",selector:{boolean:{}}},{name:"show_color_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(t)]));let Yh=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Uh.includes(t.name)?e(`editor.card.light.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Bh),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Hh(this.hass.connection.haVersion,i);return N` + `}};n([ht({attribute:!1})],Qh.prototype,"hass",void 0),n([ht({attribute:!1})],Qh.prototype,"lovelace",void 0),n([mt()],Qh.prototype,"_config",void 0),n([mt()],Qh.prototype,"_GUImode",void 0),n([mt()],Qh.prototype,"_guiModeAvailable",void 0),n([mt()],Qh.prototype,"_cardTab",void 0),n([gt("mushroom-chip-element-editor")],Qh.prototype,"_cardEditorEl",void 0),Qh=n([dt(Xl("conditional"))],Qh);var tm=Object.freeze({__proto__:null,get ConditionalChipEditor(){return Qh}});const em=he(Gc,he(Kc,Yc,Uc),xe({icon_color:we(ke()),show_brightness_control:we(_e()),show_color_temp_control:we(_e()),show_color_control:we(_e()),collapsible_controls:we(_e()),use_light_color:we(_e())})),im=["show_brightness_control","use_light_color","show_color_temp_control","show_color_control"],om=xt((t=>[{name:"entity",selector:{entity:{domain:ec}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_color",selector:{"mush-color":{}}}]},...Xc,{type:"grid",name:"",schema:[{name:"use_light_color",selector:{boolean:{}}},{name:"show_brightness_control",selector:{boolean:{}}},{name:"show_color_temp_control",selector:{boolean:{}}},{name:"show_color_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc()]));let nm=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):im.includes(t.name)?e(`editor.card.light.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,em),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=om(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Yh.prototype,"_config",void 0),Yh=n([at("mushroom-light-card-editor")],Yh);var Xh=Object.freeze({__proto__:null,LIGHT_LABELS:Uh,get LightCardEditor(){return Yh}});const Wh=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:Ts}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"use_light_color",selector:{boolean:{}}}]},...fc(t)]));let qh=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Uh.includes(t.name)?e(`editor.card.light.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Wh(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],nm.prototype,"_config",void 0),nm=n([dt("mushroom-light-card-editor")],nm);var rm=Object.freeze({__proto__:null,LIGHT_LABELS:im,get LightCardEditor(){return nm}});const am=xt((t=>[{name:"entity",selector:{entity:{domain:ec}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"use_light_color",selector:{boolean:{}}}]},...Hc()]));let lm=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):im.includes(t.name)?e(`editor.card.light.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=am(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],qh.prototype,"hass",void 0),n([ct()],qh.prototype,"_config",void 0),qh=n([at(El("light"))],qh);var Kh=Object.freeze({__proto__:null,get LightChipEditor(){return qh}});const Gh=["more-info","navigate","url","call-service","none"],Zh=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:fl}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{name:"icon",selector:{icon:{placeholder:e}}},...fc(t,Gh)]));let Jh=class extends ot{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Zh(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],lm.prototype,"hass",void 0),n([mt()],lm.prototype,"_config",void 0),lm=n([dt(Xl("light"))],lm);var sm=Object.freeze({__proto__:null,get LightChipEditor(){return lm}});const cm=["more-info","navigate","url","call-service","none"],dm=xt((t=>[{name:"entity",selector:{entity:{domain:Ll}}},{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"content_info",selector:{"mush-info":{}}}]},{name:"icon",selector:{icon:{placeholder:t}}},...Hc(cm)]));let um=class extends st{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}setConfig(t){this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=dm(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([st({attribute:!1})],Jh.prototype,"hass",void 0),n([ct()],Jh.prototype,"_config",void 0),Jh=n([at(El("alarm-control-panel"))],Jh);var Qh=Object.freeze({__proto__:null,get AlarmControlPanelChipEditor(){return Jh}});let tm=class extends ot{constructor(){super(...arguments),this._guiModeAvailable=!0,this._guiMode=!0}render(){const t=Hi(this.hass);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([ht({attribute:!1})],um.prototype,"hass",void 0),n([mt()],um.prototype,"_config",void 0),um=n([dt(Xl("alarm-control-panel"))],um);var hm=Object.freeze({__proto__:null,get AlarmControlPanelChipEditor(){return um}});let mm=class extends st{constructor(){super(...arguments),this._guiModeAvailable=!0,this._guiMode=!0}render(){const t=ao(this.hass);return B`
- ${"chip"===this.config.type?N` + ${"chip"===this.config.type?B` `:""} - `}_goBack(){At(this,"go-back")}_toggleMode(){var t;null===(t=this._editorElement)||void 0===t||t.toggleMode()}_handleGUIModeChanged(t){t.stopPropagation(),this._guiMode=t.detail.guiMode,this._guiModeAvailable=t.detail.guiModeAvailable}_handleConfigChanged(t){this._guiModeAvailable=t.detail.guiModeAvailable}static get styles(){return d` + `}_goBack(){zt(this,"go-back")}_toggleMode(){var t;null===(t=this._editorElement)||void 0===t||t.toggleMode()}_handleGUIModeChanged(t){t.stopPropagation(),this._guiMode=t.detail.guiMode,this._guiModeAvailable=t.detail.guiModeAvailable}_handleConfigChanged(t){this._guiModeAvailable=t.detail.guiModeAvailable}static get styles(){return h` .header { display: flex; justify-content: space-between; @@ -3396,23 +3576,23 @@ const Vc={},Rc=Ae(class extends Se{constructor(t){if(super(t),t.type!==$e&&t.typ align-items: center; justify-content: center; } - `}};n([st({attribute:!1})],tm.prototype,"config",void 0),n([ct()],tm.prototype,"_guiModeAvailable",void 0),n([ct()],tm.prototype,"_guiMode",void 0),n([ht(".editor")],tm.prototype,"_editorElement",void 0),tm=n([at("mushroom-sub-element-editor")],tm); + `}};n([ht({attribute:!1})],mm.prototype,"config",void 0),n([mt()],mm.prototype,"_guiModeAvailable",void 0),n([mt()],mm.prototype,"_guiMode",void 0),n([gt(".editor")],mm.prototype,"_editorElement",void 0),mm=n([dt("mushroom-sub-element-editor")],mm); /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const em={},im=Ae(class extends Se{constructor(){super(...arguments),this.nt=em}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.nt)&&this.nt.length===e.length&&e.every(((t,e)=>t===this.nt[e])))return R}else if(this.nt===e)return R;return this.nt=Array.isArray(e)?Array.from(e):e,this.render(e,i)}});let nm,om=class extends ol{constructor(){super(...arguments),this._attached=!1,this._renderEmptySortable=!1}connectedCallback(){super.connectedCallback(),this._attached=!0}disconnectedCallback(){super.disconnectedCallback(),this._attached=!1}render(){if(!this.chips||!this.hass)return N``;const t=Hi(this.hass);return N` +const pm={},fm=Re(class extends Ve{constructor(){super(...arguments),this.ot=pm}render(t,e){return e()}update(t,[e,i]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every(((t,e)=>t===this.ot[e])))return H}else if(this.ot===e)return H;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,i)}});let gm,_m=class extends wl{constructor(){super(...arguments),this._attached=!1,this._renderEmptySortable=!1}connectedCallback(){super.connectedCallback(),this._attached=!0}disconnectedCallback(){super.disconnectedCallback(),this._attached=!1}render(){if(!this.chips||!this.hass)return B``;const t=ao(this.hass);return B`

${this.label||`${t("editor.chip.chip-picker.chips")} (${this.hass.localize("ui.panel.lovelace.editor.card.config.required")})`}

- ${im([this.chips,this._renderEmptySortable],(()=>this._renderEmptySortable?"":this.chips.map(((e,i)=>N` + ${fm([this.chips,this._renderEmptySortable],(()=>this._renderEmptySortable?"":this.chips.map(((e,i)=>B`
- ${N` + ${B`
${this._renderChipLabel(e)} @@ -3448,13 +3628,13 @@ const em={},im=Ae(class extends Se{constructor(){super(...arguments),this.nt=em} fixedMenuPosition naturalMenuWidth > - ${Vh.map((e=>N` + ${Jh.map((e=>B` ${t(`editor.chip.chip-picker.types.${e}`)} `))} - `}updated(t){var e;super.updated(t);const i=t.has("_attached"),n=t.has("chips");if(n||i)return i&&!this._attached?(null===(e=this._sortable)||void 0===e||e.destroy(),void(this._sortable=void 0)):void(this._sortable||!this.chips?n&&this._handleChipsChanged():this._createSortable())}async _handleChipsChanged(){this._renderEmptySortable=!0,await this.updateComplete;const t=this.shadowRoot.querySelector(".chips");for(;t.lastElementChild;)t.removeChild(t.lastElementChild);this._renderEmptySortable=!1}async _createSortable(){if(!nm){const t=await Promise.resolve().then((function(){return Hg}));nm=t.Sortable,nm.mount(t.OnSpill),nm.mount(t.AutoScroll())}this._sortable=new nm(this.shadowRoot.querySelector(".chips"),{animation:150,fallbackClass:"sortable-fallback",handle:".handle",onEnd:async t=>this._chipMoved(t)})}async _addChips(t){const e=t.target,i=e.value;if(""===i)return;let n;const o=Nh(i);n=o&&o.getStubConfig?await o.getStubConfig(this.hass):{type:i};const r=this.chips.concat(n);e.value="",At(this,"chips-changed",{chips:r})}_chipMoved(t){if(t.oldIndex===t.newIndex)return;const e=this.chips.concat();e.splice(t.newIndex,0,e.splice(t.oldIndex,1)[0]),At(this,"chips-changed",{chips:e})}_removeChip(t){const e=t.currentTarget.index,i=this.chips.concat();i.splice(e,1),At(this,"chips-changed",{chips:i})}_editChip(t){const e=t.currentTarget.index;At(this,"edit-detail-element",{subElementConfig:{index:e,type:"chip",elementConfig:this.chips[e]}})}_renderChipLabel(t){var e;let i=Hi(this.hass)(`editor.chip.chip-picker.types.${t.type}`);if("conditional"===t.type&&t.conditions.length>0){const n=t.conditions[0];i+=` - ${null!==(e=this.getEntityName(n.entity))&&void 0!==e?e:n.entity} ${n.state?`= ${n.state}`:n.state_not?`≠ ${n.state_not}`:null}`}return i}_renderChipSecondary(t){var e;const i=Hi(this.hass);if("entity"in t&&t.entity)return`${null!==(e=this.getEntityName(t.entity))&&void 0!==e?e:t.entity}`;if("chip"in t&&t.chip){const e=i(`editor.chip.chip-picker.types.${t.chip.type}`);return`${this._renderChipSecondary(t.chip)} (via ${e})`}}getEntityName(t){if(!this.hass)return;const e=this.hass.states[t];return e?e.attributes.friendly_name:void 0}static get styles(){return[super.styles,Ue,d` + `}updated(t){var e;super.updated(t);const i=t.has("_attached"),o=t.has("chips");if(o||i)return i&&!this._attached?(null===(e=this._sortable)||void 0===e||e.destroy(),void(this._sortable=void 0)):void(this._sortable||!this.chips?o&&this._handleChipsChanged():this._createSortable())}async _handleChipsChanged(){this._renderEmptySortable=!0,await this.updateComplete;const t=this.shadowRoot.querySelector(".chips");for(;t.lastElementChild;)t.removeChild(t.lastElementChild);this._renderEmptySortable=!1}async _createSortable(){if(!gm){const t=await Promise.resolve().then((function(){return f_}));gm=t.Sortable,gm.mount(t.OnSpill),gm.mount(t.AutoScroll())}this._sortable=new gm(this.shadowRoot.querySelector(".chips"),{animation:150,fallbackClass:"sortable-fallback",handle:".handle",onEnd:async t=>this._chipMoved(t)})}async _addChips(t){const e=t.target,i=e.value;if(""===i)return;let o;const n=Zh(i);o=n&&n.getStubConfig?await n.getStubConfig(this.hass):{type:i};const r=this.chips.concat(o);e.value="",zt(this,"chips-changed",{chips:r})}_chipMoved(t){if(t.oldIndex===t.newIndex)return;const e=this.chips.concat();e.splice(t.newIndex,0,e.splice(t.oldIndex,1)[0]),zt(this,"chips-changed",{chips:e})}_removeChip(t){const e=t.currentTarget.index,i=this.chips.concat();i.splice(e,1),zt(this,"chips-changed",{chips:i})}_editChip(t){const e=t.currentTarget.index;zt(this,"edit-detail-element",{subElementConfig:{index:e,type:"chip",elementConfig:this.chips[e]}})}_renderChipLabel(t){var e;let i=ao(this.hass)(`editor.chip.chip-picker.types.${t.type}`);if("conditional"===t.type&&t.conditions.length>0){const o=t.conditions[0];i+=` - ${null!==(e=this.getEntityName(o.entity))&&void 0!==e?e:o.entity} ${o.state?`= ${o.state}`:o.state_not?`≠ ${o.state_not}`:null}`}return i}_renderChipSecondary(t){var e;const i=ao(this.hass);if("entity"in t&&t.entity)return`${null!==(e=this.getEntityName(t.entity))&&void 0!==e?e:t.entity}`;if("chip"in t&&t.chip){const e=i(`editor.chip.chip-picker.types.${t.chip.type}`);return`${this._renderChipSecondary(t.chip)} (via ${e})`}}getEntityName(t){if(!this.hass)return;const e=this.hass.states[t];return e?e.attributes.friendly_name:void 0}static get styles(){return[super.styles,ei,h` .chip { display: flex; align-items: center; @@ -3501,7 +3681,7 @@ const em={},im=Ae(class extends Se{constructor(){super(...arguments),this.nt=em} font-size: 12px; color: var(--secondary-text-color); } - `]}};n([st({attribute:!1})],om.prototype,"chips",void 0),n([st()],om.prototype,"label",void 0),n([ct()],om.prototype,"_attached",void 0),n([ct()],om.prototype,"_renderEmptySortable",void 0),om=n([at("mushroom-chips-card-chips-editor")],om);const rm=ce({type:le("action"),icon:de(ue()),icon_color:de(ue()),tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be)}),am=ce({type:le("back"),icon:de(ue()),icon_color:de(ue())}),lm=ce({type:le("entity"),entity:de(ue()),name:de(ue()),content_info:de(ue()),icon:de(ue()),icon_color:de(ue()),use_entity_picture:de(re()),tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be)}),sm=ce({type:le("menu"),icon:de(ue()),icon_color:de(ue())}),cm=ce({type:le("weather"),entity:de(ue()),tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be),show_temperature:de(re()),show_conditions:de(re())}),dm=ce({entity:ue(),state:de(ue()),state_not:de(ue())}),um=ce({type:le("conditional"),chip:de(ne()),conditions:de(oe(dm))}),hm=ce({type:le("light"),entity:de(ue()),name:de(ue()),content_info:de(ue()),icon:de(ue()),use_light_color:de(re()),tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be)}),mm=ce({type:le("template"),entity:de(ue()),tap_action:de(Be),hold_action:de(Be),double_tap_action:de(Be),content:de(ue()),icon:de(ue()),icon_color:de(ue()),picture:de(ue()),entity_id:de(me([ue(),oe(ue())]))}),pm=ie((t=>{if(t&&"object"==typeof t&&"type"in t)switch(t.type){case"action":return rm;case"back":return am;case"entity":return lm;case"menu":return sm;case"weather":return cm;case"conditional":return um;case"light":return hm;case"template":return mm}return ce()})),fm=te(xc,ce({chips:oe(pm),alignment:de(ue())}));let gm=class extends ol{connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,fm),this._config=t}get _title(){return this._config.title||""}get _theme(){return this._config.theme||""}render(){if(!this.hass||!this._config)return N``;if(this._subElementEditorConfig)return N` + `]}};n([ht({attribute:!1})],_m.prototype,"chips",void 0),n([ht()],_m.prototype,"label",void 0),n([mt()],_m.prototype,"_attached",void 0),n([mt()],_m.prototype,"_renderEmptySortable",void 0),_m=n([dt("mushroom-chips-card-chips-editor")],_m);const vm=xe({type:be("action"),icon:we(ke()),icon_color:we(ke()),tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti)}),bm=xe({type:be("back"),icon:we(ke()),icon_color:we(ke())}),ym=xe({type:be("entity"),entity:we(ke()),name:we(ke()),content_info:we(ke()),icon:we(ke()),icon_color:we(ke()),use_entity_picture:we(_e()),tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti)}),xm=xe({type:be("menu"),icon:we(ke()),icon_color:we(ke())}),wm=xe({type:be("weather"),entity:we(ke()),tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti),show_temperature:we(_e()),show_conditions:we(_e())}),km=xe({entity:ke(),state:we(ke()),state_not:we(ke())}),Cm=xe({type:be("conditional"),chip:we(fe()),conditions:we(ge(km))}),$m=xe({type:be("light"),entity:we(ke()),name:we(ke()),content_info:we(ke()),icon:we(ke()),use_light_color:we(_e()),tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti)}),Em=xe({type:be("template"),entity:we(ke()),tap_action:we(ti),hold_action:we(ti),double_tap_action:we(ti),content:we(ke()),icon:we(ke()),icon_color:we(ke()),picture:we(ke()),entity_id:we($e([ke(),ge(ke())]))}),Am=pe((t=>{if(t&&"object"==typeof t&&"type"in t)switch(t.type){case"action":return vm;case"back":return bm;case"entity":return ym;case"menu":return xm;case"weather":return wm;case"conditional":return Cm;case"light":return $m;case"template":return Em}return xe()})),Sm=he(Gc,xe({chips:ge(Am),alignment:we(ke())}));let Im=class extends wl{connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Sm),this._config=t}get _title(){return this._config.title||""}get _theme(){return this._config.theme||""}render(){if(!this.hass||!this._config)return B``;if(this._subElementEditorConfig)return B` - `;const t=Hi(this.hass);return N` + `;const t=ao(this.hass);return B`
- `}_valueChanged(t){var e,i,n;if(!this._config||!this.hass)return;const o=t.target,r=o.configValue||(null===(e=this._subElementEditorConfig)||void 0===e?void 0:e.type),a=null!==(n=null!==(i=o.checked)&&void 0!==i?i:t.detail.value)&&void 0!==n?n:o.value;if("chip"===r||t.detail&&t.detail.chips){const e=t.detail.chips||this._config.chips.concat();"chip"===r&&(a?e[this._subElementEditorConfig.index]=a:(e.splice(this._subElementEditorConfig.index,1),this._goBack()),this._subElementEditorConfig.elementConfig=a),this._config=Object.assign(Object.assign({},this._config),{chips:e})}else r&&(a?this._config=Object.assign(Object.assign({},this._config),{[r]:a}):(this._config=Object.assign({},this._config),delete this._config[r]));At(this,"config-changed",{config:this._config})}_handleSubElementChanged(t){var e;if(t.stopPropagation(),!this._config||!this.hass)return;const i=null===(e=this._subElementEditorConfig)||void 0===e?void 0:e.type,n=t.detail.config;if("chip"===i){const t=this._config.chips.concat();n?t[this._subElementEditorConfig.index]=n:(t.splice(this._subElementEditorConfig.index,1),this._goBack()),this._config=Object.assign(Object.assign({},this._config),{chips:t})}else i&&(""===n?(this._config=Object.assign({},this._config),delete this._config[i]):this._config=Object.assign(Object.assign({},this._config),{[i]:n}));this._subElementEditorConfig=Object.assign(Object.assign({},this._subElementEditorConfig),{elementConfig:n}),At(this,"config-changed",{config:this._config})}_editDetailElement(t){this._subElementEditorConfig=t.detail.subElementConfig}_goBack(){this._subElementEditorConfig=void 0}};n([ct()],gm.prototype,"_config",void 0),n([ct()],gm.prototype,"_subElementEditorConfig",void 0),gm=n([at("mushroom-chips-card-editor")],gm);var _m=Object.freeze({__proto__:null,get ChipsCardEditor(){return gm}});const vm=["auto","heat_cool","heat","cool","dry","fan_only","off"],bm=te(xc,te(yc,gc,pc),ce({show_temperature_control:de(re()),hvac_modes:de(oe(ue())),collapsible_controls:de(re())})),ym=["hvac_modes","show_temperature_control"],xm=_t(((t,e,i)=>[{name:"entity",selector:{entity:{domain:Zl}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:i}}},..._c,{type:"grid",name:"",schema:[{name:"hvac_modes",selector:{select:{options:vm.map((e=>({value:e,label:t(`component.climate.state._.${e}`)}))),mode:"dropdown",multiple:!0}}},{name:"show_temperature_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(e)]));let wm=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):ym.includes(t.name)?e(`editor.card.climate.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,bm),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=xm(this.hass.localize,this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){var e,i,o;if(!this._config||!this.hass)return;const n=t.target,r=n.configValue||(null===(e=this._subElementEditorConfig)||void 0===e?void 0:e.type),a=null!==(o=null!==(i=n.checked)&&void 0!==i?i:t.detail.value)&&void 0!==o?o:n.value;if("chip"===r||t.detail&&t.detail.chips){const e=t.detail.chips||this._config.chips.concat();"chip"===r&&(a?e[this._subElementEditorConfig.index]=a:(e.splice(this._subElementEditorConfig.index,1),this._goBack()),this._subElementEditorConfig.elementConfig=a),this._config=Object.assign(Object.assign({},this._config),{chips:e})}else r&&(a?this._config=Object.assign(Object.assign({},this._config),{[r]:a}):(this._config=Object.assign({},this._config),delete this._config[r]));zt(this,"config-changed",{config:this._config})}_handleSubElementChanged(t){var e;if(t.stopPropagation(),!this._config||!this.hass)return;const i=null===(e=this._subElementEditorConfig)||void 0===e?void 0:e.type,o=t.detail.config;if("chip"===i){const t=this._config.chips.concat();o?t[this._subElementEditorConfig.index]=o:(t.splice(this._subElementEditorConfig.index,1),this._goBack()),this._config=Object.assign(Object.assign({},this._config),{chips:t})}else i&&(""===o?(this._config=Object.assign({},this._config),delete this._config[i]):this._config=Object.assign(Object.assign({},this._config),{[i]:o}));this._subElementEditorConfig=Object.assign(Object.assign({},this._subElementEditorConfig),{elementConfig:o}),zt(this,"config-changed",{config:this._config})}_editDetailElement(t){this._subElementEditorConfig=t.detail.subElementConfig}_goBack(){this._subElementEditorConfig=void 0}};n([mt()],Im.prototype,"_config",void 0),n([mt()],Im.prototype,"_subElementEditorConfig",void 0),Im=n([dt("mushroom-chips-card-editor")],Im);var Tm=Object.freeze({__proto__:null,get ChipsCardEditor(){return Im}});const zm=["auto","heat_cool","heat","cool","dry","fan_only","off"],Om=he(Gc,he(Kc,Yc,Uc),xe({show_temperature_control:we(_e()),hvac_modes:we(ge(ke())),collapsible_controls:we(_e())})),Mm=["hvac_modes","show_temperature_control"],Dm=xt(((t,e,i)=>[{name:"entity",selector:{entity:{domain:_s}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:i}}},...Xc,{type:"grid",name:"",schema:[{name:"hvac_modes",selector:{select:{options:zm.map((i=>({value:i,label:t(Xt(e,2023,4)?`component.climate.entity_component._.state.${i}`:`component.climate.state._.${i}`)}))),mode:"dropdown",multiple:!0}}},{name:"show_temperature_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc()]));let Lm=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Mm.includes(t.name)?e(`editor.card.climate.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Om),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Dm(this.hass.localize,this.hass.connection.haVersion,i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],wm.prototype,"_config",void 0),wm=n([at("mushroom-climate-card-editor")],wm);var km=Object.freeze({__proto__:null,get ClimateCardEditor(){return wm}});const Cm=te(xc,te(yc,gc,pc),ce({show_buttons_control:de(re()),show_position_control:de(re()),show_tilt_position_control:de(re())})),$m=["show_buttons_control","show_position_control","show_tilt_position_control"],Em=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:ss}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,{type:"grid",name:"",schema:[{name:"show_position_control",selector:{boolean:{}}},{name:"show_tilt_position_control",selector:{boolean:{}}},{name:"show_buttons_control",selector:{boolean:{}}}]},...fc(t)]));let Am=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):$m.includes(t.name)?e(`editor.card.cover.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Cm),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Em(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Lm.prototype,"_config",void 0),Lm=n([dt("mushroom-climate-card-editor")],Lm);var jm=Object.freeze({__proto__:null,get ClimateCardEditor(){return Lm}});const Pm=he(Gc,he(Kc,Yc,Uc),xe({show_buttons_control:we(_e()),show_position_control:we(_e()),show_tilt_position_control:we(_e())})),Nm=["show_buttons_control","show_position_control","show_tilt_position_control"],Rm=xt((t=>[{name:"entity",selector:{entity:{domain:Ss}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:t}}},...Xc,{type:"grid",name:"",schema:[{name:"show_position_control",selector:{boolean:{}}},{name:"show_tilt_position_control",selector:{boolean:{}}},{name:"show_buttons_control",selector:{boolean:{}}}]},...Hc()]));let Vm=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Nm.includes(t.name)?e(`editor.card.cover.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Pm),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Rm(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Am.prototype,"_config",void 0),Am=n([at("mushroom-cover-card-editor")],Am);var Sm=Object.freeze({__proto__:null,get CoverCardEditor(){return Am}});const Im=te(xc,te(yc,gc,pc),ce({icon_color:de(ue())})),Tm=_t(((t,e)=>[{name:"entity",selector:{entity:{}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"icon_color",selector:{"mush-color":{}}}]},..._c,...fc(t)]));let zm=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Im),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Tm(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Vm.prototype,"_config",void 0),Vm=n([dt("mushroom-cover-card-editor")],Vm);var Fm=Object.freeze({__proto__:null,get CoverCardEditor(){return Vm}});const Bm=he(Gc,he(Kc,Yc,Uc),xe({icon_color:we(ke())})),Um=xt((t=>[{name:"entity",selector:{entity:{}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_color",selector:{"mush-color":{}}}]},...Xc,...Hc()]));let Hm=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Bm),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Um(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],zm.prototype,"_config",void 0),zm=n([at("mushroom-entity-card-editor")],zm);var Om=Object.freeze({__proto__:null,get EntityCardEditor(){return zm}});const Mm=te(xc,te(yc,gc,pc),ce({icon_animation:de(re()),show_percentage_control:de(re()),show_oscillate_control:de(re()),collapsible_controls:de(re())})),Lm=["icon_animation","show_percentage_control","show_oscillate_control"],Dm=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:xs}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"icon_animation",selector:{boolean:{}}}]},..._c,{type:"grid",name:"",schema:[{name:"show_percentage_control",selector:{boolean:{}}},{name:"show_oscillate_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(t)]));let jm=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Lm.includes(t.name)?e(`editor.card.fan.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Mm),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Dm(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Hm.prototype,"_config",void 0),Hm=n([dt("mushroom-entity-card-editor")],Hm);var Ym=Object.freeze({__proto__:null,get EntityCardEditor(){return Hm}});const Xm=he(Gc,he(Kc,Yc,Uc),xe({icon_animation:we(_e()),show_percentage_control:we(_e()),show_oscillate_control:we(_e()),collapsible_controls:we(_e())})),Wm=["icon_animation","show_percentage_control","show_oscillate_control"],qm=xt((t=>[{name:"entity",selector:{entity:{domain:Us}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_animation",selector:{boolean:{}}}]},...Xc,{type:"grid",name:"",schema:[{name:"show_percentage_control",selector:{boolean:{}}},{name:"show_oscillate_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc()]));let Km=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Wm.includes(t.name)?e(`editor.card.fan.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Xm),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=qm(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],jm.prototype,"_config",void 0),jm=n([at("mushroom-fan-card-editor")],jm);var Pm=Object.freeze({__proto__:null,get FanCardEditor(){return jm}});const Nm=te(xc,te(yc,gc,pc),ce({show_target_humidity_control:de(re()),collapsible_controls:de(re())})),Vm=["show_target_humidity_control"],Rm=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:As}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,{type:"grid",name:"",schema:[{name:"show_target_humidity_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(t)]));let Fm=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Vm.includes(t.name)?e(`editor.card.humidifier.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Nm),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Rm(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Km.prototype,"_config",void 0),Km=n([dt("mushroom-fan-card-editor")],Km);var Gm=Object.freeze({__proto__:null,get FanCardEditor(){return Km}});const Zm=he(Gc,he(Kc,Yc,Uc),xe({show_target_humidity_control:we(_e()),collapsible_controls:we(_e())})),Jm=["show_target_humidity_control"],Qm=xt((t=>[{name:"entity",selector:{entity:{domain:Ks}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:t}}},...Xc,{type:"grid",name:"",schema:[{name:"show_target_humidity_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc()]));let tp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Jm.includes(t.name)?e(`editor.card.humidifier.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Zm),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Qm(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Fm.prototype,"_config",void 0),Fm=n([at("mushroom-humidifier-card-editor")],Fm);var Bm=Object.freeze({__proto__:null,get HumidifierCardEditor(){return Fm}});const Um=te(xc,te(yc,gc,pc)),Hm=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:Ps}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,...fc(t)]));let Ym=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Um),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Hm(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],tp.prototype,"_config",void 0),tp=n([dt("mushroom-humidifier-card-editor")],tp);var ep=Object.freeze({__proto__:null,get HumidifierCardEditor(){return tp}});const ip=["slider","buttons"],op=he(Gc,he(Kc,Yc,Uc),xe({icon_color:we(ke()),display_mode:we(ve(ip))})),np=["display_mode"],rp=xt(((t,e)=>[{name:"entity",selector:{entity:{domain:Js}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"icon_color",selector:{"mush-color":{}}}]},...Xc,{name:"display_mode",selector:{select:{options:["default",...ip].map((e=>({value:e,label:t(`editor.card.number.display_mode_list.${e}`)}))),mode:"dropdown"}}},...Hc()]));let ap=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return np.includes(t.name)?e(`editor.card.number.${t.name}`):Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,op),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=ao(this.hass),n=rp(o,i),r=Object.assign({},this._config);return r.display_mode||(r.display_mode="default"),B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Ym.prototype,"_config",void 0),Ym=n([at("mushroom-lock-card-editor")],Ym);var Xm=Object.freeze({__proto__:null,get LockCardEditor(){return Ym}});const Wm=["on_off","shuffle","previous","play_pause_stop","next","repeat"],qm=["volume_mute","volume_set","volume_buttons"],Km=te(xc,te(yc,gc,pc),ce({use_media_info:de(re()),show_volume_level:de(re()),volume_controls:de(oe(ae(qm))),media_controls:de(oe(ae(Wm))),collapsible_controls:de(re())})),Gm=["use_media_info","use_media_artwork","show_volume_level","media_controls","volume_controls"],Zm=_t(((t,e,i)=>[{name:"entity",selector:{entity:{domain:Hs}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:i}}},..._c,{type:"grid",name:"",schema:[{name:"use_media_info",selector:{boolean:{}}},{name:"show_volume_level",selector:{boolean:{}}}]},{type:"grid",name:"",schema:[{name:"volume_controls",selector:{select:{options:qm.map((e=>({value:e,label:t(`editor.card.media-player.volume_controls_list.${e}`)}))),mode:"list",multiple:!0}}},{name:"media_controls",selector:{select:{options:Wm.map((e=>({value:e,label:t(`editor.card.media-player.media_controls_list.${e}`)}))),mode:"list",multiple:!0}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(e)]));let Jm=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):Gm.includes(t.name)?e(`editor.card.media-player.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,Km),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Hi(this.hass),o=Zm(n,this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){const e=t.detail.value;"default"===e.display_mode&&delete e.display_mode,zt(this,"config-changed",{config:t.detail.value})}};n([mt()],ap.prototype,"_config",void 0),ap=n([dt("mushroom-number-card-editor")],ap);var lp=Object.freeze({__proto__:null,NUMBER_LABELS:np,get NumberCardEditor(){return ap}});const sp=he(Gc,he(Kc,Yc,Uc)),cp=xt((t=>[{name:"entity",selector:{entity:{domain:sc}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:t}}},...Xc,...Hc()]));let dp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,sp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=cp(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],Jm.prototype,"_config",void 0),Jm=n([at("mushroom-media-player-card-editor")],Jm);var Qm=Object.freeze({__proto__:null,MEDIA_LABELS:Gm,get MediaCardEditor(){return Jm}});const tp=te(xc,te(yc,gc,pc)),ep=["more-info","navigate","url","call-service","none"],ip=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:Js}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,...fc(t,ep)]));let np=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,tp),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=ip(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],dp.prototype,"_config",void 0),dp=n([dt("mushroom-lock-card-editor")],dp);var up=Object.freeze({__proto__:null,get LockCardEditor(){return dp}});const hp=["on_off","shuffle","previous","play_pause_stop","next","repeat"],mp=["volume_mute","volume_set","volume_buttons"],pp=he(Gc,he(Kc,Yc,Uc),xe({use_media_info:we(_e()),show_volume_level:we(_e()),volume_controls:we(ge(ve(mp))),media_controls:we(ge(ve(hp))),collapsible_controls:we(_e())})),fp=["use_media_info","use_media_artwork","show_volume_level","media_controls","volume_controls"],gp=xt(((t,e)=>[{name:"entity",selector:{entity:{domain:fc}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},...Xc,{type:"grid",name:"",schema:[{name:"use_media_info",selector:{boolean:{}}},{name:"show_volume_level",selector:{boolean:{}}}]},{type:"grid",name:"",schema:[{name:"volume_controls",selector:{select:{options:mp.map((e=>({value:e,label:t(`editor.card.media-player.volume_controls_list.${e}`)}))),mode:"list",multiple:!0}}},{name:"media_controls",selector:{select:{options:hp.map((e=>({value:e,label:t(`editor.card.media-player.media_controls_list.${e}`)}))),mode:"list",multiple:!0}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc()]));let _p=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):fp.includes(t.name)?e(`editor.card.media-player.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,pp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=ao(this.hass),n=gp(o,i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],np.prototype,"_config",void 0),np=n([at("mushroom-person-card-editor")],np);var op=Object.freeze({__proto__:null,get SwitchCardEditor(){return np}});const rp=te(xc,ce({title:de(ue()),subtitle:de(ue()),alignment:de(ue())})),ap=["title","subtitle"],lp=_t((t=>[{name:"title",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"subtitle",selector:He(t,2022,5)?{template:{}}:{text:{multiline:!0}}},{name:"alignment",selector:{"mush-alignment":{}}}]));let sp=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return ap.includes(t.name)?e(`editor.card.title.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,rp),this._config=t}render(){return this.hass&&this._config?N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],_p.prototype,"_config",void 0),_p=n([dt("mushroom-media-player-card-editor")],_p);var vp=Object.freeze({__proto__:null,MEDIA_LABELS:fp,get MediaCardEditor(){return _p}});const bp=he(Gc,he(Kc,Yc,Uc)),yp=["more-info","navigate","url","call-service","none"],xp=xt((t=>[{name:"entity",selector:{entity:{domain:kc}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:t}}},...Xc,...Hc(yp)]));let wp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,bp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=xp(i);return B` - `:N``}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],sp.prototype,"_config",void 0),sp=n([at("mushroom-title-card-editor")],sp);var cp=Object.freeze({__proto__:null,get TitleCardEditor(){return sp}});const dp=te(xc,te(yc,gc,pc),ce({show_buttons_control:de(re()),collapsible_controls:de(re())})),up=["show_buttons_control"],hp=["more-info","navigate","url","call-service","none"],mp=_t(((t,e)=>[{name:"entity",selector:{entity:{domain:oc}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:e}}},..._c,{type:"grid",name:"",schema:[{name:"show_buttons_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...fc(t,hp)]));let pp=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):up.includes(t.name)?e(`editor.card.update.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,dp),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=mp(this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],wp.prototype,"_config",void 0),wp=n([dt("mushroom-person-card-editor")],wp);var kp=Object.freeze({__proto__:null,get SwitchCardEditor(){return wp}});const Cp=he(Gc,he(Kc,Yc,Uc),xe({icon_color:we(ke())})),$p=["more-info","navigate","url","call-service","none"],Ep=xt((t=>[{name:"entity",selector:{entity:{domain:$c}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:t}}},{name:"icon_color",selector:{"mush-color":{}}}]},...Xc,...Hc($p)]));let Ap=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Cp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Ep(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],pp.prototype,"_config",void 0),pp=n([at("mushroom-update-card-editor")],pp);var fp=Object.freeze({__proto__:null,get UpdateCardEditor(){return pp}});const gp=["on_off","start_pause","stop","locate","clean_spot","return_home"],_p=te(xc,te(yc,gc,pc),ce({icon_animation:de(re()),commands:de(oe(ue()))})),vp=["commands"],bp=_t(((t,e,i,n)=>[{name:"entity",selector:{entity:{domain:sc}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:n}}},{name:"icon_animation",selector:{boolean:{}}}]},..._c,{name:"commands",selector:{select:{mode:"list",multiple:!0,options:gp.map((i=>({value:i,label:"on_off"===i?e(`editor.card.vacuum.commands_list.${i}`):t(`ui.dialogs.more_info_control.vacuum.${i}`)})))}}},...fc(i)]));let yp=class extends ol{constructor(){super(...arguments),this._computeLabel=t=>{const e=Hi(this.hass);return vc.includes(t.name)?e(`editor.card.generic.${t.name}`):vp.includes(t.name)?e(`editor.card.vacuum.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),bc(this.hass.connection.haVersion)}setConfig(t){Jt(t,_p),this._config=t}render(){if(!this.hass||!this._config)return N``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?pl(t):void 0,i=this._config.icon||e,n=Hi(this.hass),o=bp(this.hass.localize,n,this.hass.connection.haVersion,i);return N` + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Ap.prototype,"_config",void 0),Ap=n([dt("mushroom-select-card-editor")],Ap);var Sp=Object.freeze({__proto__:null,get SelectCardEditor(){return Ap}});const Ip=he(Gc,xe({title:we(ke()),subtitle:we(ke()),alignment:we(ke()),title_tap_action:we(ti),subtitle_tap_action:we(ti)})),Tp=["navigate","url","none"],zp=["title","subtitle","title_tap_action","subtitle_tap_action"],Op=xt((()=>[{name:"title",selector:{template:{}}},{name:"subtitle",selector:{template:{}}},{name:"alignment",selector:{"mush-alignment":{}}},{name:"title_tap_action",selector:{"ui-action":{actions:Tp}}},{name:"subtitle_tap_action",selector:{"ui-action":{actions:Tp}}}]));let Mp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return zp.includes(t.name)?e(`editor.card.title.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Ip),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=Op();return B` + + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Mp.prototype,"_config",void 0),Mp=n([dt("mushroom-title-card-editor")],Mp);var Dp=Object.freeze({__proto__:null,get TitleCardEditor(){return Mp}});const Lp=he(Gc,he(Kc,Yc,Uc),xe({show_buttons_control:we(_e()),collapsible_controls:we(_e())})),jp=["show_buttons_control"],Pp=["more-info","navigate","url","call-service","none"],Np=xt((t=>[{name:"entity",selector:{entity:{domain:Mc}}},{name:"name",selector:{text:{}}},{name:"icon",selector:{icon:{placeholder:t}}},...Xc,{type:"grid",name:"",schema:[{name:"show_buttons_control",selector:{boolean:{}}},{name:"collapsible_controls",selector:{boolean:{}}}]},...Hc(Pp)]));let Rp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):jp.includes(t.name)?e(`editor.card.update.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Lp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=Np(i);return B` - `}_valueChanged(t){At(this,"config-changed",{config:t.detail.value})}};n([ct()],yp.prototype,"_config",void 0),yp=n([at("mushroom-vacuum-card-editor")],yp);var xp=Object.freeze({__proto__:null,get VacuumCardEditor(){return yp}}); + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Rp.prototype,"_config",void 0),Rp=n([dt("mushroom-update-card-editor")],Rp);var Vp=Object.freeze({__proto__:null,get UpdateCardEditor(){return Rp}});const Fp=["on_off","start_pause","stop","locate","clean_spot","return_home"],Bp=he(Gc,he(Kc,Yc,Uc),xe({icon_animation:we(_e()),commands:we(ge(ke()))})),Up=["commands"],Hp=xt(((t,e,i)=>[{name:"entity",selector:{entity:{domain:Pc}}},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:i}}},{name:"icon_animation",selector:{boolean:{}}}]},...Xc,{name:"commands",selector:{select:{mode:"list",multiple:!0,options:Fp.map((i=>({value:i,label:"on_off"===i?e(`editor.card.vacuum.commands_list.${i}`):t(`ui.dialogs.more_info_control.vacuum.${i}`)})))}}},...Hc()]));let Yp=class extends wl{constructor(){super(...arguments),this._computeLabel=t=>{const e=ao(this.hass);return Wc.includes(t.name)?e(`editor.card.generic.${t.name}`):Up.includes(t.name)?e(`editor.card.vacuum.${t.name}`):this.hass.localize(`ui.panel.lovelace.editor.card.generic.${t.name}`)}}connectedCallback(){super.connectedCallback(),qc()}setConfig(t){de(t,Bp),this._config=t}render(){if(!this.hass||!this._config)return B``;const t=this._config.entity?this.hass.states[this._config.entity]:void 0,e=t?Dl(t):void 0,i=this._config.icon||e,o=ao(this.hass),n=Hp(this.hass.localize,o,i);return B` + + `}_valueChanged(t){zt(this,"config-changed",{config:t.detail.value})}};n([mt()],Yp.prototype,"_config",void 0),Yp=n([dt("mushroom-vacuum-card-editor")],Yp);var Xp=Object.freeze({__proto__:null,get VacuumCardEditor(){return Yp}}); /**! * Sortable 1.15.0 * @author RubaXa * @author owenm * @license MIT - */function wp(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,n)}return i}function kp(t){for(var e=1;e=0||(o[i]=t[i]);return o}(t,e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(t,i)&&(o[i]=t[i])}return o}function Sp(t){return function(t){if(Array.isArray(t))return Ip(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(!t)return;if("string"==typeof t)return Ip(t,e);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return Ip(t,e)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Ip(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,n=new Array(e);i"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function Fp(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function Bp(t,e,i,n){if(t){i=i||document;do{if(null!=e&&(">"===e[0]?t.parentNode===i&&Rp(t,e):Rp(t,e))||n&&t===i)return t;if(t===i)break}while(t=Fp(t))}return null}var Up,Hp=/\s+/g;function Yp(t,e,i){if(t&&e)if(t.classList)t.classList[i?"add":"remove"](e);else{var n=(" "+t.className+" ").replace(Hp," ").replace(" "+e+" "," ");t.className=(n+(i?" "+e:"")).replace(Hp," ")}}function Xp(t,e,i){var n=t&&t.style;if(n){if(void 0===i)return document.defaultView&&document.defaultView.getComputedStyle?i=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(i=t.currentStyle),void 0===e?i:i[e];e in n||-1!==e.indexOf("webkit")||(e="-webkit-"+e),n[e]=i+("string"==typeof i?"":"px")}}function Wp(t,e){var i="";if("string"==typeof t)i=t;else do{var n=Xp(t,"transform");n&&"none"!==n&&(i=n+" "+i)}while(!e&&(t=t.parentNode));var o=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return o&&new o(i)}function qp(t,e,i){if(t){var n=t.getElementsByTagName(e),o=0,r=n.length;if(i)for(;o=r:o<=r))return n;if(n===Kp())break;n=nf(n,!1)}return!1}function Jp(t,e,i,n){for(var o=0,r=0,a=t.children;r2&&void 0!==arguments[2]?arguments[2]:{},n=i.evt,o=Ap(i,gf);pf.pluginEvent.bind(cg)(t,e,kp({dragEl:bf,parentEl:yf,ghostEl:xf,rootEl:wf,nextEl:kf,lastDownEl:Cf,cloneEl:$f,cloneHidden:Ef,dragStarted:Vf,putSortable:Of,activeSortable:cg.active,originalEvent:n,oldIndex:Af,oldDraggableIndex:If,newIndex:Sf,newDraggableIndex:Tf,hideGhostForTarget:rg,unhideGhostForTarget:ag,cloneNowHidden:function(){Ef=!0},cloneNowShown:function(){Ef=!1},dispatchSortableEvent:function(t){vf({sortable:e,name:t,originalEvent:n})}},o))};function vf(t){ff(kp({putSortable:Of,cloneEl:$f,targetEl:bf,rootEl:wf,oldIndex:Af,oldDraggableIndex:If,newIndex:Sf,newDraggableIndex:Tf},t))}var bf,yf,xf,wf,kf,Cf,$f,Ef,Af,Sf,If,Tf,zf,Of,Mf,Lf,Df,jf,Pf,Nf,Vf,Rf,Ff,Bf,Uf,Hf=!1,Yf=!1,Xf=[],Wf=!1,qf=!1,Kf=[],Gf=!1,Zf=[],Jf="undefined"!=typeof document,Qf=Dp,tg=Op||zp?"cssFloat":"float",eg=Jf&&!jp&&!Dp&&"draggable"in document.createElement("div"),ig=function(){if(Jf){if(zp)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),ng=function(t,e){var i=Xp(t),n=parseInt(i.width)-parseInt(i.paddingLeft)-parseInt(i.paddingRight)-parseInt(i.borderLeftWidth)-parseInt(i.borderRightWidth),o=Jp(t,0,e),r=Jp(t,1,e),a=o&&Xp(o),l=r&&Xp(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+Gp(o).width,c=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+Gp(r).width;if("flex"===i.display)return"column"===i.flexDirection||"column-reverse"===i.flexDirection?"vertical":"horizontal";if("grid"===i.display)return i.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(o&&a.float&&"none"!==a.float){var d="left"===a.float?"left":"right";return!r||"both"!==l.clear&&l.clear!==d?"horizontal":"vertical"}return o&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||s>=n&&"none"===i[tg]||r&&"none"===i[tg]&&s+c>n)?"vertical":"horizontal"},og=function(t){function e(t,i){return function(n,o,r,a){var l=n.options.group.name&&o.options.group.name&&n.options.group.name===o.options.group.name;if(null==t&&(i||l))return!0;if(null==t||!1===t)return!1;if(i&&"clone"===t)return t;if("function"==typeof t)return e(t(n,o,r,a),i)(n,o,r,a);var s=(i?n:o).options.group.name;return!0===t||"string"==typeof t&&t===s||t.join&&t.indexOf(s)>-1}}var i={},n=t.group;n&&"object"==Cp(n)||(n={name:n}),i.name=n.name,i.checkPull=e(n.pull,!0),i.checkPut=e(n.put),i.revertClone=n.revertClone,t.group=i},rg=function(){!ig&&xf&&Xp(xf,"display","none")},ag=function(){!ig&&xf&&Xp(xf,"display","")};Jf&&!jp&&document.addEventListener("click",(function(t){if(Yf)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Yf=!1,!1}),!0);var lg=function(t){if(bf){var e=function(t,e){var i;return Xf.some((function(n){var o=n[df].options.emptyInsertThreshold;if(o&&!Qp(n)){var r=Gp(n),a=t>=r.left-o&&t<=r.right+o,l=e>=r.top-o&&e<=r.bottom+o;return a&&l?i=n:void 0}})),i}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var i={};for(var n in t)t.hasOwnProperty(n)&&(i[n]=t[n]);i.target=i.rootEl=e,i.preventDefault=void 0,i.stopPropagation=void 0,e[df]._onDragOver(i)}}},sg=function(t){bf&&bf.parentNode[df]._isOutsideThisEl(t.target)};function cg(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=Ep({},e),t[df]=this;var i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return ng(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==cg.supportPointer&&"PointerEvent"in window&&!Lp,emptyInsertThreshold:5};for(var n in pf.initializePlugins(this,t,i),i)!(n in e)&&(e[n]=i[n]);for(var o in og(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&eg,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?Np(t,"pointerdown",this._onTapStart):(Np(t,"mousedown",this._onTapStart),Np(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(Np(t,"dragover",this),Np(t,"dragenter",this)),Xf.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),Ep(this,uf())}function dg(t,e,i,n,o,r,a,l){var s,c,d=t[df],u=d.options.onMove;return!window.CustomEvent||zp||Op?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=i,s.draggedRect=n,s.related=o||e,s.relatedRect=r||Gp(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),u&&(c=u.call(d,s,a)),c}function ug(t){t.draggable=!1}function hg(){Gf=!1}function mg(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,i=e.length,n=0;i--;)n+=e.charCodeAt(i);return n.toString(36)}function pg(t){return setTimeout(t,0)}function fg(t){return clearTimeout(t)}cg.prototype={constructor:cg,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(Rf=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,bf):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,i=this.el,n=this.options,o=n.preventOnFilter,r=t.type,a=t.touches&&t.touches[0]||t.pointerType&&"touch"===t.pointerType&&t,l=(a||t).target,s=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||l,c=n.filter;if(function(t){Zf.length=0;var e=t.getElementsByTagName("input"),i=e.length;for(;i--;){var n=e[i];n.checked&&Zf.push(n)}}(i),!bf&&!(/mousedown|pointerdown/.test(r)&&0!==t.button||n.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!Lp||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=Bp(l,n.draggable,i,!1))&&l.animated||Cf===l)){if(Af=tf(l),If=tf(l,n.draggable),"function"==typeof c){if(c.call(this,t,l,this))return vf({sortable:e,rootEl:s,name:"filter",targetEl:l,toEl:i,fromEl:i}),_f("filter",e,{evt:t}),void(o&&t.cancelable&&t.preventDefault())}else if(c&&(c=c.split(",").some((function(n){if(n=Bp(s,n.trim(),i,!1))return vf({sortable:e,rootEl:n,name:"filter",targetEl:l,fromEl:i,toEl:i}),_f("filter",e,{evt:t}),!0}))))return void(o&&t.cancelable&&t.preventDefault());n.handle&&!Bp(s,n.handle,i,!1)||this._prepareDragStart(t,a,l)}}},_prepareDragStart:function(t,e,i){var n,o=this,r=o.el,a=o.options,l=r.ownerDocument;if(i&&!bf&&i.parentNode===r){var s=Gp(i);if(wf=r,yf=(bf=i).parentNode,kf=bf.nextSibling,Cf=i,zf=a.group,cg.dragged=bf,Mf={target:bf,clientX:(e||t).clientX,clientY:(e||t).clientY},Pf=Mf.clientX-s.left,Nf=Mf.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,bf.style["will-change"]="all",n=function(){_f("delayEnded",o,{evt:t}),cg.eventCanceled?o._onDrop():(o._disableDelayedDragEvents(),!Mp&&o.nativeDraggable&&(bf.draggable=!0),o._triggerDragStart(t,e),vf({sortable:o,name:"choose",originalEvent:t}),Yp(bf,a.chosenClass,!0))},a.ignore.split(",").forEach((function(t){qp(bf,t.trim(),ug)})),Np(l,"dragover",lg),Np(l,"mousemove",lg),Np(l,"touchmove",lg),Np(l,"mouseup",o._onDrop),Np(l,"touchend",o._onDrop),Np(l,"touchcancel",o._onDrop),Mp&&this.nativeDraggable&&(this.options.touchStartThreshold=4,bf.draggable=!0),_f("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(Op||zp))n();else{if(cg.eventCanceled)return void this._onDrop();Np(l,"mouseup",o._disableDelayedDrag),Np(l,"touchend",o._disableDelayedDrag),Np(l,"touchcancel",o._disableDelayedDrag),Np(l,"mousemove",o._delayedDragTouchMoveHandler),Np(l,"touchmove",o._delayedDragTouchMoveHandler),a.supportPointer&&Np(l,"pointermove",o._delayedDragTouchMoveHandler),o._dragStartTimer=setTimeout(n,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){bf&&ug(bf),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;Vp(t,"mouseup",this._disableDelayedDrag),Vp(t,"touchend",this._disableDelayedDrag),Vp(t,"touchcancel",this._disableDelayedDrag),Vp(t,"mousemove",this._delayedDragTouchMoveHandler),Vp(t,"touchmove",this._delayedDragTouchMoveHandler),Vp(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?Np(document,"pointermove",this._onTouchMove):Np(document,e?"touchmove":"mousemove",this._onTouchMove):(Np(bf,"dragend",this),Np(wf,"dragstart",this._onDragStart));try{document.selection?pg((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(Hf=!1,wf&&bf){_f("dragStarted",this,{evt:e}),this.nativeDraggable&&Np(document,"dragover",sg);var i=this.options;!t&&Yp(bf,i.dragClass,!1),Yp(bf,i.ghostClass,!0),cg.active=this,t&&this._appendGhost(),vf({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(Lf){this._lastX=Lf.clientX,this._lastY=Lf.clientY,rg();for(var t=document.elementFromPoint(Lf.clientX,Lf.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(Lf.clientX,Lf.clientY))!==e;)e=t;if(bf.parentNode[df]._isOutsideThisEl(t),e)do{if(e[df]){if(e[df]._onDragOver({clientX:Lf.clientX,clientY:Lf.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);ag()}},_onTouchMove:function(t){if(Mf){var e=this.options,i=e.fallbackTolerance,n=e.fallbackOffset,o=t.touches?t.touches[0]:t,r=xf&&Wp(xf,!0),a=xf&&r&&r.a,l=xf&&r&&r.d,s=Qf&&Uf&&ef(Uf),c=(o.clientX-Mf.clientX+n.x)/(a||1)+(s?s[0]-Kf[0]:0)/(a||1),d=(o.clientY-Mf.clientY+n.y)/(l||1)+(s?s[1]-Kf[1]:0)/(l||1);if(!cg.active&&!Hf){if(i&&Math.max(Math.abs(o.clientX-this._lastX),Math.abs(o.clientY-this._lastY))n.right+o||t.clientX<=n.right&&t.clientY>n.bottom&&t.clientX>=n.left:t.clientX>n.right&&t.clientY>n.top||t.clientX<=n.right&&t.clientY>n.bottom+o}(t,o,this)&&!f.animated){if(f===bf)return z(!1);if(f&&r===t.target&&(a=f),a&&(i=Gp(a)),!1!==dg(wf,r,bf,e,a,i,t,!!a))return T(),f&&f.nextSibling?r.insertBefore(bf,f.nextSibling):r.appendChild(bf),yf=r,O(),z(!0)}else if(f&&function(t,e,i){var n=Gp(Jp(i.el,0,i.options,!0)),o=10;return e?t.clientXd+c*r/2:su-Bf)return-Ff}else if(s>d+c*(1-o)/2&&su-c*r/2))return s>d+c/2?1:-1;return 0}(t,a,i,o,x?1:l.swapThreshold,null==l.invertedSwapThreshold?l.swapThreshold:l.invertedSwapThreshold,qf,Rf===a),0!==_){var $=tf(bf);do{$-=_,b=yf.children[$]}while(b&&("none"===Xp(b,"display")||b===xf))}if(0===_||b===a)return z(!1);Rf=a,Ff=_;var E=a.nextElementSibling,A=!1,S=dg(wf,r,bf,e,a,i,t,A=1===_);if(!1!==S)return 1!==S&&-1!==S||(A=1===S),Gf=!0,setTimeout(hg,30),T(),A&&!E?r.appendChild(bf):a.parentNode.insertBefore(bf,A?E:a),k&&af(k,0,C-k.scrollTop),yf=bf.parentNode,void 0===v||qf||(Bf=Math.abs(v-Gp(a)[w])),O(),z(!0)}if(r.contains(bf))return z(!1)}return!1}function I(l,s){_f(l,m,kp({evt:t,isOwner:d,axis:o?"vertical":"horizontal",revert:n,dragRect:e,targetRect:i,canSort:u,fromSortable:h,target:a,completed:z,onMove:function(i,n){return dg(wf,r,bf,e,i,Gp(i),t,n)},changed:O},s))}function T(){I("dragOverAnimationCapture"),m.captureAnimationState(),m!==h&&h.captureAnimationState()}function z(e){return I("dragOverCompleted",{insertion:e}),e&&(d?c._hideClone():c._showClone(m),m!==h&&(Yp(bf,Of?Of.options.ghostClass:c.options.ghostClass,!1),Yp(bf,l.ghostClass,!0)),Of!==m&&m!==cg.active?Of=m:m===cg.active&&Of&&(Of=null),h===m&&(m._ignoreWhileAnimating=a),m.animateAll((function(){I("dragOverAnimationComplete"),m._ignoreWhileAnimating=null})),m!==h&&(h.animateAll(),h._ignoreWhileAnimating=null)),(a===bf&&!bf.animated||a===r&&!a.animated)&&(Rf=null),l.dragoverBubble||t.rootEl||a===document||(bf.parentNode[df]._isOutsideThisEl(t.target),!e&&lg(t)),!l.dragoverBubble&&t.stopPropagation&&t.stopPropagation(),p=!0}function O(){Sf=tf(bf),Tf=tf(bf,l.draggable),vf({sortable:m,name:"change",toEl:r,newIndex:Sf,newDraggableIndex:Tf,originalEvent:t})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){Vp(document,"mousemove",this._onTouchMove),Vp(document,"touchmove",this._onTouchMove),Vp(document,"pointermove",this._onTouchMove),Vp(document,"dragover",lg),Vp(document,"mousemove",lg),Vp(document,"touchmove",lg)},_offUpEvents:function(){var t=this.el.ownerDocument;Vp(t,"mouseup",this._onDrop),Vp(t,"touchend",this._onDrop),Vp(t,"pointerup",this._onDrop),Vp(t,"touchcancel",this._onDrop),Vp(document,"selectstart",this)},_onDrop:function(t){var e=this.el,i=this.options;Sf=tf(bf),Tf=tf(bf,i.draggable),_f("drop",this,{evt:t}),yf=bf&&bf.parentNode,Sf=tf(bf),Tf=tf(bf,i.draggable),cg.eventCanceled||(Hf=!1,qf=!1,Wf=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),fg(this.cloneId),fg(this._dragStartId),this.nativeDraggable&&(Vp(document,"drop",this),Vp(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),Lp&&Xp(document.body,"user-select",""),Xp(bf,"transform",""),t&&(Vf&&(t.cancelable&&t.preventDefault(),!i.dropBubble&&t.stopPropagation()),xf&&xf.parentNode&&xf.parentNode.removeChild(xf),(wf===yf||Of&&"clone"!==Of.lastPutMode)&&$f&&$f.parentNode&&$f.parentNode.removeChild($f),bf&&(this.nativeDraggable&&Vp(bf,"dragend",this),ug(bf),bf.style["will-change"]="",Vf&&!Hf&&Yp(bf,Of?Of.options.ghostClass:this.options.ghostClass,!1),Yp(bf,this.options.chosenClass,!1),vf({sortable:this,name:"unchoose",toEl:yf,newIndex:null,newDraggableIndex:null,originalEvent:t}),wf!==yf?(Sf>=0&&(vf({rootEl:yf,name:"add",toEl:yf,fromEl:wf,originalEvent:t}),vf({sortable:this,name:"remove",toEl:yf,originalEvent:t}),vf({rootEl:yf,name:"sort",toEl:yf,fromEl:wf,originalEvent:t}),vf({sortable:this,name:"sort",toEl:yf,originalEvent:t})),Of&&Of.save()):Sf!==Af&&Sf>=0&&(vf({sortable:this,name:"update",toEl:yf,originalEvent:t}),vf({sortable:this,name:"sort",toEl:yf,originalEvent:t})),cg.active&&(null!=Sf&&-1!==Sf||(Sf=Af,Tf=If),vf({sortable:this,name:"end",toEl:yf,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){_f("nulling",this),wf=bf=yf=xf=kf=$f=Cf=Ef=Mf=Lf=Vf=Sf=Tf=Af=If=Rf=Ff=Of=zf=cg.dragged=cg.ghost=cg.clone=cg.active=null,Zf.forEach((function(t){t.checked=!0})),Zf.length=Df=jf=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":bf&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],i=this.el.children,n=0,o=i.length,r=this.options;n1&&(Pg.forEach((function(t){n.addAnimationState({target:t,rect:Rg?Gp(t):o}),cf(t),t.fromRect=o,e.removeAnimationState(t)})),Rg=!1,function(t,e){Pg.forEach((function(i,n){var o=e.children[i.sortableIndex+(t?Number(n):0)];o?e.insertBefore(i,o):e.appendChild(i)}))}(!this.options.removeCloneOnHide,i))},dragOverCompleted:function(t){var e=t.sortable,i=t.isOwner,n=t.insertion,o=t.activeSortable,r=t.parentEl,a=t.putSortable,l=this.options;if(n){if(i&&o._hideClone(),Vg=!1,l.animation&&Pg.length>1&&(Rg||!i&&!o.options.sort&&!a)){var s=Gp(Lg,!1,!0,!0);Pg.forEach((function(t){t!==Lg&&(sf(t,s),r.appendChild(t))})),Rg=!0}if(!i)if(Rg||Ug(),Pg.length>1){var c=jg;o._showClone(e),o.options.animation&&!jg&&c&&Ng.forEach((function(t){o.addAnimationState({target:t,rect:Dg}),t.fromRect=Dg,t.thisAnimationDuration=null}))}else o._showClone(e)}},dragOverAnimationCapture:function(t){var e=t.dragRect,i=t.isOwner,n=t.activeSortable;if(Pg.forEach((function(t){t.thisAnimationDuration=null})),n.options.animation&&!i&&n.multiDrag.isMultiDrag){Dg=Ep({},e);var o=Wp(Lg,!0);Dg.top-=o.f,Dg.left-=o.e}},dragOverAnimationComplete:function(){Rg&&(Rg=!1,Ug())},drop:function(t){var e=t.originalEvent,i=t.rootEl,n=t.parentEl,o=t.sortable,r=t.dispatchSortableEvent,a=t.oldIndex,l=t.putSortable,s=l||this.sortable;if(e){var c=this.options,d=n.children;if(!Fg)if(c.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),Yp(Lg,c.selectedClass,!~Pg.indexOf(Lg)),~Pg.indexOf(Lg))Pg.splice(Pg.indexOf(Lg),1),Og=null,ff({sortable:o,rootEl:i,name:"deselect",targetEl:Lg,originalEvent:e});else{if(Pg.push(Lg),ff({sortable:o,rootEl:i,name:"select",targetEl:Lg,originalEvent:e}),e.shiftKey&&Og&&o.el.contains(Og)){var u,h,m=tf(Og),p=tf(Lg);if(~m&&~p&&m!==p)for(p>m?(h=m,u=p):(h=p,u=m+1);h1){var f=Gp(Lg),g=tf(Lg,":not(."+this.options.selectedClass+")");if(!Vg&&c.animation&&(Lg.thisAnimationDuration=null),s.captureAnimationState(),!Vg&&(c.animation&&(Lg.fromRect=f,Pg.forEach((function(t){if(t.thisAnimationDuration=null,t!==Lg){var e=Rg?Gp(t):f;t.fromRect=e,s.addAnimationState({target:t,rect:e})}}))),Ug(),Pg.forEach((function(t){d[g]?n.insertBefore(t,d[g]):n.appendChild(t),g++})),a===tf(Lg))){var _=!1;Pg.forEach((function(t){t.sortableIndex===tf(t)||(_=!0)})),_&&r("update")}Pg.forEach((function(t){cf(t)})),s.animateAll()}Mg=s}(i===n||l&&"clone"!==l.lastPutMode)&&Ng.forEach((function(t){t.parentNode&&t.parentNode.removeChild(t)}))}},nullingGlobal:function(){this.isMultiDrag=Fg=!1,Ng.length=0},destroyGlobal:function(){this._deselectMultiDrag(),Vp(document,"pointerup",this._deselectMultiDrag),Vp(document,"mouseup",this._deselectMultiDrag),Vp(document,"touchend",this._deselectMultiDrag),Vp(document,"keydown",this._checkKeyDown),Vp(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==Fg&&Fg||Mg!==this.sortable||t&&Bp(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;Pg.length;){var e=Pg[0];Yp(e,this.options.selectedClass,!1),Pg.shift(),ff({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},Ep(t,{pluginName:"multiDrag",utils:{select:function(t){var e=t.parentNode[df];e&&e.options.multiDrag&&!~Pg.indexOf(t)&&(Mg&&Mg!==e&&(Mg.multiDrag._deselectMultiDrag(),Mg=e),Yp(t,e.options.selectedClass,!0),Pg.push(t))},deselect:function(t){var e=t.parentNode[df],i=Pg.indexOf(t);e&&e.options.multiDrag&&~i&&(Yp(t,e.options.selectedClass,!1),Pg.splice(i,1))}},eventProperties:function(){var t=this,e=[],i=[];return Pg.forEach((function(n){var o;e.push({multiDragElement:n,index:n.sortableIndex}),o=Rg&&n!==Lg?-1:Rg?tf(n,":not(."+t.options.selectedClass+")"):tf(n),i.push({multiDragElement:n,index:o})})),{items:Sp(Pg),clones:[].concat(Ng),oldIndicies:e,newIndicies:i}},optionListeners:{multiDragKey:function(t){return"ctrl"===(t=t.toLowerCase())?t="Control":t.length>1&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})},OnSpill:zg,Sortable:cg,Swap:function(){function t(){this.defaults={swapClass:"sortable-swap-highlight"}}return t.prototype={dragStart:function(t){var e=t.dragEl;Tg=e},dragOverValid:function(t){var e=t.completed,i=t.target,n=t.onMove,o=t.activeSortable,r=t.changed,a=t.cancel;if(o.options.swap){var l=this.sortable.el,s=this.options;if(i&&i!==l){var c=Tg;!1!==n(i)?(Yp(i,s.swapClass,!0),Tg=i):Tg=null,c&&c!==Tg&&Yp(c,s.swapClass,!1)}r(),e(!0),a()}},drop:function(t){var e=t.activeSortable,i=t.putSortable,n=t.dragEl,o=i||this.sortable,r=this.options;Tg&&Yp(Tg,r.swapClass,!1),Tg&&(r.swap||i&&i.options.swap)&&n!==Tg&&(o.captureAnimationState(),o!==e&&e.captureAnimationState(),function(t,e){var i,n,o=t.parentNode,r=e.parentNode;if(!o||!r||o.isEqualNode(e)||r.isEqualNode(t))return;i=tf(t),n=tf(e),o.isEqualNode(r)&&i=0||(n[i]=t[i]);return n}(t,e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(t,i)&&(n[i]=t[i])}return n}function Qp(t){return function(t){if(Array.isArray(t))return tf(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(!t)return;if("string"==typeof t)return tf(t,e);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return tf(t,e)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function tf(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,o=new Array(e);i"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function mf(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function pf(t,e,i,o){if(t){i=i||document;do{if(null!=e&&(">"===e[0]?t.parentNode===i&&hf(t,e):hf(t,e))||o&&t===i)return t;if(t===i)break}while(t=mf(t))}return null}var ff,gf=/\s+/g;function _f(t,e,i){if(t&&e)if(t.classList)t.classList[i?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(gf," ").replace(" "+e+" "," ");t.className=(o+(i?" "+e:"")).replace(gf," ")}}function vf(t,e,i){var o=t&&t.style;if(o){if(void 0===i)return document.defaultView&&document.defaultView.getComputedStyle?i=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(i=t.currentStyle),void 0===e?i:i[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=i+("string"==typeof i?"":"px")}}function bf(t,e){var i="";if("string"==typeof t)i=t;else do{var o=vf(t,"transform");o&&"none"!==o&&(i=o+" "+i)}while(!e&&(t=t.parentNode));var n=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return n&&new n(i)}function yf(t,e,i){if(t){var o=t.getElementsByTagName(e),n=0,r=o.length;if(i)for(;n=r:n<=r))return o;if(o===xf())break;o=Sf(o,!1)}return!1}function Cf(t,e,i,o){for(var n=0,r=0,a=t.children;r2&&void 0!==arguments[2]?arguments[2]:{},o=i.evt,n=Jp(i,Ff);Rf.pluginEvent.bind(Dg)(t,e,qp({dragEl:Hf,parentEl:Yf,ghostEl:Xf,rootEl:Wf,nextEl:qf,lastDownEl:Kf,cloneEl:Gf,cloneHidden:Zf,dragStarted:dg,putSortable:og,activeSortable:Dg.active,originalEvent:o,oldIndex:Jf,oldDraggableIndex:tg,newIndex:Qf,newDraggableIndex:eg,hideGhostForTarget:Tg,unhideGhostForTarget:zg,cloneNowHidden:function(){Zf=!0},cloneNowShown:function(){Zf=!1},dispatchSortableEvent:function(t){Uf({sortable:e,name:t,originalEvent:o})}},n))};function Uf(t){Vf(qp({putSortable:og,cloneEl:Gf,targetEl:Hf,rootEl:Wf,oldIndex:Jf,oldDraggableIndex:tg,newIndex:Qf,newDraggableIndex:eg},t))}var Hf,Yf,Xf,Wf,qf,Kf,Gf,Zf,Jf,Qf,tg,eg,ig,og,ng,rg,ag,lg,sg,cg,dg,ug,hg,mg,pg,fg=!1,gg=!1,_g=[],vg=!1,bg=!1,yg=[],xg=!1,wg=[],kg="undefined"!=typeof document,Cg=lf,$g=nf||of?"cssFloat":"float",Eg=kg&&!sf&&!lf&&"draggable"in document.createElement("div"),Ag=function(){if(kg){if(of)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),Sg=function(t,e){var i=vf(t),o=parseInt(i.width)-parseInt(i.paddingLeft)-parseInt(i.paddingRight)-parseInt(i.borderLeftWidth)-parseInt(i.borderRightWidth),n=Cf(t,0,e),r=Cf(t,1,e),a=n&&vf(n),l=r&&vf(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+wf(n).width,c=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+wf(r).width;if("flex"===i.display)return"column"===i.flexDirection||"column-reverse"===i.flexDirection?"vertical":"horizontal";if("grid"===i.display)return i.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(n&&a.float&&"none"!==a.float){var d="left"===a.float?"left":"right";return!r||"both"!==l.clear&&l.clear!==d?"horizontal":"vertical"}return n&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||s>=o&&"none"===i[$g]||r&&"none"===i[$g]&&s+c>o)?"vertical":"horizontal"},Ig=function(t){function e(t,i){return function(o,n,r,a){var l=o.options.group.name&&n.options.group.name&&o.options.group.name===n.options.group.name;if(null==t&&(i||l))return!0;if(null==t||!1===t)return!1;if(i&&"clone"===t)return t;if("function"==typeof t)return e(t(o,n,r,a),i)(o,n,r,a);var s=(i?o:n).options.group.name;return!0===t||"string"==typeof t&&t===s||t.join&&t.indexOf(s)>-1}}var i={},o=t.group;o&&"object"==Kp(o)||(o={name:o}),i.name=o.name,i.checkPull=e(o.pull,!0),i.checkPut=e(o.put),i.revertClone=o.revertClone,t.group=i},Tg=function(){!Ag&&Xf&&vf(Xf,"display","none")},zg=function(){!Ag&&Xf&&vf(Xf,"display","")};kg&&!sf&&document.addEventListener("click",(function(t){if(gg)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),gg=!1,!1}),!0);var Og=function(t){if(Hf){var e=function(t,e){var i;return _g.some((function(o){var n=o[Lf].options.emptyInsertThreshold;if(n&&!$f(o)){var r=wf(o),a=t>=r.left-n&&t<=r.right+n,l=e>=r.top-n&&e<=r.bottom+n;return a&&l?i=o:void 0}})),i}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var i={};for(var o in t)t.hasOwnProperty(o)&&(i[o]=t[o]);i.target=i.rootEl=e,i.preventDefault=void 0,i.stopPropagation=void 0,e[Lf]._onDragOver(i)}}},Mg=function(t){Hf&&Hf.parentNode[Lf]._isOutsideThisEl(t.target)};function Dg(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=Zp({},e),t[Lf]=this;var i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Sg(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Dg.supportPointer&&"PointerEvent"in window&&!af,emptyInsertThreshold:5};for(var o in Rf.initializePlugins(this,t,i),i)!(o in e)&&(e[o]=i[o]);for(var n in Ig(e),this)"_"===n.charAt(0)&&"function"==typeof this[n]&&(this[n]=this[n].bind(this));this.nativeDraggable=!e.forceFallback&&Eg,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?df(t,"pointerdown",this._onTapStart):(df(t,"mousedown",this._onTapStart),df(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(df(t,"dragover",this),df(t,"dragenter",this)),_g.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),Zp(this,jf())}function Lg(t,e,i,o,n,r,a,l){var s,c,d=t[Lf],u=d.options.onMove;return!window.CustomEvent||of||nf?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=i,s.draggedRect=o,s.related=n||e,s.relatedRect=r||wf(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),u&&(c=u.call(d,s,a)),c}function jg(t){t.draggable=!1}function Pg(){xg=!1}function Ng(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,i=e.length,o=0;i--;)o+=e.charCodeAt(i);return o.toString(36)}function Rg(t){return setTimeout(t,0)}function Vg(t){return clearTimeout(t)}Dg.prototype={constructor:Dg,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ug=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Hf):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,i=this.el,o=this.options,n=o.preventOnFilter,r=t.type,a=t.touches&&t.touches[0]||t.pointerType&&"touch"===t.pointerType&&t,l=(a||t).target,s=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||l,c=o.filter;if(function(t){wg.length=0;var e=t.getElementsByTagName("input"),i=e.length;for(;i--;){var o=e[i];o.checked&&wg.push(o)}}(i),!Hf&&!(/mousedown|pointerdown/.test(r)&&0!==t.button||o.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!af||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=pf(l,o.draggable,i,!1))&&l.animated||Kf===l)){if(Jf=Ef(l),tg=Ef(l,o.draggable),"function"==typeof c){if(c.call(this,t,l,this))return Uf({sortable:e,rootEl:s,name:"filter",targetEl:l,toEl:i,fromEl:i}),Bf("filter",e,{evt:t}),void(n&&t.cancelable&&t.preventDefault())}else if(c&&(c=c.split(",").some((function(o){if(o=pf(s,o.trim(),i,!1))return Uf({sortable:e,rootEl:o,name:"filter",targetEl:l,fromEl:i,toEl:i}),Bf("filter",e,{evt:t}),!0}))))return void(n&&t.cancelable&&t.preventDefault());o.handle&&!pf(s,o.handle,i,!1)||this._prepareDragStart(t,a,l)}}},_prepareDragStart:function(t,e,i){var o,n=this,r=n.el,a=n.options,l=r.ownerDocument;if(i&&!Hf&&i.parentNode===r){var s=wf(i);if(Wf=r,Yf=(Hf=i).parentNode,qf=Hf.nextSibling,Kf=i,ig=a.group,Dg.dragged=Hf,ng={target:Hf,clientX:(e||t).clientX,clientY:(e||t).clientY},sg=ng.clientX-s.left,cg=ng.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Hf.style["will-change"]="all",o=function(){Bf("delayEnded",n,{evt:t}),Dg.eventCanceled?n._onDrop():(n._disableDelayedDragEvents(),!rf&&n.nativeDraggable&&(Hf.draggable=!0),n._triggerDragStart(t,e),Uf({sortable:n,name:"choose",originalEvent:t}),_f(Hf,a.chosenClass,!0))},a.ignore.split(",").forEach((function(t){yf(Hf,t.trim(),jg)})),df(l,"dragover",Og),df(l,"mousemove",Og),df(l,"touchmove",Og),df(l,"mouseup",n._onDrop),df(l,"touchend",n._onDrop),df(l,"touchcancel",n._onDrop),rf&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Hf.draggable=!0),Bf("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(nf||of))o();else{if(Dg.eventCanceled)return void this._onDrop();df(l,"mouseup",n._disableDelayedDrag),df(l,"touchend",n._disableDelayedDrag),df(l,"touchcancel",n._disableDelayedDrag),df(l,"mousemove",n._delayedDragTouchMoveHandler),df(l,"touchmove",n._delayedDragTouchMoveHandler),a.supportPointer&&df(l,"pointermove",n._delayedDragTouchMoveHandler),n._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Hf&&jg(Hf),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;uf(t,"mouseup",this._disableDelayedDrag),uf(t,"touchend",this._disableDelayedDrag),uf(t,"touchcancel",this._disableDelayedDrag),uf(t,"mousemove",this._delayedDragTouchMoveHandler),uf(t,"touchmove",this._delayedDragTouchMoveHandler),uf(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?df(document,"pointermove",this._onTouchMove):df(document,e?"touchmove":"mousemove",this._onTouchMove):(df(Hf,"dragend",this),df(Wf,"dragstart",this._onDragStart));try{document.selection?Rg((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(fg=!1,Wf&&Hf){Bf("dragStarted",this,{evt:e}),this.nativeDraggable&&df(document,"dragover",Mg);var i=this.options;!t&&_f(Hf,i.dragClass,!1),_f(Hf,i.ghostClass,!0),Dg.active=this,t&&this._appendGhost(),Uf({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(rg){this._lastX=rg.clientX,this._lastY=rg.clientY,Tg();for(var t=document.elementFromPoint(rg.clientX,rg.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(rg.clientX,rg.clientY))!==e;)e=t;if(Hf.parentNode[Lf]._isOutsideThisEl(t),e)do{if(e[Lf]){if(e[Lf]._onDragOver({clientX:rg.clientX,clientY:rg.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);zg()}},_onTouchMove:function(t){if(ng){var e=this.options,i=e.fallbackTolerance,o=e.fallbackOffset,n=t.touches?t.touches[0]:t,r=Xf&&bf(Xf,!0),a=Xf&&r&&r.a,l=Xf&&r&&r.d,s=Cg&&pg&&Af(pg),c=(n.clientX-ng.clientX+o.x)/(a||1)+(s?s[0]-yg[0]:0)/(a||1),d=(n.clientY-ng.clientY+o.y)/(l||1)+(s?s[1]-yg[1]:0)/(l||1);if(!Dg.active&&!fg){if(i&&Math.max(Math.abs(n.clientX-this._lastX),Math.abs(n.clientY-this._lastY))o.right+n||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+n}(t,n,this)&&!f.animated){if(f===Hf)return z(!1);if(f&&r===t.target&&(a=f),a&&(i=wf(a)),!1!==Lg(Wf,r,Hf,e,a,i,t,!!a))return T(),f&&f.nextSibling?r.insertBefore(Hf,f.nextSibling):r.appendChild(Hf),Yf=r,O(),z(!0)}else if(f&&function(t,e,i){var o=wf(Cf(i.el,0,i.options,!0)),n=10;return e?t.clientXd+c*r/2:su-mg)return-hg}else if(s>d+c*(1-n)/2&&su-c*r/2))return s>d+c/2?1:-1;return 0}(t,a,i,n,x?1:l.swapThreshold,null==l.invertedSwapThreshold?l.swapThreshold:l.invertedSwapThreshold,bg,ug===a),0!==_){var $=Ef(Hf);do{$-=_,b=Yf.children[$]}while(b&&("none"===vf(b,"display")||b===Xf))}if(0===_||b===a)return z(!1);ug=a,hg=_;var E=a.nextElementSibling,A=!1,S=Lg(Wf,r,Hf,e,a,i,t,A=1===_);if(!1!==S)return 1!==S&&-1!==S||(A=1===S),xg=!0,setTimeout(Pg,30),T(),A&&!E?r.appendChild(Hf):a.parentNode.insertBefore(Hf,A?E:a),k&&zf(k,0,C-k.scrollTop),Yf=Hf.parentNode,void 0===v||bg||(mg=Math.abs(v-wf(a)[w])),O(),z(!0)}if(r.contains(Hf))return z(!1)}return!1}function I(l,s){Bf(l,m,qp({evt:t,isOwner:d,axis:n?"vertical":"horizontal",revert:o,dragRect:e,targetRect:i,canSort:u,fromSortable:h,target:a,completed:z,onMove:function(i,o){return Lg(Wf,r,Hf,e,i,wf(i),t,o)},changed:O},s))}function T(){I("dragOverAnimationCapture"),m.captureAnimationState(),m!==h&&h.captureAnimationState()}function z(e){return I("dragOverCompleted",{insertion:e}),e&&(d?c._hideClone():c._showClone(m),m!==h&&(_f(Hf,og?og.options.ghostClass:c.options.ghostClass,!1),_f(Hf,l.ghostClass,!0)),og!==m&&m!==Dg.active?og=m:m===Dg.active&&og&&(og=null),h===m&&(m._ignoreWhileAnimating=a),m.animateAll((function(){I("dragOverAnimationComplete"),m._ignoreWhileAnimating=null})),m!==h&&(h.animateAll(),h._ignoreWhileAnimating=null)),(a===Hf&&!Hf.animated||a===r&&!a.animated)&&(ug=null),l.dragoverBubble||t.rootEl||a===document||(Hf.parentNode[Lf]._isOutsideThisEl(t.target),!e&&Og(t)),!l.dragoverBubble&&t.stopPropagation&&t.stopPropagation(),p=!0}function O(){Qf=Ef(Hf),eg=Ef(Hf,l.draggable),Uf({sortable:m,name:"change",toEl:r,newIndex:Qf,newDraggableIndex:eg,originalEvent:t})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){uf(document,"mousemove",this._onTouchMove),uf(document,"touchmove",this._onTouchMove),uf(document,"pointermove",this._onTouchMove),uf(document,"dragover",Og),uf(document,"mousemove",Og),uf(document,"touchmove",Og)},_offUpEvents:function(){var t=this.el.ownerDocument;uf(t,"mouseup",this._onDrop),uf(t,"touchend",this._onDrop),uf(t,"pointerup",this._onDrop),uf(t,"touchcancel",this._onDrop),uf(document,"selectstart",this)},_onDrop:function(t){var e=this.el,i=this.options;Qf=Ef(Hf),eg=Ef(Hf,i.draggable),Bf("drop",this,{evt:t}),Yf=Hf&&Hf.parentNode,Qf=Ef(Hf),eg=Ef(Hf,i.draggable),Dg.eventCanceled||(fg=!1,bg=!1,vg=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Vg(this.cloneId),Vg(this._dragStartId),this.nativeDraggable&&(uf(document,"drop",this),uf(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),af&&vf(document.body,"user-select",""),vf(Hf,"transform",""),t&&(dg&&(t.cancelable&&t.preventDefault(),!i.dropBubble&&t.stopPropagation()),Xf&&Xf.parentNode&&Xf.parentNode.removeChild(Xf),(Wf===Yf||og&&"clone"!==og.lastPutMode)&&Gf&&Gf.parentNode&&Gf.parentNode.removeChild(Gf),Hf&&(this.nativeDraggable&&uf(Hf,"dragend",this),jg(Hf),Hf.style["will-change"]="",dg&&!fg&&_f(Hf,og?og.options.ghostClass:this.options.ghostClass,!1),_f(Hf,this.options.chosenClass,!1),Uf({sortable:this,name:"unchoose",toEl:Yf,newIndex:null,newDraggableIndex:null,originalEvent:t}),Wf!==Yf?(Qf>=0&&(Uf({rootEl:Yf,name:"add",toEl:Yf,fromEl:Wf,originalEvent:t}),Uf({sortable:this,name:"remove",toEl:Yf,originalEvent:t}),Uf({rootEl:Yf,name:"sort",toEl:Yf,fromEl:Wf,originalEvent:t}),Uf({sortable:this,name:"sort",toEl:Yf,originalEvent:t})),og&&og.save()):Qf!==Jf&&Qf>=0&&(Uf({sortable:this,name:"update",toEl:Yf,originalEvent:t}),Uf({sortable:this,name:"sort",toEl:Yf,originalEvent:t})),Dg.active&&(null!=Qf&&-1!==Qf||(Qf=Jf,eg=tg),Uf({sortable:this,name:"end",toEl:Yf,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){Bf("nulling",this),Wf=Hf=Yf=Xf=qf=Gf=Kf=Zf=ng=rg=dg=Qf=eg=Jf=tg=ug=hg=og=ig=Dg.dragged=Dg.ghost=Dg.clone=Dg.active=null,wg.forEach((function(t){t.checked=!0})),wg.length=ag=lg=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":Hf&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],i=this.el.children,o=0,n=i.length,r=this.options;o1&&(s_.forEach((function(t){o.addAnimationState({target:t,rect:u_?wf(t):n}),Df(t),t.fromRect=n,e.removeAnimationState(t)})),u_=!1,function(t,e){s_.forEach((function(i,o){var n=e.children[i.sortableIndex+(t?Number(o):0)];n?e.insertBefore(i,n):e.appendChild(i)}))}(!this.options.removeCloneOnHide,i))},dragOverCompleted:function(t){var e=t.sortable,i=t.isOwner,o=t.insertion,n=t.activeSortable,r=t.parentEl,a=t.putSortable,l=this.options;if(o){if(i&&n._hideClone(),d_=!1,l.animation&&s_.length>1&&(u_||!i&&!n.options.sort&&!a)){var s=wf(r_,!1,!0,!0);s_.forEach((function(t){t!==r_&&(Mf(t,s),r.appendChild(t))})),u_=!0}if(!i)if(u_||p_(),s_.length>1){var c=l_;n._showClone(e),n.options.animation&&!l_&&c&&c_.forEach((function(t){n.addAnimationState({target:t,rect:a_}),t.fromRect=a_,t.thisAnimationDuration=null}))}else n._showClone(e)}},dragOverAnimationCapture:function(t){var e=t.dragRect,i=t.isOwner,o=t.activeSortable;if(s_.forEach((function(t){t.thisAnimationDuration=null})),o.options.animation&&!i&&o.multiDrag.isMultiDrag){a_=Zp({},e);var n=bf(r_,!0);a_.top-=n.f,a_.left-=n.e}},dragOverAnimationComplete:function(){u_&&(u_=!1,p_())},drop:function(t){var e=t.originalEvent,i=t.rootEl,o=t.parentEl,n=t.sortable,r=t.dispatchSortableEvent,a=t.oldIndex,l=t.putSortable,s=l||this.sortable;if(e){var c=this.options,d=o.children;if(!h_)if(c.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),_f(r_,c.selectedClass,!~s_.indexOf(r_)),~s_.indexOf(r_))s_.splice(s_.indexOf(r_),1),o_=null,Vf({sortable:n,rootEl:i,name:"deselect",targetEl:r_,originalEvent:e});else{if(s_.push(r_),Vf({sortable:n,rootEl:i,name:"select",targetEl:r_,originalEvent:e}),e.shiftKey&&o_&&n.el.contains(o_)){var u,h,m=Ef(o_),p=Ef(r_);if(~m&&~p&&m!==p)for(p>m?(h=m,u=p):(h=p,u=m+1);h1){var f=wf(r_),g=Ef(r_,":not(."+this.options.selectedClass+")");if(!d_&&c.animation&&(r_.thisAnimationDuration=null),s.captureAnimationState(),!d_&&(c.animation&&(r_.fromRect=f,s_.forEach((function(t){if(t.thisAnimationDuration=null,t!==r_){var e=u_?wf(t):f;t.fromRect=e,s.addAnimationState({target:t,rect:e})}}))),p_(),s_.forEach((function(t){d[g]?o.insertBefore(t,d[g]):o.appendChild(t),g++})),a===Ef(r_))){var _=!1;s_.forEach((function(t){t.sortableIndex===Ef(t)||(_=!0)})),_&&r("update")}s_.forEach((function(t){Df(t)})),s.animateAll()}n_=s}(i===o||l&&"clone"!==l.lastPutMode)&&c_.forEach((function(t){t.parentNode&&t.parentNode.removeChild(t)}))}},nullingGlobal:function(){this.isMultiDrag=h_=!1,c_.length=0},destroyGlobal:function(){this._deselectMultiDrag(),uf(document,"pointerup",this._deselectMultiDrag),uf(document,"mouseup",this._deselectMultiDrag),uf(document,"touchend",this._deselectMultiDrag),uf(document,"keydown",this._checkKeyDown),uf(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==h_&&h_||n_!==this.sortable||t&&pf(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;s_.length;){var e=s_[0];_f(e,this.options.selectedClass,!1),s_.shift(),Vf({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},Zp(t,{pluginName:"multiDrag",utils:{select:function(t){var e=t.parentNode[Lf];e&&e.options.multiDrag&&!~s_.indexOf(t)&&(n_&&n_!==e&&(n_.multiDrag._deselectMultiDrag(),n_=e),_f(t,e.options.selectedClass,!0),s_.push(t))},deselect:function(t){var e=t.parentNode[Lf],i=s_.indexOf(t);e&&e.options.multiDrag&&~i&&(_f(t,e.options.selectedClass,!1),s_.splice(i,1))}},eventProperties:function(){var t=this,e=[],i=[];return s_.forEach((function(o){var n;e.push({multiDragElement:o,index:o.sortableIndex}),n=u_&&o!==r_?-1:u_?Ef(o,":not(."+t.options.selectedClass+")"):Ef(o),i.push({multiDragElement:o,index:n})})),{items:Qp(s_),clones:[].concat(c_),oldIndicies:e,newIndicies:i}},optionListeners:{multiDragKey:function(t){return"ctrl"===(t=t.toLowerCase())?t="Control":t.length>1&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})},OnSpill:i_,Sortable:Dg,Swap:function(){function t(){this.defaults={swapClass:"sortable-swap-highlight"}}return t.prototype={dragStart:function(t){var e=t.dragEl;e_=e},dragOverValid:function(t){var e=t.completed,i=t.target,o=t.onMove,n=t.activeSortable,r=t.changed,a=t.cancel;if(n.options.swap){var l=this.sortable.el,s=this.options;if(i&&i!==l){var c=e_;!1!==o(i)?(_f(i,s.swapClass,!0),e_=i):e_=null,c&&c!==e_&&_f(c,s.swapClass,!1)}r(),e(!0),a()}},drop:function(t){var e=t.activeSortable,i=t.putSortable,o=t.dragEl,n=i||this.sortable,r=this.options;e_&&_f(e_,r.swapClass,!1),e_&&(r.swap||i&&i.options.swap)&&o!==e_&&(n.captureAnimationState(),n!==e&&e.captureAnimationState(),function(t,e){var i,o,n=t.parentNode,r=e.parentNode;if(!n||!r||n.isEqualNode(e)||r.isEqualNode(t))return;i=Ef(t),o=Ef(e),n.isEqualNode(r)&&i Date: Tue, 11 Apr 2023 23:16:35 +0200 Subject: [PATCH 063/158] Unique_ids for thermal comfort sensors --- sensors.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sensors.yaml b/sensors.yaml index 5614554b..d0580693 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -139,37 +139,46 @@ sensors: living_room: friendly_name: Living Room + unique_id: living_room_thermal_comfort temperature_sensor: sensor.living_room_temperature humidity_sensor: sensor.living_room_humidity dining_nook: friendly_name: Dining Nook + unique_id: dining_nook_thermal_comfort temperature_sensor: sensor.nook_temperature humidity_sensor: sensor.nook_humidity hall: friendly_name: Hall + unique_id: hall_thermal_comfort temperature_sensor: sensor.hall_temperature humidity_sensor: sensor.hall_humidity bathroom: friendly_name: Bathroom + unique_id: bathroom_thermal_comfort temperature_sensor: sensor.bathroom_temperature humidity_sensor: sensor.bathroom_humidity front_hall: friendly_name: Front Hall + unique_id: front_hall_thermal_comfort temperature_sensor: sensor.front_hall_sensor_temperature humidity_sensor: sensor.front_hall_sensor_humidity craft_room: friendly_name: Craft Room + unique_id: craft_room_thermal_comfort temperature_sensor: sensor.craft_room_temperature humidity_sensor: sensor.craft_room_humidity study: friendly_name: Study + unique_id: study_thermal_comfort temperature_sensor: sensor.study_temperature humidity_sensor: sensor.study_humidity guest_room: friendly_name: Guest Room + unique_id: guest_room_thermal_comfort temperature_sensor: sensor.guest_room_temperature humidity_sensor: sensor.guest_room_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 From 1e10ecfc9111e75a1c9636dde1122efacd88de1b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 24 Apr 2023 15:42:32 +0100 Subject: [PATCH 064/158] Include missing sensors --- packages/outside_motion.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/outside_motion.yaml b/packages/outside_motion.yaml index b499904e..310bf04b 100644 --- a/packages/outside_motion.yaml +++ b/packages/outside_motion.yaml @@ -6,11 +6,12 @@ automation: entity_id: - binary_sensor.outside_front_motion - binary_sensor.outside_driveway_motion + - binary_sensor.back_door_motion - binary_sensor.front_door_motion - binary_sensor.back_door_person_occupancy - binary_sensor.driveway_person_occupancy - - binary_sensor.driveway_person_occupancy - binary_sensor.gates_person_occupancy + - binary_sensor.back_door_all_occupancy to: 'on' condition: - condition: state From 54224c6fe55721b21cb1e830935408b4e04521f9 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 24 Apr 2023 21:35:16 +0100 Subject: [PATCH 065/158] Fix notification --- automation/aurora_alert.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/aurora_alert.yaml b/automation/aurora_alert.yaml index b5226dc8..c94e3449 100644 --- a/automation/aurora_alert.yaml +++ b/automation/aurora_alert.yaml @@ -15,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 data: title: "Aurora Alert" message: "Alert! The Aurora Borealis might be visible right now!" From b9688c695c4a2962807b6f84fa8d01658b131285 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 8 May 2023 21:42:58 +0100 Subject: [PATCH 066/158] No white channels whilst https://github.com/home-assistant/core/issues/89846 exists --- esphome/common/h801_common.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/common/h801_common.yaml b/esphome/common/h801_common.yaml index 7b0d3494..a4cf2573 100644 --- a/esphome/common/h801_common.yaml +++ b/esphome/common/h801_common.yaml @@ -45,13 +45,13 @@ output: # RGB, RGBW, RGBWW also available light: - - platform: rgbww + - platform: rgb name: ${friendly_name} red: pwm_r green: pwm_g blue: pwm_b - cold_white: pwm_w1 - warm_white: pwm_w2 + #cold_white: pwm_w1 + #warm_white: pwm_w2 id: thelight effects: - random: From ea8d05e23cff6e47c436260ab06650124bdaf1a4 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 20 May 2023 18:49:45 +0100 Subject: [PATCH 067/158] Sankey chart --- www/ha-sankey-chart.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 www/ha-sankey-chart.js diff --git a/www/ha-sankey-chart.js b/www/ha-sankey-chart.js new file mode 100644 index 00000000..ee3c4d17 --- /dev/null +++ b/www/ha-sankey-chart.js @@ -0,0 +1 @@ +function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,n);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(r=(o<3?s(r):o>3?s(e,i,r):s(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r}const e=window,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,n=Symbol(),s=new WeakMap;class o{constructor(t,e,i){if(this._$cssResult$=!0,i!==n)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(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}}const r=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1]),t[0]);return new o(i,t,n)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new o("string"==typeof t?t:t+"",void 0,n))(e)})(t):t;var c;const l=window,h=l.trustedTypes,d=h?h.emptyScript:"",u=l.reactiveElementPolyfillSupport,p={toAttribute(t,e){switch(e){case Boolean:t=t?d: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}},g=(t,e)=>e!==t&&(e==e||t==t),f={attribute:!0,type:String,converter:p,reflect:!1,hasChanged:g};class m extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(t){var e;this.finalize(),(null!==(e=this.h)&&void 0!==e?e:this.h=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const n=this._$Ep(i,e);void 0!==n&&(this._$Ev.set(n,i),t.push(n))})),t}static createProperty(t,e=f){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const s=this[t];this[e]=n,this.requestUpdate(t,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||f}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Ep(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._$E_=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$ES)||void 0===e||e.splice(this._$ES.indexOf(t)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Ei.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const n=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,n)=>{i?t.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((i=>{const n=document.createElement("style"),s=e.litNonce;void 0!==s&&n.setAttribute("nonce",s),n.textContent=i.cssText,t.appendChild(n)}))})(n,this.constructor.elementStyles),n}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$EO(t,e,i=f){var n;const s=this.constructor._$Ep(t,i);if(void 0!==s&&!0===i.reflect){const o=(void 0!==(null===(n=i.converter)||void 0===n?void 0:n.toAttribute)?i.converter:p).toAttribute(e,i.type);this._$El=t,null==o?this.removeAttribute(s):this.setAttribute(s,o),this._$El=null}}_$AK(t,e){var i;const n=this.constructor,s=n._$Ev.get(t);if(void 0!==s&&this._$El!==s){const t=n.getPropertyOptions(s),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(i=t.converter)||void 0===i?void 0:i.fromAttribute)?t.converter:p;this._$El=s,this[s]=o.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||g)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((t,e)=>this[e]=t)),this._$Ei=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$ES)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$EO(e,this[e],t))),this._$EC=void 0),this._$Ek()}updated(t){}firstUpdated(t){}}var _;m.finalized=!0,m.elementProperties=new Map,m.elementStyles=[],m.shadowRootOptions={mode:"open"},null==u||u({ReactiveElement:m}),(null!==(c=l.reactiveElementVersions)&&void 0!==c?c:l.reactiveElementVersions=[]).push("1.4.2");const y=window,v=y.trustedTypes,b=v?v.createPolicy("lit-html",{createHTML:t=>t}):void 0,$=`lit$${(Math.random()+"").slice(9)}$`,w="?"+$,x=`<${w}>`,C=document,A=(t="")=>C.createComment(t),S=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,k=t=>E(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),O=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,j=/-->/g,P=/>/g,M=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),U=/'/g,z=/"/g,N=/^(?:script|style|textarea|title)$/i,T=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),H=T(1),B=T(2),I=Symbol.for("lit-noChange"),D=Symbol.for("lit-nothing"),R=new WeakMap,Y=C.createTreeWalker(C,129,null,!1),L=(t,e)=>{const i=t.length-1,n=[];let s,o=2===e?"":"",r=O;for(let e=0;e"===c[0]?(r=null!=s?s:O,l=-1):void 0===c[1]?l=-2:(l=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?M:'"'===c[3]?z:U):r===z||r===U?r=M:r===j||r===P?r=O:(r=M,s=void 0);const d=r===M&&t[e+1].startsWith("/>")?" ":"";o+=r===O?i+x:l>=0?(n.push(a),i.slice(0,l)+"$lit$"+i.slice(l)+$+d):i+$+(-2===l?(n.push(void 0),e):d)}const a=o+(t[i]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==b?b.createHTML(a):a,n]};class V{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let s=0,o=0;const r=t.length-1,a=this.parts,[c,l]=L(t,e);if(this.el=V.createElement(c,i),Y.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(n=Y.nextNode())&&a.length0){n.textContent=v?v.emptyScript:"";for(let i=0;i2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=D}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,n){const s=this.strings;let o=!1;if(void 0===s)t=q(this,t,e,0),o=!S(t)||t!==this._$AH&&t!==I,o&&(this._$AH=t);else{const n=t;let r,a;for(t=s[0],r=0;r{var n,s;const o=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:e;let r=o._$litPart$;if(void 0===r){const t=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:null;o._$litPart$=r=new W(e.insertBefore(A(),t),t,void 0,null!=i?i:{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!1)}render(){return I}}st.finalized=!0,st._$litElement$=!0,null===(it=globalThis.litElementHydrateSupport)||void 0===it||it.call(globalThis,{LitElement:st});const ot=globalThis.litElementPolyfillSupport;null==ot||ot({LitElement:st}),(null!==(nt=globalThis.litElementVersions)&&void 0!==nt?nt:globalThis.litElementVersions=[]).push("3.2.2");const rt=t=>e=>"function"==typeof e?((t,e)=>(customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:n}=e;return{kind:i,elements:n,finisher(e){customElements.define(t,e)}}})(t,e),at=(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 ct(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):at(t,e)}function lt(t){return ct({...t,state:!0})}var ht;null===(ht=window.HTMLSlotElement)||void 0===ht||ht.prototype.assignedElements;var dt={version:"Version",invalid_configuration:"Invalid configuration",show_warning:"Show Warning",entity_not_found:"Entity state not found",missing_child:"Missing child entity",loading:"Loading..."},ut={yaml_disclaimer:"Please use yaml mode for the other options",docs:"Documentation",autoconfig:"Autoconfig",enable:"Enable",print:"Print auto generated config yaml",sections:"Sections",section:"Section",add_section:"+ Add section",add_entity:"+ Add entity",entity_editor:"Entity editor",fields:{entity:"Entity",type:"Type",children:"Children",name:"Name",icon:"Icon",color:"Color",unit_of_measurement:"Unit of measurement",tap_action:"Tap action",color_on_state:"Change color based on state",color_limit:"State limit for color change",color_above:"Color above limit",color_below:"Color below limit"},entity_types:{entity:"Entity",remaining_parent_state:"Remaining parent state",remaining_child_state:"Remaining child state",passthrough:"Passthrough"}},pt={common:dt,editor:ut};const gt={en:Object.freeze({__proto__:null,common:dt,editor:ut,default:pt})};function ft(t,e="",i=""){const n=(localStorage.getItem("selectedLanguage")||"en").replace(/['"]+/g,"").replace("-","_");let s;try{s=t.split(".").reduce(((t,e)=>t[e]),gt[n])}catch(e){s=t.split(".").reduce(((t,e)=>t[e]),gt.en)}return void 0===s&&(s=t.split(".").reduce(((t,e)=>t&&t[e]),gt.en)),""!==e&&""!==i&&(s=s.replace(e,i)),s}var mt,_t;function yt(t){return t.substr(0,t.indexOf("."))}!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"}(mt||(mt={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(_t||(_t={}));var vt=["closed","locked","off"],bt=function(t,e,i,n){n=n||{},i=null==i?{}:i;var s=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return s.detail=i,t.dispatchEvent(s),s},$t=new Set(["call-service","divider","section","weblink","cast","select"]),wt={alert:"toggle",automation:"toggle",climate:"climate",cover:"cover",fan:"toggle",group:"group",input_boolean:"toggle",input_number:"input-number",input_select:"input-select",input_text:"input-text",light:"toggle",lock:"lock",media_player:"media-player",remote:"toggle",scene:"scene",script:"script",sensor:"sensor",timer:"timer",switch:"toggle",vacuum:"toggle",water_heater:"climate",input_datetime:"input-datetime"},xt={alert:"mdi:alert",automation:"mdi:playlist-play",calendar:"mdi:calendar",camera:"mdi:video",climate:"mdi:thermostat",configurator:"mdi:settings",conversation:"mdi:text-to-speech",device_tracker:"mdi:account",fan:"mdi:fan",group:"mdi:google-circles-communities",history_graph:"mdi:chart-line",homeassistant:"mdi:home-assistant",homekit:"mdi:home-automation",image_processing:"mdi:image-filter-frames",input_boolean:"mdi:drawing",input_datetime:"mdi:calendar-clock",input_number:"mdi:ray-vertex",input_select:"mdi:format-list-bulleted",input_text:"mdi:textbox",light:"mdi:lightbulb",mailbox:"mdi:mailbox",notify:"mdi:comment-alert",person:"mdi:account",plant:"mdi:flower",proximity:"mdi:apple-safari",remote:"mdi:remote",scene:"mdi:google-pages",script:"mdi:file-document",sensor:"mdi:eye",simple_alarm:"mdi:bell",sun:"mdi:white-balance-sunny",switch:"mdi:flash",timer:"mdi:timer",updater:"mdi:cloud-upload",vacuum:"mdi:robot-vacuum",water_heater:"mdi:thermometer",weblink:"mdi:open-in-new"};function Ct(t,e){if(t in xt)return xt[t];switch(t){case"alarm_control_panel":switch(e){case"armed_home":return"mdi:bell-plus";case"armed_night":return"mdi:bell-sleep";case"disarmed":return"mdi:bell-outline";case"triggered":return"mdi:bell-ring";default:return"mdi:bell"}case"binary_sensor":return e&&"off"===e?"mdi:radiobox-blank":"mdi:checkbox-marked-circle";case"cover":return"closed"===e?"mdi:window-closed":"mdi:window-open";case"lock":return e&&"unlocked"===e?"mdi:lock-open":"mdi:lock";case"media_player":return e&&"off"!==e&&"idle"!==e?"mdi:cast-connected":"mdi:cast";case"zwave":switch(e){case"dead":return"mdi:emoticon-dead";case"sleeping":return"mdi:sleep";case"initializing":return"mdi:timer-sand";default:return"mdi:z-wave"}default:return console.warn("Unable to find icon for domain "+t+" ("+e+")"),"mdi:bookmark"}}var At=function(t){bt(window,"haptic",t)},St=function(t,e){return function(t,e,i){void 0===i&&(i=!0);var n,s=yt(e),o="group"===s?"homeassistant":s;switch(s){case"lock":n=i?"unlock":"lock";break;case"cover":n=i?"open_cover":"close_cover";break;default:n=i?"turn_on":"turn_off"}return t.callService(o,n,{entity_id:e})}(t,e,vt.includes(t.states[e].state))},Et={humidity:"mdi:water-percent",illuminance:"mdi:brightness-5",temperature:"mdi:thermometer",pressure:"mdi:gauge",power:"mdi:flash",signal_strength:"mdi:wifi"},kt={binary_sensor:function(t,e){var i="off"===t;switch(null==e?void 0:e.attributes.device_class){case"battery":return i?"mdi:battery":"mdi:battery-outline";case"battery_charging":return i?"mdi:battery":"mdi:battery-charging";case"cold":return i?"mdi:thermometer":"mdi:snowflake";case"connectivity":return i?"mdi:server-network-off":"mdi:server-network";case"door":return i?"mdi:door-closed":"mdi:door-open";case"garage_door":return i?"mdi:garage":"mdi:garage-open";case"power":case"plug":return i?"mdi:power-plug-off":"mdi:power-plug";case"gas":case"problem":case"safety":case"tamper":return i?"mdi:check-circle":"mdi:alert-circle";case"smoke":return i?"mdi:check-circle":"mdi:smoke";case"heat":return i?"mdi:thermometer":"mdi:fire";case"light":return i?"mdi:brightness-5":"mdi:brightness-7";case"lock":return i?"mdi:lock":"mdi:lock-open";case"moisture":return i?"mdi:water-off":"mdi:water";case"motion":return i?"mdi:walk":"mdi:run";case"occupancy":case"presence":return i?"mdi:home-outline":"mdi:home";case"opening":return i?"mdi:square":"mdi:square-outline";case"running":return i?"mdi:stop":"mdi:play";case"sound":return i?"mdi:music-note-off":"mdi:music-note";case"update":return i?"mdi:package":"mdi:package-up";case"vibration":return i?"mdi:crop-portrait":"mdi:vibrate";case"window":return i?"mdi:window-closed":"mdi:window-open";default:return i?"mdi:radiobox-blank":"mdi:checkbox-marked-circle"}},cover:function(t){var e="closed"!==t.state;switch(t.attributes.device_class){case"garage":return e?"mdi:garage-open":"mdi:garage";case"door":return e?"mdi:door-open":"mdi:door-closed";case"shutter":return e?"mdi:window-shutter-open":"mdi:window-shutter";case"blind":return e?"mdi:blinds-open":"mdi:blinds";case"window":return e?"mdi:window-open":"mdi:window-closed";default:return Ct("cover",t.state)}},sensor:function(t){var e=t.attributes.device_class;if(e&&e in Et)return Et[e];if("battery"===e){var i=Number(t.state);if(isNaN(i))return"mdi:battery-unknown";var n=10*Math.round(i/10);return n>=100?"mdi:battery":n<=0?"mdi:battery-alert":"hass:battery-"+n}var s=t.attributes.unit_of_measurement;return"°C"===s||"°F"===s?"mdi:thermometer":Ct("sensor")},input_datetime:function(t){return t.attributes.has_date?t.attributes.has_time?Ct("input_datetime"):"mdi:calendar":"mdi:clock"}},Ot=function(t){if(!t)return"mdi:bookmark";if(t.attributes.icon)return t.attributes.icon;var e=yt(t.entity_id);return e in kt?kt[e](t):Ct(e,t.state)};const jt={m:.001,k:1e3,M:1e6,G:1e9,T:1e12},Pt={type:"entity"};function Mt(t,e,i){const n=Math.max(0,e);if(!i)return{state:n,unit_of_measurement:i};const s=Object.keys(jt).find((t=>0===i.indexOf(t)))||"",o=jt[s]||1,r=jt[t]||1;return o===r?{state:n,unit_of_measurement:i}:{state:n*o/r,unit_of_measurement:s?i.replace(s,t):t+i}}function Ut(t){return"string"==typeof t?t:t.entity_id}function zt(t){let e=Object.assign({sections:[]},(i=t,JSON.parse(JSON.stringify(i))));var i;const{autoconfig:n}=t;(n||"object"==typeof n)&&(e=Object.assign(Object.assign({energy_date_selection:!0,unit_prefix:"k",round:1},e),{sections:[]}));const s=e.sections.map((t=>Object.assign(Object.assign({},t),{entities:t.entities.map((t=>"string"==typeof t?Object.assign(Object.assign({},Pt),{children:[],entity_id:t}):Object.assign(Object.assign(Object.assign({},Pt),{children:[]}),t)))})));return s.forEach(((t,e)=>{t.entities.forEach((t=>{if(t.children&&t.children.length&&t.children.forEach((t=>{for(let i=e+1;iUt(e)===t));if(n){if(i>e+1)for(let o=e+1;ot!==o[n].entity_id)),n++;const r="remaining"+Date.now()+Math.random(),a="string"==typeof t.remaining?{name:t.remaining}:t.remaining;t.children=[...t.children,r],s[e+1].entities=[...o.slice(0,n),Object.assign(Object.assign(Object.assign({},t),a),{entity_id:r,type:"remaining_parent_state",remaining:void 0,children:[]}),...o.slice(n)]}t.substract_entities&&(t.subtract_entities=t.substract_entities)}))})),Object.assign(Object.assign({height:200,unit_prefix:"",round:0,min_box_height:3,min_box_distance:5,show_states:!0,show_units:!0},e),{min_state:e.min_state?Math.abs(e.min_state):0,sections:s})}async function Nt(t,e,i){const n={type:"error",error:t,origConfig:e};let s;const o=window.loadCardHelpers?window.loadCardHelpers():void 0;return s=o?(await o).createCardElement(n):function(t,e){void 0===e&&(e=!1);var i=function(t,e){return n("hui-error-card",{type:"error",error:t,config:e})},n=function(t,e){var n=window.document.createElement(t);try{if(!n.setConfig)return;n.setConfig(e)}catch(n){return console.error(t,n),i(n.message,e)}return n};if(!t||"object"!=typeof t||!e&&!t.type)return i("No type defined",t);var s=t.type;if(s&&s.startsWith("custom:"))s=s.substr("custom:".length);else if(e)if($t.has(s))s="hui-"+s+"-row";else{if(!t.entity)return i("Invalid config given.",t);var o=t.entity.split(".",1)[0];s="hui-"+(wt[o]||"text")+"-entity-row"}else s="hui-"+s+"-card";if(customElements.get(s))return n(s,t);var r=i("Custom element doesn't exist: "+t.type+".",t);r.style.display="None";var a=setTimeout((function(){r.style.display=""}),2e3);return customElements.whenDefined(t.type).then((function(){clearTimeout(a),bt(r,"ll-rebuild",{},r)})),r}(n),i&&(s.hass=i),H`${s}`}const Tt=e=>{class i extends e{connectedCallback(){super.connectedCallback(),this.__checkSubscribed()}disconnectedCallback(){super.disconnectedCallback(),this.hassUnsubscribe()}updated(t){super.updated(t),t.has("hass")&&this.__checkSubscribed()}resetSubscriptions(){this.hassUnsubscribe(),this.__checkSubscribed()}hassSubscribe(){return[]}hassUnsubscribe(){if(this.__unsubs){for(;this.__unsubs.length;){const t=this.__unsubs.pop();t instanceof Promise?t.then((t=>t())):t()}this.__unsubs=void 0}}__checkSubscribed(){void 0===this.__unsubs&&this.isConnected&&void 0!==this.hass&&(this.__unsubs=this.hassSubscribe())}}return t([ct({attribute:!1})],i.prototype,"hass",void 0),i},Ht=1,Bt=2,It=t=>(...e)=>({_$litDirective$:t,values:e});class Dt{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}const Rt=It(class extends Dt{constructor(t){var e;if(super(t),t.type!==Ht||"style"!==t.name||(null===(e=t.strings)||void 0===e?void 0:e.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce(((e,i)=>{const n=t[i];return null==n?e:e+`${i=i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${n};`}),"")}update(t,[e]){const{style:i}=t.element;if(void 0===this.vt){this.vt=new Set;for(const t in e)this.vt.add(t);return this.render(e)}this.vt.forEach((t=>{null==e[t]&&(this.vt.delete(t),t.includes("-")?i.removeProperty(t):i[t]="")}));for(const t in e){const n=e[t];null!=n&&(this.vt.add(t),t.includes("-")?i.setProperty(t,n):i[t]=n)}return I}}),Yt=It(class extends Dt{constructor(t){var e;if(super(t),t.type!==Ht||"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 i,n;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===(i=this.st)||void 0===i?void 0:i.has(t))&&this.nt.add(t);return this.render(e)}const s=t.element.classList;this.nt.forEach((t=>{t in e||(s.remove(t),this.nt.delete(t))}));for(const t in e){const i=!!e[t];i===this.nt.has(t)||(null===(n=this.st)||void 0===n?void 0:n.has(t))||(i?(s.add(t),this.nt.add(t)):(s.remove(t),this.nt.delete(t)))}return I}}),{I:Lt}=tt,Vt=()=>document.createComment(""),qt=(t,e,i)=>{var n;const s=t._$AA.parentNode,o=void 0===e?t._$AB:e._$AA;if(void 0===i){const e=s.insertBefore(Vt(),o),n=s.insertBefore(Vt(),o);i=new Lt(e,n,t,t.options)}else{const e=i._$AB.nextSibling,r=i._$AM,a=r!==t;if(a){let e;null===(n=i._$AQ)||void 0===n||n.call(i,t),i._$AM=t,void 0!==i._$AP&&(e=t._$AU)!==r._$AU&&i._$AP(e)}if(e!==o||a){let t=i._$AA;for(;t!==e;){const e=t.nextSibling;s.insertBefore(t,o),t=e}}}return i},Ft=(t,e,i=t)=>(t._$AI(e,i),t),Wt={},Zt=t=>{var e;null===(e=t._$AP)||void 0===e||e.call(t,!1,!0);let i=t._$AA;const n=t._$AB.nextSibling;for(;i!==n;){const t=i.nextSibling;i.remove(),i=t}},Kt=(t,e)=>{var i,n;const s=t._$AN;if(void 0===s)return!1;for(const t of s)null===(n=(i=t)._$AO)||void 0===n||n.call(i,e,!1),Kt(t,e);return!0},Jt=t=>{let e,i;do{if(void 0===(e=t._$AM))break;i=e._$AN,i.delete(t),t=e}while(0===(null==i?void 0:i.size))},Gt=t=>{for(let e;e=t._$AM;t=e){let i=e._$AN;if(void 0===i)e._$AN=i=new Set;else if(i.has(t))break;i.add(t),te(e)}};function Xt(t){void 0!==this._$AN?(Jt(this),this._$AM=t,Gt(this)):this._$AM=t}function Qt(t,e=!1,i=0){const n=this._$AH,s=this._$AN;if(void 0!==s&&0!==s.size)if(e)if(Array.isArray(n))for(let t=i;t{var e,i,n,s;t.type==Bt&&(null!==(e=(n=t)._$AP)&&void 0!==e||(n._$AP=Qt),null!==(i=(s=t)._$AQ)&&void 0!==i||(s._$AQ=Xt))};class ee extends Dt{constructor(){super(...arguments),this._$AN=void 0}_$AT(t,e,i){super._$AT(t,e,i),Gt(this),this.isConnected=t._$AU}_$AO(t,e=!0){var i,n;t!==this.isConnected&&(this.isConnected=t,t?null===(i=this.reconnected)||void 0===i||i.call(this):null===(n=this.disconnected)||void 0===n||n.call(this)),e&&(Kt(this,t),Jt(this))}setValue(t){if((t=>void 0===t.strings)(this._$Ct))this._$Ct._$AI(t,this);else{const e=[...this._$Ct._$AH];e[this._$Ci]=t,this._$Ct._$AI(e,this,0)}}disconnected(){}reconnected(){}}class ie{constructor(t){this.Y=t}disconnect(){this.Y=void 0}reconnect(t){this.Y=t}deref(){return this.Y}}class ne{constructor(){this.Z=void 0,this.q=void 0}get(){return this.Z}pause(){var t;null!==(t=this.Z)&&void 0!==t||(this.Z=new Promise((t=>this.q=t)))}resume(){var t;null===(t=this.q)||void 0===t||t.call(this),this.Z=this.q=void 0}}const se=t=>!(t=>null===t||"object"!=typeof t&&"function"!=typeof t)(t)&&"function"==typeof t.then;const oe=It(class extends ee{constructor(){super(...arguments),this._$Cwt=1073741823,this._$Cyt=[],this._$CK=new ie(this),this._$CX=new ne}render(...t){var e;return null!==(e=t.find((t=>!se(t))))&&void 0!==e?e:I}update(t,e){const i=this._$Cyt;let n=i.length;this._$Cyt=e;const s=this._$CK,o=this._$CX;this.isConnected||this.disconnected();for(let t=0;tthis._$Cwt);t++){const r=e[t];if(!se(r))return this._$Cwt=t,r;t{for(;o.get();)await o.get();const e=s.deref();if(void 0!==e){const i=e._$Cyt.indexOf(r);i>-1&&ibt(t,"hass-notification",e),ce=async(t,e,i,n)=>{var s,o,r,a;let c=i.tap_action;if("double_tap"===n&&i.double_tap_action?c=i.double_tap_action:"hold"===n&&i.hold_action&&(c=i.hold_action),c||(c={action:"more-info"}),!c.confirmation||c.confirmation.exemptions&&c.confirmation.exemptions.some((t=>t.user===e.user.id))||(At("warning"),confirm(c.confirmation.text||e.localize("ui.panel.lovelace.cards.actions.action_confirmation","action",e.localize("ui.panel.lovelace.editor.action-editor.actions."+c.action)||c.action))))switch(c.action){case"more-info":bt(t,"hass-more-info",{entityId:null!==(r=null!==(s=c.entity)&&void 0!==s?s:null===(o=c.data)||void 0===o?void 0:o.entity_id)&&void 0!==r?r:i.entity_id});break;case"navigate":c.navigation_path?function(t,e,i){void 0===i&&(i=!1),i?history.replaceState(null,"",e):history.pushState(null,"",e),bt(window,"location-changed",{replace:i})}(0,c.navigation_path):(ae(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_navigation_path")}),At("failure"));break;case"url":c.url_path?window.open(c.url_path):(ae(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_url")}),At("failure"));break;case"toggle":St(e,i.entity_id),At("light");break;case"call-service":{if(!c.service)return ae(t,{message:e.localize("ui.panel.lovelace.cards.actions.no_service")}),void At("failure");const[i,n]=c.service.split(".",2);e.callService(i,n,null!==(a=c.data)&&void 0!==a?a:c.service_data,c.target),At("light");break}case"fire-dom-event":bt(t,"ll-custom",c)}};let le=class extends st{constructor(){super(...arguments),this.sections=[],this.entityIds=[],this.connections=[],this.connectionsByParent=new Map,this.connectionsByChild=new Map,this.statePerPixelY=0,this.entityStates=new Map,this.highlightedEntities=[],this.lastUpdate=0}shouldUpdate(t){if(!this.config)return!1;if(t.has("forceUpdateTs"))return!0;const e=Date.now();if(this.config.throttle&&e-this.lastUpdate{t===this.lastUpdate&&this.requestUpdate()}),e-this.lastUpdate),!1}if(t.has("highlightedEntities"))return!0;if(t.has("config"))return!0;const i=t.get("states");return!!i&&(!Object.keys(i).length||this.entityIds.some((t=>i[t]!==this.states[t]&&i[t].state!==this.states[t].state)))}willUpdate(t){this.entityIds.length&&!t.has("config")||(this.entityIds=[],this.connections=[],this.connectionsByParent.clear(),this.connectionsByChild.clear(),this.config.sections.forEach((({entities:t},e)=>{t.forEach((t=>{"entity"===t.type&&this.entityIds.push(t.entity_id),t.children.forEach((i=>{var n;const s=null===(n=this.config.sections[e+1])||void 0===n?void 0:n.entities.find((t=>t.entity_id===i));if(!s)throw new Error(ft("common.missing_child")+" "+i);const o={parent:t,child:s,state:0,prevParentState:0,prevChildState:0,ready:!1};this.connections.push(o),this.connectionsByParent.has(t)||this.connectionsByParent.set(t,[]),this.connectionsByParent.get(t).push(o),this.connectionsByChild.has(s)||this.connectionsByChild.set(s,[]),this.connectionsByChild.get(s).push(o)}))}))})))}_calcConnections(){const t=new Map,e=new Map;this.connections.forEach((t=>{t.ready=!1})),this.connections.forEach((i=>this._calcConnection(i,t,e)))}_calcConnection(t,e,i){var n,s,o,r;if(t.ready)return;const{parent:a,child:c}=t;[a,c].forEach((t=>{var n;"remaining_child_state"===t.type&&this.connectionsByParent.get(t).forEach((t=>{var n;t.ready||null===(n=this.connectionsByChild.get(t.child))||void 0===n||n.forEach((t=>{t.parent!==a&&this._calcConnection(t,e,i)}))})),"remaining_parent_state"===t.type&&(null===(n=this.connectionsByChild.get(t))||void 0===n||n.forEach((t=>{var n;t.ready||null===(n=this.connectionsByParent.get(t.parent))||void 0===n||n.forEach((t=>{t.child!==c&&this._calcConnection(t,e,i)}))})))}));const l=null!==(n=this._getMemoizedState(a).state)&&void 0!==n?n:0;t.prevParentState=null!==(s=i.get(a))&&void 0!==s?s:0;const h=Math.max(0,l-t.prevParentState),d=null!==(o=this._getMemoizedState(c).state)&&void 0!==o?o:0;t.prevChildState=null!==(r=e.get(c))&&void 0!==r?r:0;const u=Math.max(0,d-t.prevChildState);h&&u?(t.state=Math.min(h,u),i.set(a,t.prevParentState+t.state),e.set(c,t.prevChildState+t.state)):t.state=0,t.ready=!0,"passthrough"===c.type&&this.entityStates.delete(c)}_getMemoizedState(t){if(!this.entityStates.has(t)){const e=this._getEntityState(t),i=t.unit_of_measurement||e.attributes.unit_of_measurement,n=Mt(this.config.unit_prefix,Number(e.state),i);if("passthrough"===t.type){const e=this.connectionsByChild.get(t);if(!e)throw new Error("Invalid entity config "+JSON.stringify(t));const i=e.reduce(((t,e)=>e.ready?t+e.state:1/0),0);i!==1/0&&(n.state=i)}if(t.add_entities&&t.add_entities.forEach((t=>{const e=this._getEntityState({entity_id:t,children:[]}),{state:s}=Mt(this.config.unit_prefix,Number(e.state),e.attributes.unit_of_measurement||i);n.state+=s})),t.subtract_entities&&t.subtract_entities.forEach((t=>{const e=this._getEntityState({entity_id:t,children:[]}),{state:s}=Mt(this.config.unit_prefix,Number(e.state),e.attributes.unit_of_measurement||i);n.state-=Math.min(s,n.state)})),n.state===1/0)return n;this.entityStates.set(t,n)}return this.entityStates.get(t)}_calcBoxes(){this.statePerPixelY=0,this.sections=this.config.sections.map((t=>{let e=0;const i=t.entities.filter((t=>{var e,i;const{min_state:n}=this.config;if("remaining_parent_state"===t.type)return null===(e=this.connectionsByChild.get(t))||void 0===e?void 0:e.some((t=>t.state&&t.state>=n));if("remaining_child_state"===t.type)return null===(i=this.connectionsByParent.get(t))||void 0===i?void 0:i.some((t=>t.state&&t.state>=n));const{state:s}=this._getMemoizedState(t);return s&&s>=n})).map((t=>{const{state:i,unit_of_measurement:n}=this._getMemoizedState(t);e+=i;let s=t.color||"var(--primary-color)";if(void 0!==t.color_on_state&&t.color_on_state){const e=void 0===t.color_limit?1:t.color_limit,n=void 0===t.color_below?"var(--primary-color)":t.color_below,o=void 0===t.color_above?"var(--paper-item-icon-color)":t.color_above;s=i>e?o:n}return{config:t,entity:this._getEntityState(t),entity_id:Ut(t),state:i,unit_of_measurement:n,color:s,children:t.children,connections:{parents:[]},top:0,size:0}}));if(!i.length)return{boxes:i,total:e,spacerH:0,statePerPixelY:0};const n=this.config.height-(i.length-1)*this.config.min_box_distance,s=this._calcBoxHeights(i,n,e);return{boxes:this._sortBoxes(s.boxes,t.sort_by,t.sort_dir),total:e,statePerPixelY:s.statePerPixelY}})).filter((t=>t.boxes.length>0)).map((t=>{let e=0,{boxes:i}=t;t.statePerPixelY!==this.statePerPixelY?i=i.map((t=>{const i=Math.max(this.config.min_box_height,Math.floor(t.state/this.statePerPixelY));return e+=i,Object.assign(Object.assign({},t),{size:i})})):e=i.reduce(((t,e)=>t+e.size),0);const n=this.config.height-e,s=i.length>1?n/(i.length-1):this.config.height;let o=0;return i=i.map((t=>{const e=o;return o+=t.size+s,Object.assign(Object.assign({},t),{top:e})})),Object.assign(Object.assign({},t),{boxes:i,spacerH:s})}))}_sortBoxes(t,e,i="desc"){return"state"===e&&("desc"===i?t.sort(((t,e)=>t.state>e.state?-1:t.statet.statee.state?1:0))),t}_calcBoxHeights(t,e,i){const n=i/e;n>this.statePerPixelY&&(this.statePerPixelY=n);let s=0;const o=t.map((t=>{if(t.size===this.config.min_box_height)return t;let e=Math.floor(t.state/this.statePerPixelY);return e0?this._calcBoxHeights(o,e-s,i):{boxes:o,statePerPixelY:this.statePerPixelY}}highlightPath(t,e){var i,n;this.highlightedEntities.push(t),"children"===e?null===(i=this.connectionsByParent.get(t))||void 0===i||i.forEach((t=>{t.highlighted=!0,this.highlightPath(t.child,"children")})):null===(n=this.connectionsByChild.get(t))||void 0===n||n.forEach((t=>{t.highlighted=!0,this.highlightPath(t.parent,"parents")}))}_handleBoxClick(t){ce(this,this.hass,t.config,"tap")}_handleMouseEnter(t){this.highlightPath(t.config,"children"),this.highlightPath(t.config,"parents"),this.highlightedEntities=[...this.highlightedEntities]}_handleMouseLeave(){this.highlightedEntities=[],this.connections.forEach((t=>{t.highlighted=!1}))}_getEntityState(t){if("remaining_parent_state"===t.type){const e=this.connectionsByChild.get(t);if(!e)throw new Error("Invalid entity config "+JSON.stringify(t));const{parent:i}=e[0],n=e.reduce(((t,e)=>e.ready?t+e.state:1/0),0),s=this._getEntityState(i),{unit_of_measurement:o}=Mt(this.config.unit_prefix,0,s.attributes.unit_of_measurement);return Object.assign(Object.assign({},s),{state:n,attributes:Object.assign(Object.assign({},s.attributes),{unit_of_measurement:o})})}if("remaining_child_state"===t.type){const e=this.connectionsByParent.get(t);if(!e)throw new Error("Invalid entity config "+JSON.stringify(t));const{child:i}=e[0],n=e.reduce(((t,e)=>e.ready?t+e.state:1/0),0),s=this._getEntityState(i),{unit_of_measurement:o}=Mt(this.config.unit_prefix,0,s.attributes.unit_of_measurement);return Object.assign(Object.assign({},s),{state:n,attributes:Object.assign(Object.assign({},s.attributes),{unit_of_measurement:o})})}let e=this.states[Ut(t)];if(!e)throw new Error("Entity not found "+Ut(t));return"object"==typeof t&&t.attribute&&(e=Object.assign(Object.assign({},e),{state:e.attributes[t.attribute]}),t.unit_of_measurement&&(e=Object.assign(Object.assign({},e),{attributes:Object.assign(Object.assign({},e.attributes),{unit_of_measurement:t.unit_of_measurement})}))),e}static get styles(){return re}renderSection(t){const{show_names:e,show_icons:i,show_states:n,show_units:s}=this.config,o=this.sections[t],{boxes:r,spacerH:a}=o,c=tt.children.length>0)),{min_width:l}=this.config.sections[t];return H`
${c?H`
${this.renderBranchConnectors(t)}
`:null} ${r.map(((t,o)=>{const{entity:r,extraSpacers:c}=t,l=function(t,e){let i,n=e;do{i=t.toFixed(n++)}while(/^[0\.]*$/.test(i)&&n<100);return parseFloat(i).toLocaleString()}(t.state,this.config.round),h="passthrough"!==t.config.type,d=t.config.name||r.attributes.friendly_name||"",u=t.config.icon||Ot(r),p=t.size+a-1,g={lineHeight:"15px"},f={};if(p<15){const t=p/15;g.fontSize=`${t}em`,g.lineHeight=`${t}em`}const m=d.split("\n").filter((t=>t)).length;return m>1&&(f.whiteSpace="pre",g.fontSize?(f.fontSize=1/m+.1+"rem",f.lineHeight=1/m+.1+"rem"):p<15*m&&(f.fontSize=p/15/m*1.1+"em",f.lineHeight=p/15/m*1.1+"em")),H`${o>0?H`
`:null} ${c?H`
`:null}
${i&&h?H``:null}
${n&&h?H`${l}${s?H`${t.unit_of_measurement}`:null}`:null} ${e&&h?H` ${d}`:null}
${c?H`
`:null}`}))}
`}renderBranchConnectors(t){const e=this.sections[t],{boxes:i}=e;return i.filter((t=>t.children.length>0)).map((e=>{const i=this.sections[t+1].boxes.filter((t=>e.children.includes(t.entity_id))),n=function(t,e,i){let n=0;return e.map((e=>{const s=null==i?void 0:i.find((t=>t.child.entity_id===e.entity_id));if(!s)throw new Error(`Missing connection: ${t.entity_id} - ${e.entity_id}`);const{state:o,prevChildState:r}=s;if(o<=0)return{state:o};const a=n/t.state*t.size+t.top;n+=o;const c=Math.max(o/t.state*t.size,0),l=r/e.state*e.size+e.top,h=Math.max(o/e.state*e.size,0);return{startY:a,startSize:c,startColor:t.color,endY:l,endSize:h,endColor:e.color,state:o,highlighted:s.highlighted}}))}(e,i,this.connectionsByParent.get(e.config)).filter(((t,e)=>{var n;if(t.state>0){if(i[e].connections.parents.push(t),"passthrough"===i[e].config.type){const s=(null===(n=this.connectionsByChild.get(i[e].config))||void 0===n?void 0:n.reduce(((t,e)=>t+e.state),0))||0;if(s!==i[e].state){i[e].state=s;const n=Math.floor(s/this.statePerPixelY);i[e].extraSpacers=(i[e].size-n)/2,t.endY+=i[e].extraSpacers,i[e].top+=i[e].extraSpacers,i[e].size=n}}return!0}return!1}));return B`${n.map(((t,i)=>B``))}${n.map(((t,i)=>B``))}`}))}render(){try{this.entityStates.clear();const t=Yt({container:!0,wide:!!this.config.wide,"with-header":!!this.config.title});return Object.keys(this.states).length?(this._calcConnections(),this._calcBoxes(),this.lastUpdate=Date.now(),H`
${this.sections.map(((t,e)=>this.renderSection(e)))}
`):H`
${ft("common.loading")}
`}catch(t){return console.error(t),H`${oe(Nt(String(t),this.config,this.hass))}`}}};function he(t){if(null===t||!0===t||!1===t)return NaN;var e=Number(t);return isNaN(e)?e:e<0?Math.ceil(e):Math.floor(e)}function de(t,e){if(e.length1?"s":"")+" required, but only "+e.length+" present")}function ue(t){return ue="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},ue(t)}function pe(t){de(1,arguments);var e=Object.prototype.toString.call(t);return t instanceof Date||"object"===ue(t)&&"[object Date]"===e?new Date(t.getTime()):"number"==typeof t||"[object Number]"===e?new Date(t):("string"!=typeof t&&"[object String]"!==e||"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 ge(t,e){de(2,arguments);var i=pe(t).getTime(),n=he(e);return new Date(i+n)}t([ct({attribute:!1})],le.prototype,"hass",void 0),t([ct({attribute:!1})],le.prototype,"states",void 0),t([ct({attribute:!1})],le.prototype,"forceUpdateTs",void 0),t([lt()],le.prototype,"config",void 0),t([lt()],le.prototype,"sections",void 0),t([lt()],le.prototype,"entityIds",void 0),t([lt()],le.prototype,"connections",void 0),t([lt()],le.prototype,"connectionsByParent",void 0),t([lt()],le.prototype,"connectionsByChild",void 0),t([lt()],le.prototype,"statePerPixelY",void 0),t([lt()],le.prototype,"entityStates",void 0),t([lt()],le.prototype,"highlightedEntities",void 0),t([lt()],le.prototype,"lastUpdate",void 0),le=t([rt("sankey-chart-base")],le);function fe(t){var e=new Date(Date.UTC(t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()));return e.setUTCFullYear(t.getFullYear()),t.getTime()-e.getTime()}function me(t){de(1,arguments);var e=pe(t);return e.setHours(0,0,0,0),e}function _e(t,e){de(2,arguments);var i=me(t),n=me(e),s=i.getTime()-fe(i),o=n.getTime()-fe(n);return Math.round((s-o)/864e5)}function ye(t,e){var i=t.getFullYear()-e.getFullYear()||t.getMonth()-e.getMonth()||t.getDate()-e.getDate()||t.getHours()-e.getHours()||t.getMinutes()-e.getMinutes()||t.getSeconds()-e.getSeconds()||t.getMilliseconds()-e.getMilliseconds();return i<0?-1:i>0?1:i}const ve=["grid","solar","battery"],be=(t,e="_energy")=>t.connection[e]?t.connection[e]:null,$e=t=>{if(!t||t.length<2)return null;const e=t[t.length-1].sum;if(null===e)return null;const i=t[0].sum;return null===i?e:e-i};async function we(t,e,i){const n=function(t,e){de(2,arguments);var i=pe(t),n=pe(e),s=ye(i,n),o=Math.abs(_e(i,n));i.setDate(i.getDate()-s*o);var r=Number(ye(i,n)===-s),a=s*(o-r);return 0===a?0:a}(e.end||new Date,e.start),s=function(t,e){de(2,arguments);var i=he(e);return ge(t,36e5*i)}(e.start,-1),o=n>35?"month":n>2?"day":"hour",r=await((t,e,i,n,s="hour")=>t.callWS({type:"recorder/statistics_during_period",start_time:e.toISOString(),end_time:null==i?void 0:i.toISOString(),statistic_ids:n,period:s}))(t,s,e.end,i,o);return Object.values(r).forEach((t=>{t.length&&new Date(t[0].start)>s&&t.unshift(Object.assign(Object.assign({},t[0]),{start:s.toISOString(),end:s.toISOString(),sum:0,state:0}))})),i.reduce(((t,e)=>Object.assign(Object.assign({},t),{[e]:$e(r[e])})),{})}function xe(t){return"solar"===t?"var(--warning-color)":"battery"===t?"var(--success-color)":void 0}async function Ce(t,e){const i=await((t,e)=>t.callWS({type:"config/entity_registry/get",entity_id:e}))(t,e);return i.area_id}const Ae=(t,e,i)=>{const n=new Map;for(let s=e;s<=i;s++)n.set(t[s],s);return n},Se=It(class extends Dt{constructor(t){if(super(t),t.type!==Bt)throw Error("repeat() can only be used in text expressions")}ht(t,e,i){let n;void 0===i?i=e:void 0!==e&&(n=e);const s=[],o=[];let r=0;for(const e of t)s[r]=n?n(e,r):r,o[r]=i(e,r),r++;return{values:o,keys:s}}render(t,e,i){return this.ht(t,e,i).values}update(t,[e,i,n]){var s;const o=(t=>t._$AH)(t),{values:r,keys:a}=this.ht(e,i,n);if(!Array.isArray(o))return this.ut=a,r;const c=null!==(s=this.ut)&&void 0!==s?s:this.ut=[],l=[];let h,d,u=0,p=o.length-1,g=0,f=r.length-1;for(;u<=p&&g<=f;)if(null===o[u])u++;else if(null===o[p])p--;else if(c[u]===a[g])l[g]=Ft(o[u],r[g]),u++,g++;else if(c[p]===a[f])l[f]=Ft(o[p],r[f]),p--,f--;else if(c[u]===a[f])l[f]=Ft(o[u],r[f]),qt(t,l[f+1],o[u]),u++,f--;else if(c[p]===a[g])l[g]=Ft(o[p],r[g]),qt(t,o[u],o[p]),p--,g++;else if(void 0===h&&(h=Ae(a,g,f),d=Ae(c,u,p)),h.has(c[u]))if(h.has(c[p])){const e=d.get(a[g]),i=void 0!==e?o[e]:null;if(null===i){const e=qt(t,o[u]);Ft(e,r[g]),l[g]=e}else l[g]=Ft(i,r[g]),qt(t,o[u],i),o[e]=null;g++}else Zt(o[p]),p--;else Zt(o[u]),u++;for(;g<=f;){const e=qt(t,l[f+1]);Ft(e,r[g]),l[g++]=e}for(;u<=p;){const t=o[u++];null!==t&&Zt(t)}return this.ut=a,((t,e=Wt)=>{t._$AH=e})(t,l),I}});let Ee=class extends st{constructor(){super(...arguments),this._computeLabel=t=>ft("editor.fields."+t.name)}_valueChanged(t){this.onChange(t.detail.value)}_editChild(t){var e;const{detail:{value:i},target:n}=t,s="string"==typeof this.entity?{entity_id:this.entity}:this.entity;let o=null!==(e=s.children)&&void 0!==e?e:[];"number"==typeof(null==n?void 0:n.index)?i?o[n.index]=i:o=o.filter(((t,e)=>e!==n.index)):i&&(o=[...o,i],n.value=""),this.onChange(Object.assign(Object.assign({},s),{children:o}))}_getEntityIcon(t){const e=this.hass.states[t];return e?Ot(e):void 0}render(){const t="string"==typeof this.entity?{entity_id:this.entity}:this.entity,e=Object.assign(Object.assign({},Pt),t),i=((t,e)=>[{name:"type",selector:{select:{mode:"dropdown",options:[{value:"entity",label:ft("editor.entity_types.entity")},{value:"remaining_parent_state",label:ft("editor.entity_types.remaining_parent_state")},{value:"remaining_child_state",label:ft("editor.entity_types.remaining_child_state")},{value:"passthrough",label:ft("editor.entity_types.passthrough")}]}}},{name:"entity_id",selector:{entity:{}}},{type:"grid",name:"",schema:[{name:"attribute",selector:{attribute:{entity_id:t.entity_id}}},{name:"unit_of_measurement",selector:{text:{}}}]},{name:"name",selector:{text:{}}},{type:"grid",name:"",schema:[{name:"icon",selector:{icon:{placeholder:e}}},{name:"color",selector:{text:{}}}]},{name:"tap_action",selector:{"ui-action":{}}},{name:"color_on_state",selector:{boolean:{}}},...t.color_on_state?[{name:"color_limit",selector:{number:{mode:"box",unit_of_measurement:t.unit_of_measurement}}},{name:"color_above",selector:{text:{}}},{name:"color_below",selector:{text:{}}}]:[]])(e,e.icon||this._getEntityIcon(t.entity_id));return H`

${ft("editor.entity_editor")}

${Se(e.children||[],(t=>t),((t,e)=>H`
`))}
`}static get styles(){return r`.header{display:flex;align-items:center}.header ha-icon{display:flex}.children{margin-top:10px;padding:0 10px 10px}.child{margin-bottom:8px}`}};t([ct({attribute:!1})],Ee.prototype,"hass",void 0),t([ct({attribute:!1})],Ee.prototype,"entity",void 0),t([ct({attribute:!1})],Ee.prototype,"onClose",void 0),t([ct({attribute:!1})],Ee.prototype,"onChange",void 0),Ee=t([rt("sankey-chart-entity-editor")],Ee);let ke=class extends st{constructor(){super(...arguments),this._initialized=!1,this._handleEntityConfig=t=>{var e,i;this._editEntity({detail:{value:t},target:{section:null===(e=this._entityConfig)||void 0===e?void 0:e.sectionIndex,index:null===(i=this._entityConfig)||void 0===i?void 0:i.entityIndex}}),this._entityConfig=Object.assign(Object.assign({},this._entityConfig),{entity:t})}}setConfig(t){this._config=t,this.loadCardHelpers()}shouldUpdate(){return this._initialized||this._initialize(),!0}_initialize(){var t,e;void 0!==this.hass&&void 0!==this._config&&void 0!==this._helpers&&(this._initialized=!0,customElements.get("ha-form")||null===(t=customElements.get("hui-button-card"))||void 0===t||t.getConfigElement(),customElements.get("ha-entity-picker")||null===(e=customElements.get("hui-entities-card"))||void 0===e||e.getConfigElement())}async loadCardHelpers(){this._helpers=await window.loadCardHelpers()}_valueChanged(t){if(!this._config||!this.hass)return;const e=t.target;if(e.configValue)if("function"==typeof e.configValue)this._config=e.configValue(this._config,void 0!==e.checked?e.checked:e.value);else if(""===e.value){const t=Object.assign({},this._config);delete t[e.configValue],this._config=t}else this._config=Object.assign(Object.assign({},this._config),{[e.configValue]:void 0!==e.checked?e.checked:e.value});this._updateConfig()}_addEntity(t){var e;const i=t.detail.value;if(""===i)return;const n=t.target;if("number"==typeof n.section){const t=(null===(e=this._config)||void 0===e?void 0:e.sections)||[];this._config=Object.assign(Object.assign({},this._config),{sections:t.map(((t,e)=>e===n.section?Object.assign(Object.assign({},t),{entities:[...t.entities,i]}):t))})}t.target.value="",this._updateConfig()}_editEntity(t){var e;const{value:i}=t.detail,n="string"==typeof i?{entity_id:i}:i,s=t.target;if("number"==typeof s.section&&"number"==typeof s.index){const t=(null===(e=this._config)||void 0===e?void 0:e.sections)||[];this._config=Object.assign(Object.assign({},this._config),{sections:t.map(((t,e)=>{if(e!==s.section)return t;const i=t.entities[s.index],o="string"==typeof i?n:Object.assign(Object.assign({},i),n);return Object.assign(Object.assign({},t),{entities:(null==n?void 0:n.entity_id)?[...t.entities.slice(0,s.index),o,...t.entities.slice(s.index+1)]:t.entities.filter(((t,e)=>e!==s.index))})}))})}this._updateConfig()}_configEntity(t,e){var i;const n=(null===(i=this._config)||void 0===i?void 0:i.sections)||[];this._entityConfig={sectionIndex:t,entityIndex:e,entity:n[t].entities[e]}}_updateConfig(){bt(this,"config-changed",{config:this._config})}render(){if(!this.hass||!this._helpers)return H``;const t=this._config||{},{autoconfig:e}=t,i=t.sections||[];return this._entityConfig?H``:H`

${ft("editor.autoconfig")}

${e?H``:D}
${e?D:this._renderSections(i)}

${ft("editor.yaml_disclaimer")}

${ft("editor.docs")}

`}_renderSections(t){return H`

${ft("editor.sections")}

${t.map(((t,e)=>H`
${Se(t.entities,(t=>e+Ut(t)),((t,i)=>H`
`))}
`))}${ft("editor.add_section")}
`}static get styles(){return r`.card-config{padding:16px}.options{display:grid;margin-bottom:20px}.sections{display:flex;flex-direction:column;margin-bottom:20px}.section{margin-bottom:16px;padding:0 10px 10px}ha-formfield{padding-bottom:8px}.add-entity{display:block;margin-left:31px;margin-right:36px;margin-inline-start:31px;margin-inline-end:36px;direction:var(--direction)}.entity{display:flex;align-items:center;margin-bottom:8px}.entity .handle{visibility:hidden;padding-right:8px;cursor:move;padding-inline-end:8px;padding-inline-start:initial;direction:var(--direction)}.entity .handle>*{pointer-events:none}.entity ha-entity-picker{flex-grow:1}.edit-icon{--mdc-icon-button-size:36px}`}};t([ct({attribute:!1})],ke.prototype,"hass",void 0),t([ct({attribute:!1})],ke.prototype,"lovelace",void 0),t([lt()],ke.prototype,"_config",void 0),t([lt()],ke.prototype,"_helpers",void 0),t([lt()],ke.prototype,"_entityConfig",void 0),ke=t([rt("sankey-chart-editor")],ke),console.info(`%c sankey-chart %c ${ft("common.version")} 1.13.0 `,"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),window.customCards=window.customCards||[],window.customCards.push({type:"sankey-chart",name:"Sankey Chart",description:"A card to display a sankey chart. For example for power consumptionA template custom card for you to create something awesome"});let Oe=class extends(Tt(st)){constructor(){super(...arguments),this.states={},this.entityIds=[]}static async getConfigElement(){return document.createElement("sankey-chart-editor")}static getStubConfig(){return{autoconfig:{print_yaml:!1}}}hassSubscribe(){if(!this.config.energy_date_selection)return[];const t=Date.now(),e=(i,n)=>{const s=be(this.hass);s?i(s):Date.now()-t>1e4?(console.debug(be(this.hass)),n(new Error("No energy data received. Make sure to add a `type: energy-date-selection` card to this screen."))):setTimeout((()=>e(i,n)),100)},i=new Promise(e);return setTimeout((()=>{this.error||Object.keys(this.states).length||(this.error=new Error("Something went wrong. No energy data received."),console.debug(be(this.hass)))}),2e4),i.catch((t=>{this.error=t})),[i.then((async t=>{const e=this.config.autoconfig||"object"==typeof this.config.autoconfig;if(e&&!this.config.sections.length)try{await this.autoconfig(t)}catch(t){this.error=new Error((null==t?void 0:t.message)||t)}return t.subscribe((async i=>{if(e&&!this.config.sections.length)try{await this.autoconfig(t)}catch(t){return void(this.error=new Error((null==t?void 0:t.message)||t))}if(this.entityIds.length){const t=await we(this.hass,i,this.entityIds),e={};Object.keys(t).forEach((i=>{this.hass.states[i]&&(e[i]=Object.assign(Object.assign({},this.hass.states[i]),{state:String(t[i])}))})),this.states=e}this.forceUpdateTs=Date.now()}))}))]}setConfig(t){if("object"!=typeof t)throw new Error(ft("common.invalid_configuration"));this.setNormalizedConfig(zt(t)),this.resetSubscriptions()}setNormalizedConfig(t){this.config=t,this.entityIds=[],this.config.sections.forEach((({entities:t})=>{t.forEach((t=>{"entity"===t.type&&this.entityIds.push(t.entity_id),t.add_entities&&t.add_entities.forEach((t=>this.entityIds.push(t))),t.subtract_entities&&t.subtract_entities.forEach((t=>this.entityIds.push(t)))}))}))}async autoconfig(t){var e,i,n;if(!t.prefs)return;const s=((null===(e=t.prefs)||void 0===e?void 0:e.energy_sources)||[]).map((t=>Object.assign(Object.assign({},t),{ids:[t,...t.flow_from||[]].map((t=>t.stat_energy_from)).filter((t=>!(!t||!this.hass.states[t])||(t&&console.warn("Ignoring missing entity "+t),!1)))}))).filter((t=>ve.includes(t.type)&&t.ids.length)).sort(((t,e)=>t.type===e.type?0:"solar"===t.type||"battery"===t.type&&"solar"!==e.type?-1:1)),o=((null===(i=t.prefs)||void 0===i?void 0:i.device_consumption)||[]).map((t=>t.stat_consumption)).filter((t=>!!this.hass.states[t]||(console.warn("Ignoring missing entity "+t),!1))),r=await async function(t,e){const i={};for(const n of e){const e=await Ce(t,n),s=e?t.areas[e]:{area_id:"no_area",name:"No area"};i[s.area_id]||(i[s.area_id]={area:s,entities:[]}),i[s.area_id].entities.push(n)}return i}(this.hass,o),a=Object.values(r).sort(((t,e)=>"No area"===t.area.name?1:"No area"===e.area.name?-1:0)),c=a.reduce(((t,e)=>[...t,...e.entities]),[]),l=[{entities:s.map((t=>{var e,i;const n=t.stat_energy_to?[t.stat_energy_to]:(null===(e=t.flow_to)||void 0===e?void 0:e.map((t=>t.stat_energy_to)))||void 0;return{entity_id:t.ids[0],add_entities:(null===(i=t.ids)||void 0===i?void 0:i.length)>1?t.ids.slice(1):void 0,subtract_entities:n,type:"entity",color:xe(t.type),children:["total"]}}))},{entities:[{entity_id:"total",type:"remaining_parent_state",name:"Total Consumption",children:[...a.map((t=>t.area.area_id)),"unknown"]}]},{entities:[...a.map((({area:t,entities:e})=>({entity_id:t.area_id,type:"remaining_child_state",name:t.name,children:e}))),{entity_id:"unknown",type:"remaining_parent_state",name:"Unknown",children:[]}]},{entities:c.map((t=>({entity_id:t,type:"entity",children:[]})))}],h=s.find((t=>"grid"===t.type));h&&(null===(n=null==h?void 0:h.flow_to)||void 0===n?void 0:n.length)&&(null==h||h.flow_to.forEach((({stat_energy_to:t})=>{l[1].entities.unshift({entity_id:t,subtract_entities:(h.flow_from||[]).map((t=>t.stat_energy_from)),type:"entity",color:xe(h.type),children:[]}),l[0].entities.forEach((e=>{e.children.unshift(t)}))})));const d=s.find((t=>"battery"===t.type));d&&d.stat_energy_from&&d.stat_energy_to&&(l[1].entities.unshift({entity_id:d.stat_energy_to,subtract_entities:[d.stat_energy_from],type:"entity",color:xe(d.type),children:[]}),l[0].entities.forEach((t=>{t.children.unshift(d.stat_energy_to)}))),this.setNormalizedConfig(Object.assign(Object.assign({},this.config),{sections:l}))}getCardSize(){return 4}render(){var t;if(this.error)return H`${oe(Nt(String(this.error),this.config,this.hass))}`;const e=null===(t=this.config.autoconfig)||void 0===t?void 0:t.print_yaml;return H`${e&&this.config.sections.length?H`${oe(Nt("",Object.assign(Object.assign({},this.config),{autoconfig:void 0}),this.hass))}`:""}`}};t([ct({attribute:!1})],Oe.prototype,"hass",void 0),t([function(t,e){return(({finisher:t,descriptor:e})=>(i,n)=>{var s;if(void 0===n){const n=null!==(s=i.originalKey)&&void 0!==s?s:i.key,o=null!=e?{kind:"method",placement:"prototype",key:n,descriptor:e(i.key)}:{...i,key:n};return null!=t&&(o.finisher=function(e){t(e,n)}),o}{const s=i.constructor;void 0!==e&&Object.defineProperty(i,n,e(n)),null==t||t(s,n)}})({descriptor:i=>{const n={get(){var e,i;return null!==(i=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;n.get=function(){var i,n;return void 0===this[e]&&(this[e]=null!==(n=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null),this[e]}}return n}})}("ha-chart-base")],Oe.prototype,"_chart",void 0),t([lt()],Oe.prototype,"config",void 0),t([lt()],Oe.prototype,"states",void 0),t([lt()],Oe.prototype,"entityIds",void 0),t([lt()],Oe.prototype,"error",void 0),t([lt()],Oe.prototype,"forceUpdateTs",void 0),Oe=t([rt("sankey-chart")],Oe);export{Oe as SankeyChart}; From 4b59f3b95905d4a11fd2217cd65400433140d18e Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 3 Jun 2023 18:45:58 +0100 Subject: [PATCH 068/158] Rename ensuite sensor --- groups.yaml | 2 +- packages/device_alerts.yaml | 2 +- packages/ensuite.yaml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/groups.yaml b/groups.yaml index 45a2a6ce..4762d2ba 100644 --- a/groups.yaml +++ b/groups.yaml @@ -94,7 +94,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 diff --git a/packages/device_alerts.yaml b/packages/device_alerts.yaml index d9dc41d4..77c7152b 100644 --- a/packages/device_alerts.yaml +++ b/packages/device_alerts.yaml @@ -9,7 +9,7 @@ automation: - sensor.garage_temperature - binary_sensor.hall_door_motion - binary_sensor.hall_rooms_motion - - binary_sensor.ensuite_motion + - binary_sensor.ensuite_motion_occupancy - binary_sensor.living_room_motion - binary_sensor.kitchen_motion - binary_sensor.front_hall_motion diff --git a/packages/ensuite.yaml b/packages/ensuite.yaml index f55ce55c..eb9b6ee4 100644 --- a/packages/ensuite.yaml +++ b/packages/ensuite.yaml @@ -44,7 +44,7 @@ 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: @@ -135,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 @@ -153,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 From a1aafefc9103d30dde31545a2c8fd65a04ff76ca Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 3 Jun 2023 18:58:11 +0100 Subject: [PATCH 069/158] Guest room --- groups.yaml | 6 +++--- sensors.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/groups.yaml b/groups.yaml index 4762d2ba..061b1ab1 100644 --- a/groups.yaml +++ b/groups.yaml @@ -116,9 +116,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 diff --git a/sensors.yaml b/sensors.yaml index d0580693..d9472bce 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -93,7 +93,7 @@ entity_ids: - sensor.front_hall_sensor_humidity - sensor.craft_room_humidity - - sensor.guest_room_humidity + - sensor.guest_room_multi_sensor_humidity - sensor.living_room_humidity - sensor.master_bedroom_humidity - sensor.nook_humidity @@ -175,8 +175,8 @@ guest_room: friendly_name: Guest Room unique_id: guest_room_thermal_comfort - temperature_sensor: sensor.guest_room_temperature - humidity_sensor: sensor.guest_room_humidity + 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 From a151f7278df79a9a936aa7d534e3240184d2bada Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 3 Jun 2023 19:03:40 +0100 Subject: [PATCH 070/158] Master bathroom --- packages/master_bathroom.yaml | 2 +- sensors.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/sensors.yaml b/sensors.yaml index d9472bce..d49380ef 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -85,7 +85,7 @@ - sensor.nook_temperature - sensor.hall_temperature - sensor.study_temperature - - sensor.bathroom_temperature + - sensor.bathroom_multi_sensor_temperature - platform: min_max name: House Average Humidity @@ -99,7 +99,7 @@ - sensor.nook_humidity - sensor.hall_humidity - sensor.study_humidity - - sensor.bathroom_humidity + - sensor.bathroom_multi_sensor_humidity - platform: time_date display_options: From 02c4fe111210a7637ad092d43c43af1fc8f9ad76 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 3 Jun 2023 19:06:49 +0100 Subject: [PATCH 071/158] More likely candidates --- sensors.yaml | 54 ++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/sensors.yaml b/sensors.yaml index d49380ef..36ce4d46 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -79,26 +79,26 @@ type: mean entity_ids: - sensor.kitchen_temperature - - sensor.craft_room_temperature - - sensor.living_room_temperature - - sensor.master_bedroom_temperature - - sensor.nook_temperature - - sensor.hall_temperature - - sensor.study_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.hall_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.front_hall_multi_sensor_humidity + - sensor.craft_room_multi_sensor_humidity - sensor.guest_room_multi_sensor_humidity - - sensor.living_room_humidity - - sensor.master_bedroom_humidity - - sensor.nook_humidity - - sensor.hall_humidity - - sensor.study_humidity + - sensor.living_room_multi_sensor_humidity + - sensor.master_bedroom_multi_sensor_humidity + - sensor.nook_multi_sensor_humidity + - sensor.hall_multi_sensor_humidity + - sensor.study_multi_sensor_humidity - sensor.bathroom_multi_sensor_humidity - platform: time_date @@ -140,38 +140,38 @@ living_room: friendly_name: Living Room unique_id: living_room_thermal_comfort - temperature_sensor: sensor.living_room_temperature - humidity_sensor: sensor.living_room_humidity + temperature_sensor: sensor.living_room_multi_sensor_temperature + humidity_sensor: sensor.living_room_multi_sensor_humidity dining_nook: friendly_name: Dining Nook unique_id: dining_nook_thermal_comfort - temperature_sensor: sensor.nook_temperature - humidity_sensor: sensor.nook_humidity + temperature_sensor: sensor.nook_multi_sensor_temperature + humidity_sensor: sensor.nook_multi_sensor_humidity hall: friendly_name: Hall unique_id: hall_thermal_comfort - temperature_sensor: sensor.hall_temperature - humidity_sensor: sensor.hall_humidity + temperature_sensor: sensor.hall_multi_sensor_temperature + humidity_sensor: sensor.hall_multi_sensor_humidity bathroom: friendly_name: Bathroom unique_id: bathroom_thermal_comfort - temperature_sensor: sensor.bathroom_temperature - humidity_sensor: sensor.bathroom_humidity + temperature_sensor: sensor.bathroom_multi_sensor_temperature + humidity_sensor: sensor.bathroom_multi_sensor_humidity front_hall: friendly_name: Front Hall unique_id: front_hall_thermal_comfort - temperature_sensor: sensor.front_hall_sensor_temperature - humidity_sensor: sensor.front_hall_sensor_humidity + temperature_sensor: sensor.front_hall_multi_sensor_temperature + humidity_sensor: sensor.front_hall_multi_sensor_humidity craft_room: friendly_name: Craft Room unique_id: craft_room_thermal_comfort - temperature_sensor: sensor.craft_room_temperature - humidity_sensor: sensor.craft_room_humidity + temperature_sensor: sensor.craft_room_multi_sensor_temperature + humidity_sensor: sensor.craft_room_multi_sensor_humidity study: friendly_name: Study unique_id: study_thermal_comfort - temperature_sensor: sensor.study_temperature - humidity_sensor: sensor.study_humidity + temperature_sensor: sensor.study_multi_sensor_temperature + humidity_sensor: sensor.study_multi_sensor_humidity guest_room: friendly_name: Guest Room unique_id: guest_room_thermal_comfort From cac6f66dd0feab78c9c2ed4822b1c591b0ab7568 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sat, 3 Jun 2023 20:08:41 +0100 Subject: [PATCH 072/158] More updates --- groups.yaml | 4 ++-- packages/bin_reminder_tts.yaml | 2 +- packages/front_hall.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/groups.yaml b/groups.yaml index 061b1ab1..a3ebb41c 100644 --- a/groups.yaml +++ b/groups.yaml @@ -40,8 +40,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 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/front_hall.yaml b/packages/front_hall.yaml index 2d4c788d..8967dc10 100644 --- a/packages/front_hall.yaml +++ b/packages/front_hall.yaml @@ -13,8 +13,8 @@ automation: 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: From 71cab1a33c7caa6ce3993609c83c65bee8021ad9 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 5 Jun 2023 09:30:27 +0100 Subject: [PATCH 073/158] Add existing reset scripts --- packages/outside_motion.yaml | 6 ------ persons.yaml | 4 ++-- scripts/hue_reset.yaml | 35 ++++++++++++++++++++++++++++++++++ scripts/ikea_reset.yaml | 35 ++++++++++++++++++++++++++++++++++ scripts/silvercrest_reset.yaml | 35 ++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 scripts/hue_reset.yaml create mode 100644 scripts/ikea_reset.yaml create mode 100644 scripts/silvercrest_reset.yaml diff --git a/packages/outside_motion.yaml b/packages/outside_motion.yaml index 310bf04b..245ef2eb 100644 --- a/packages/outside_motion.yaml +++ b/packages/outside_motion.yaml @@ -6,12 +6,6 @@ automation: entity_id: - binary_sensor.outside_front_motion - binary_sensor.outside_driveway_motion - - binary_sensor.back_door_motion - - binary_sensor.front_door_motion - - binary_sensor.back_door_person_occupancy - - binary_sensor.driveway_person_occupancy - - binary_sensor.gates_person_occupancy - - binary_sensor.back_door_all_occupancy to: 'on' condition: - condition: state 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..dc4c7dd5 --- /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..e9657902 --- /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..dbb823bd --- /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 + From f151ffbb92a8a937337ed96cb03cda0480e5ae22 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 6 Jun 2023 09:01:46 +0100 Subject: [PATCH 074/158] More name changes --- automation/media_playing_night.yaml | 2 +- groups.yaml | 12 +++++------- packages/boot_room.yaml | 8 ++++---- packages/device_alerts.yaml | 12 ++++++------ packages/hallway.yaml | 12 ++++++------ packages/kitchen.yaml | 4 ++-- packages/living_room_lights.yaml | 4 ++-- 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/automation/media_playing_night.yaml b/automation/media_playing_night.yaml index 9345ad21..3ff67a35 100644 --- a/automation/media_playing_night.yaml +++ b/automation/media_playing_night.yaml @@ -8,7 +8,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/groups.yaml b/groups.yaml index a3ebb41c..7798f0a3 100644 --- a/groups.yaml +++ b/groups.yaml @@ -26,8 +26,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 @@ -50,8 +49,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 @@ -171,10 +169,10 @@ 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 diff --git a/packages/boot_room.yaml b/packages/boot_room.yaml index 87426dbf..f4958c6a 100644 --- a/packages/boot_room.yaml +++ b/packages/boot_room.yaml @@ -34,7 +34,7 @@ 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... @@ -83,7 +83,7 @@ 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... @@ -114,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 @@ -136,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/device_alerts.yaml b/packages/device_alerts.yaml index 77c7152b..c93084e6 100644 --- a/packages/device_alerts.yaml +++ b/packages/device_alerts.yaml @@ -7,12 +7,12 @@ 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.hallway_bathroom_occupancy + - binary_sensor.hallway_rooms_occupancy - binary_sensor.ensuite_motion_occupancy - - binary_sensor.living_room_motion - - binary_sensor.kitchen_motion - - binary_sensor.front_hall_motion + - 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 @@ -25,7 +25,7 @@ automation: - 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 diff --git a/packages/hallway.yaml b/packages/hallway.yaml index 1aaae99a..b93480a4 100644 --- a/packages/hallway.yaml +++ b/packages/hallway.yaml @@ -40,8 +40,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: 'on' from: 'off' condition: @@ -65,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 @@ -87,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 diff --git a/packages/kitchen.yaml b/packages/kitchen.yaml index d5b30a5f..2b9f0f33 100644 --- a/packages/kitchen.yaml +++ b/packages/kitchen.yaml @@ -12,7 +12,7 @@ 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: @@ -31,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_lights.yaml b/packages/living_room_lights.yaml index f5cc3072..ebd601db 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -83,7 +83,7 @@ automation: - alias: Living Room motion - 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 @@ -105,7 +105,7 @@ automation: - alias: 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: From 21c2488a58cd1a10ff6e7c5fdc56b3e029998bca Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 6 Jun 2023 22:38:35 +0100 Subject: [PATCH 075/158] Fix old garage sensor name --- packages/garage.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/garage.yaml b/packages/garage.yaml index 371fb6c3..002f8ab3 100644 --- a/packages/garage.yaml +++ b/packages/garage.yaml @@ -4,7 +4,7 @@ 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: @@ -14,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 From 757827c3f77808040ed1acf3ace7dffce9849cd0 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Tue, 6 Jun 2023 22:39:09 +0100 Subject: [PATCH 076/158] Fix formatting --- scripts/hue_reset.yaml | 62 +++++++++++++++++----------------- scripts/ikea_reset.yaml | 62 +++++++++++++++++----------------- scripts/silvercrest_reset.yaml | 62 +++++++++++++++++----------------- 3 files changed, 93 insertions(+), 93 deletions(-) diff --git a/scripts/hue_reset.yaml b/scripts/hue_reset.yaml index dc4c7dd5..cf74edbc 100644 --- a/scripts/hue_reset.yaml +++ b/scripts/hue_reset.yaml @@ -1,35 +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 + - 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 index e9657902..87c876b8 100644 --- a/scripts/ikea_reset.yaml +++ b/scripts/ikea_reset.yaml @@ -1,35 +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 + - 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 index dbb823bd..ae3d3bfb 100644 --- a/scripts/silvercrest_reset.yaml +++ b/scripts/silvercrest_reset.yaml @@ -1,35 +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 + - 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 - From e11bf68958bdd906ead280a371cf0518635ffc85 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 8 Jun 2023 08:42:47 +0100 Subject: [PATCH 077/158] Group name tweak --- packages/living_room_lights.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index ebd601db..d47212c7 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -74,7 +74,7 @@ automation: - service: light.toggle data_template: entity_id: - - light.dining_nook_group + - light.dining_nook - service: logbook.log data_template: name: EVENT @@ -118,7 +118,7 @@ automation: entity_id: light.living_room state: "off" - condition: state - entity_id: light.dining_nook_group + entity_id: light.dining_nook state: "off" - condition: numeric_state entity_id: sensor.average_external_light_level @@ -147,7 +147,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 %} From d878f3a28db133b63e5b6c01be22858a7f5c5a83 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 11 Jun 2023 10:10:42 +0100 Subject: [PATCH 078/158] Provide IDs for traces to be stored --- packages/living_room_lights.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/living_room_lights.yaml b/packages/living_room_lights.yaml index d47212c7..fd10ddf9 100644 --- a/packages/living_room_lights.yaml +++ b/packages/living_room_lights.yaml @@ -39,6 +39,7 @@ light: 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 +61,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 @@ -81,6 +83,7 @@ automation: 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_occupancy @@ -103,6 +106,7 @@ automation: entity_id: light.living_room, light.dining_nook_group - alias: Living Room motion + id: living_room_motion trigger: - platform: state entity_id: binary_sensor.living_room_motion_occupancy From 618ef6ce70bd8893311c54676715eed7aab8a28b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 11 Jun 2023 10:13:12 +0100 Subject: [PATCH 079/158] More IDs for traces --- automation/living_room_sofa_dim.yaml | 1 + automation/media_playing_night.yaml | 2 ++ automation/media_stopped_night.yaml | 2 ++ packages/outside_motion.yaml | 2 ++ 4 files changed, 7 insertions(+) diff --git a/automation/living_room_sofa_dim.yaml b/automation/living_room_sofa_dim.yaml index 4bc0adc9..5efdeaeb 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 diff --git a/automation/media_playing_night.yaml b/automation/media_playing_night.yaml index 3ff67a35..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 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/packages/outside_motion.yaml b/packages/outside_motion.yaml index 245ef2eb..3b2d88d1 100644 --- a/packages/outside_motion.yaml +++ b/packages/outside_motion.yaml @@ -1,6 +1,7 @@ --- automation: - alias: Outdoor lights on + id: outdoor_lights_on trigger: - platform: state entity_id: @@ -47,6 +48,7 @@ automation: entity_id: light.front_door_floodlights - alias: Outdoor lights off + id: outdoor_lights_off trigger: - platform: state entity_id: From a5d30c82ef9bf539eca890caa9ee705d27ac8f3d Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 12 Jun 2023 15:14:23 +0100 Subject: [PATCH 080/158] More name fixes --- groups.yaml | 6 +++--- packages/device_alerts.yaml | 2 +- packages/front_hall.yaml | 7 +++++-- sensors.yaml | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/groups.yaml b/groups.yaml index 7798f0a3..0e596289 100644 --- a/groups.yaml +++ b/groups.yaml @@ -174,9 +174,9 @@ hall_view: - 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: diff --git a/packages/device_alerts.yaml b/packages/device_alerts.yaml index c93084e6..f4efe05a 100644 --- a/packages/device_alerts.yaml +++ b/packages/device_alerts.yaml @@ -58,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/front_hall.yaml b/packages/front_hall.yaml index 8967dc10..9e61c6d9 100644 --- a/packages/front_hall.yaml +++ b/packages/front_hall.yaml @@ -10,6 +10,7 @@ input_number: automation: - alias: Front hall motion + id: front_hall_motion trigger: - platform: state entity_id: @@ -37,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 @@ -58,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/sensors.yaml b/sensors.yaml index 36ce4d46..dcf27333 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -83,7 +83,7 @@ - sensor.living_room_multi_sensor_temperature - sensor.master_bedroom_multi_sensor_temperature - sensor.nook_multi_sensor_temperature - - sensor.hall_multi_sensor_temperature + - sensor.hallway_multi_sensor_temperature - sensor.study_multi_sensor_temperature - sensor.bathroom_multi_sensor_temperature @@ -97,7 +97,7 @@ - sensor.living_room_multi_sensor_humidity - sensor.master_bedroom_multi_sensor_humidity - sensor.nook_multi_sensor_humidity - - sensor.hall_multi_sensor_humidity + - sensor.hallway_multi_sensor_humidity - sensor.study_multi_sensor_humidity - sensor.bathroom_multi_sensor_humidity From 43a006f2b126629e937862c6e0fcba4d3cb62588 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 18 Jun 2023 10:44:58 +0100 Subject: [PATCH 081/158] Fix yaml that still fails to be caught by CI due to the longstanding issue at https://github.com/home-assistant/core/issues/86924 --- groups.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/groups.yaml b/groups.yaml index 0e596289..91bcfcf0 100644 --- a/groups.yaml +++ b/groups.yaml @@ -175,8 +175,8 @@ hall_view: - binary_sensor.boot_room_motion_occupancy - sensor.hall_door_motion - sensor.hallway_multi_sensor_temperature - - sensor.hallway_multi_sensor__humidity - - sensor.hallway_multi_sensor__pressure + - sensor.hallway_multi_sensor_humidity + - sensor.hallway_multi_sensor_pressure - binary_sensor.boot_room_door outside: From 6e91560afaf18ed52213d43abd9010fef917473b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 10 Jul 2023 18:51:58 +0100 Subject: [PATCH 082/158] Initial automations and support --- automation/billycountry.yaml | 15 +++++++++++++++ automation/radio_caroline.yaml | 15 +++++++++++++++ input_boolean/billy_country.yaml | 2 ++ input_boolean/radio_caroline.yaml | 2 ++ 4 files changed, 34 insertions(+) create mode 100644 automation/billycountry.yaml create mode 100644 automation/radio_caroline.yaml create mode 100644 input_boolean/billy_country.yaml create mode 100644 input_boolean/radio_caroline.yaml diff --git a/automation/billycountry.yaml b/automation/billycountry.yaml new file mode 100644 index 00000000..55e48c7d --- /dev/null +++ b/automation/billycountry.yaml @@ -0,0 +1,15 @@ +alias: "Play Billy Country" +id: play_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/automation/radio_caroline.yaml b/automation/radio_caroline.yaml new file mode 100644 index 00000000..93ba140d --- /dev/null +++ b/automation/radio_caroline.yaml @@ -0,0 +1,15 @@ +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/input_boolean/billy_country.yaml b/input_boolean/billy_country.yaml new file mode 100644 index 00000000..0d784324 --- /dev/null +++ b/input_boolean/billy_country.yaml @@ -0,0 +1,2 @@ +name: Billy Country +initial: false diff --git a/input_boolean/radio_caroline.yaml b/input_boolean/radio_caroline.yaml new file mode 100644 index 00000000..3a8cbeb8 --- /dev/null +++ b/input_boolean/radio_caroline.yaml @@ -0,0 +1,2 @@ +name: Radio Caroline +initial: false From 8657db248199c3ef3cd623b4ed6261a04d2112c9 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 10 Jul 2023 22:17:17 +0100 Subject: [PATCH 083/158] Merge Radio Caroline into package --- automation/radio_caroline.yaml | 15 --------------- input_boolean/radio_caroline.yaml | 2 -- packages/radio_caroline.yaml | 22 ++++++++++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) delete mode 100644 automation/radio_caroline.yaml delete mode 100644 input_boolean/radio_caroline.yaml create mode 100644 packages/radio_caroline.yaml diff --git a/automation/radio_caroline.yaml b/automation/radio_caroline.yaml deleted file mode 100644 index 93ba140d..00000000 --- a/automation/radio_caroline.yaml +++ /dev/null @@ -1,15 +0,0 @@ -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/input_boolean/radio_caroline.yaml b/input_boolean/radio_caroline.yaml deleted file mode 100644 index 3a8cbeb8..00000000 --- a/input_boolean/radio_caroline.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: Radio Caroline -initial: false 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 From 473c753e8a1df395022fd4a792d3140e90fd8073 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 10 Jul 2023 22:20:11 +0100 Subject: [PATCH 084/158] Also merge Billy Country radio --- automation/billycountry.yaml | 15 --------------- input_boolean/billy_country.yaml | 2 -- packages/radio_billy_country.yaml | 22 ++++++++++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) delete mode 100644 automation/billycountry.yaml delete mode 100644 input_boolean/billy_country.yaml create mode 100644 packages/radio_billy_country.yaml diff --git a/automation/billycountry.yaml b/automation/billycountry.yaml deleted file mode 100644 index 55e48c7d..00000000 --- a/automation/billycountry.yaml +++ /dev/null @@ -1,15 +0,0 @@ -alias: "Play Billy Country" -id: play_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/input_boolean/billy_country.yaml b/input_boolean/billy_country.yaml deleted file mode 100644 index 0d784324..00000000 --- a/input_boolean/billy_country.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: Billy Country -initial: false 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 From 2bc283cd58f97fd907829a43f0268400bc2eaae7 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Mon, 10 Jul 2023 22:28:05 +0100 Subject: [PATCH 085/158] Merge and update ClassicFM --- automation/classicfm.yaml | 14 -------------- input_boolean/classic_fm.yaml | 2 -- packages/radio_classic_fm.yaml | 22 ++++++++++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) delete mode 100644 automation/classicfm.yaml delete mode 100644 input_boolean/classic_fm.yaml create mode 100644 packages/radio_classic_fm.yaml 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/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/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml new file mode 100644 index 00000000..5f11ce0a --- /dev/null +++ b/packages/radio_classic_fm.yaml @@ -0,0 +1,22 @@ +--- +input_boolean: + radio_classic_fm: + name: Classic FM + initial: false + +automation: + - alias: "Play Classic FM" + id: play_radio_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: "airable://radio?version=1&radioId=4888751676771790&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" + media_content_type: music + - service: homeassistant.turn_off + entity_id: input_boolean.classic_fm From 0d1c0822eaa872bb1a34da3ee3579e95fd956f53 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 13 Jul 2023 17:45:26 +0100 Subject: [PATCH 086/158] Fix Classic FM --- packages/radio_classic_fm.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml index 5f11ce0a..0818ba13 100644 --- a/packages/radio_classic_fm.yaml +++ b/packages/radio_classic_fm.yaml @@ -1,7 +1,7 @@ --- input_boolean: radio_classic_fm: - name: Classic FM + name: Radio Classic FM initial: false automation: @@ -9,7 +9,7 @@ automation: id: play_radio_classic_fm trigger: platform: state - entity_id: input_boolean.classic_fm + entity_id: input_boolean.radio_classic_fm from: 'off' to: 'on' action: @@ -19,4 +19,4 @@ automation: media_content_id: "airable://radio?version=1&radioId=4888751676771790&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" media_content_type: music - service: homeassistant.turn_off - entity_id: input_boolean.classic_fm + entity_id: input_boolean.radio_classic_fm From 634c80ebb428728b3ae1ba10096203fc691d0f5f Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 13 Jul 2023 18:04:10 +0100 Subject: [PATCH 087/158] Try alternative URL --- packages/radio_classic_fm.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml index 0818ba13..74420bed 100644 --- a/packages/radio_classic_fm.yaml +++ b/packages/radio_classic_fm.yaml @@ -16,7 +16,7 @@ automation: - service: media_player.play_media data: entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f - media_content_id: "airable://radio?version=1&radioId=4888751676771790&deviceId=a8abcca3-9d48-4a95-8c02-cb839b85d6ab" + 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 From bd125f0e379e000757a8187ee71522b09b52d3df Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 13 Jul 2023 18:09:23 +0100 Subject: [PATCH 088/158] Turn on player first, then play --- packages/radio_classic_fm.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml index 74420bed..e6ecfea8 100644 --- a/packages/radio_classic_fm.yaml +++ b/packages/radio_classic_fm.yaml @@ -13,6 +13,10 @@ automation: from: 'off' to: 'on' action: + - service: homeassistant.turn_on + entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f + - delay: + seconds: 5 - service: media_player.play_media data: entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f From a5f670c779b662bca4300c1c1e9f08a5c7d0c744 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Thu, 13 Jul 2023 18:10:29 +0100 Subject: [PATCH 089/158] Longer delay --- packages/radio_classic_fm.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/radio_classic_fm.yaml b/packages/radio_classic_fm.yaml index e6ecfea8..67d3b8ea 100644 --- a/packages/radio_classic_fm.yaml +++ b/packages/radio_classic_fm.yaml @@ -16,7 +16,7 @@ automation: - service: homeassistant.turn_on entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f - delay: - seconds: 5 + seconds: 15 - service: media_player.play_media data: entity_id: media_player.openhome_uuid_4c494e4e_0026_0f22_3637_01475230013f From 178b5155b3e7b04dd0ee8f2e378b9c7a77e1876f Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 14 Jul 2023 09:24:28 +0100 Subject: [PATCH 090/158] Use GH build actions for ESPHome --- .github/workflows/esphome-parallel.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/esphome-parallel.yaml b/.github/workflows/esphome-parallel.yaml index c75839a5..7a80cfd5 100644 --- a/.github/workflows/esphome-parallel.yaml +++ b/.github/workflows/esphome-parallel.yaml @@ -90,8 +90,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-actions@v1 + with: + version: latest + yaml_file: ${{matrix.file}} # This is used by branch protections final: From 42bce0d57903e4bbaad70425f23681dd2dc09811 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 14 Jul 2023 12:29:38 +0100 Subject: [PATCH 091/158] Use other versions --- .github/workflows/esphome-parallel.yaml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/esphome-parallel.yaml b/.github/workflows/esphome-parallel.yaml index 7a80cfd5..e5b8f9da 100644 --- a/.github/workflows/esphome-parallel.yaml +++ b/.github/workflows/esphome-parallel.yaml @@ -46,8 +46,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-actions@v1 + with: + version: stable + yaml_file: ${{matrix.file}} loop-beta: name: Test ESPHome Beta firmware @@ -68,8 +71,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-actions@v1 + with: + version: beta + yaml_file: ${{matrix.file}} loop-dev: name: Test ESPHome Dev firmware @@ -93,7 +99,7 @@ jobs: - name: Compile all ESPHome ${{matrix.file}} uses: esphome/build-actions@v1 with: - version: latest + version: dev yaml_file: ${{matrix.file}} # This is used by branch protections From 834927becf44b13963919afda48f40414487b5ad Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Fri, 14 Jul 2023 12:32:45 +0100 Subject: [PATCH 092/158] Also trigger on workflow changes --- .github/workflows/esphome-dummy.yaml | 2 ++ .github/workflows/esphome-parallel.yaml | 2 ++ 2 files changed, 4 insertions(+) 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 e5b8f9da..648efeb4 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 * * * From 13717688a716df6c6cabde7e53101c394c137c86 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:05:13 +0100 Subject: [PATCH 093/158] Better thermostat 1.2.2 --- .../better_thermostat/__init__.py | 5 +- .../better_thermostat/adapters/deconz.py | 3 +- .../better_thermostat/adapters/generic.py | 6 +- .../better_thermostat/adapters/mqtt.py | 9 +- .../better_thermostat/adapters/tado.py | 3 +- .../better_thermostat/climate.py | 114 ++++++++--- .../better_thermostat/config_flow.py | 3 + custom_components/better_thermostat/const.py | 4 + .../better_thermostat/events/trv.py | 75 +++---- .../better_thermostat/events/window.py | 2 +- .../better_thermostat/manifest.json | 4 +- .../model_fixes/TV02-Zigbee.py | 12 +- .../better_thermostat/strings.json | 173 ++++++++-------- .../better_thermostat/translations/da.json | 161 ++++++++------- .../better_thermostat/translations/de.json | 189 +++++++++-------- .../better_thermostat/translations/en.json | 191 ++++++++++-------- .../better_thermostat/translations/fr.json | 169 +++++++++------- .../better_thermostat/translations/pl.json | 175 ++++++++-------- .../better_thermostat/utils/controlling.py | 39 +++- .../better_thermostat/utils/helpers.py | 24 +++ .../better_thermostat/utils/weather.py | 9 +- 21 files changed, 790 insertions(+), 580 deletions(-) diff --git a/custom_components/better_thermostat/__init__.py b/custom_components/better_thermostat/__init__.py index 23f9bf88..a1acec04 100644 --- a/custom_components/better_thermostat/__init__.py +++ b/custom_components/better_thermostat/__init__.py @@ -3,6 +3,7 @@ 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, @@ -13,17 +14,19 @@ ) _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 diff --git a/custom_components/better_thermostat/adapters/deconz.py b/custom_components/better_thermostat/adapters/deconz.py index 696d159c..c6055439 100644 --- a/custom_components/better_thermostat/adapters/deconz.py +++ b/custom_components/better_thermostat/adapters/deconz.py @@ -56,8 +56,7 @@ async def set_offset(self, entity_id, offset): "configure", {"entity": entity_id, "field": "/config", "data": {"offset": offset}}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) self.real_trvs[entity_id]["last_calibration"] = offset diff --git a/custom_components/better_thermostat/adapters/generic.py b/custom_components/better_thermostat/adapters/generic.py index ded8bd61..fb7a48e6 100644 --- a/custom_components/better_thermostat/adapters/generic.py +++ b/custom_components/better_thermostat/adapters/generic.py @@ -39,8 +39,7 @@ async def set_temperature(self, entity_id, temperature): "set_temperature", {"entity_id": entity_id, "temperature": temperature}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) @@ -51,8 +50,7 @@ async def set_hvac_mode(self, entity_id, hvac_mode): "set_hvac_mode", {"entity_id": entity_id, "hvac_mode": hvac_mode}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) diff --git a/custom_components/better_thermostat/adapters/mqtt.py b/custom_components/better_thermostat/adapters/mqtt.py index a648a0be..0dc59f11 100644 --- a/custom_components/better_thermostat/adapters/mqtt.py +++ b/custom_components/better_thermostat/adapters/mqtt.py @@ -64,8 +64,7 @@ async def init(self, entity_id): "set_preset_mode", {"entity_id": entity_id, "preset_mode": "manual"}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) @@ -144,8 +143,7 @@ async def set_offset(self, entity_id, offset): "value": offset, }, blocking=True, - limit=None, - context=self._context, + context=self.context, ) self.real_trvs[entity_id]["last_calibration"] = offset if ( @@ -171,6 +169,5 @@ async def set_valve(self, entity_id, valve): "value": valve, }, blocking=True, - limit=None, - context=self._context, + context=self.context, ) diff --git a/custom_components/better_thermostat/adapters/tado.py b/custom_components/better_thermostat/adapters/tado.py index eb2505e4..ea13b2f8 100644 --- a/custom_components/better_thermostat/adapters/tado.py +++ b/custom_components/better_thermostat/adapters/tado.py @@ -59,8 +59,7 @@ async def set_offset(self, entity_id, offset): "set_climate_temperature_offset", {"entity_id": entity_id, "offset": offset}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) self.real_trvs[entity_id]["last_calibration"] = offset diff --git a/custom_components/better_thermostat/climate.py b/custom_components/better_thermostat/climate.py index aa9fa801..4d5cee75 100644 --- a/custom_components/better_thermostat/climate.py +++ b/custom_components/better_thermostat/climate.py @@ -7,6 +7,8 @@ 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, @@ -18,10 +20,10 @@ from .utils.model_quirks import load_model_quirks -from .utils.helpers import convert_to_float +from .utils.helpers import convert_to_float, find_battery_entity from homeassistant.helpers import entity_platform -from homeassistant.core import callback, CoreState - +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, @@ -46,9 +48,10 @@ from homeassistant.components.group.util import reduce_attribute -from . import DOMAIN 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, @@ -79,12 +82,17 @@ 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): + 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() @@ -96,7 +104,7 @@ async def async_service_handler(self, data): platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_TEMP_TARGET_TEMPERATURE, - BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, + BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA, # type: ignore async_service_handler, [ BetterThermostatEntityFeature.TARGET_TEMPERATURE, @@ -245,7 +253,7 @@ def __init__( self.last_window_state = None self._last_call_for_heat = None self._available = False - self._context = None + self.context = None self.attr_hvac_action = None self.old_attr_hvac_action = None self.heating_start_temp = None @@ -255,6 +263,9 @@ def __init__( 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) @@ -330,6 +341,7 @@ def _async_startup(*_): _ : All parameters are piped. """ + self.context = Context() loop = asyncio.get_event_loop() loop.create_task(self.startup()) @@ -339,14 +351,21 @@ def _async_startup(*_): 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() @@ -354,15 +373,19 @@ async def _trigger_time(self, event=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( @@ -373,10 +396,12 @@ async def _trigger_humidity_change(self, event): self.async_write_ha_state() async def _trigger_trv_change(self, event): - if self._async_unsub_state_changed is None: + _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 @@ -384,8 +409,10 @@ async def _trigger_trv_change(self, event): 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 @@ -405,25 +432,39 @@ async def startup(self): self.version, ) sensor_state = self.hass.states.get(self.sensor_entity_id) - 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 - for trv in self.real_trvs.keys(): - trv_state = self.hass.states.get(trv) - if trv_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + if sensor_state is not None: + if sensor_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...", + "better_thermostat %s: waiting for sensor entity with id '%s' to become fully available...", self.name, - trv, + 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, @@ -492,16 +533,20 @@ async def startup(self): 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 @@ -655,6 +700,7 @@ async def startup(self): 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( @@ -725,13 +771,27 @@ async def startup(self): 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) @@ -764,7 +824,7 @@ async def startup(self): ) _LOGGER.info("better_thermostat %s: startup completed.", self.name) self.async_write_ha_state() - await self.async_update_ha_state() + await self.async_update_ha_state(force_refresh=True) break async def calculate_heating_power(self): @@ -849,6 +909,8 @@ def extra_state_attributes(self): 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 diff --git a/custom_components/better_thermostat/config_flow.py b/custom_components/better_thermostat/config_flow.py index d4982256..8142a02b 100644 --- a/custom_components/better_thermostat/config_flow.py +++ b/custom_components/better_thermostat/config_flow.py @@ -76,6 +76,9 @@ selector.SelectOptionDict( value=CalibrationMode.HEATING_POWER_CALIBRATION, label="AI Time Based" ), + selector.SelectOptionDict( + value=CalibrationMode.NO_CALIBRATION, label="No Calibration" + ), ], mode=selector.SelectSelectorMode.DROPDOWN, ) diff --git a/custom_components/better_thermostat/const.py b/custom_components/better_thermostat/const.py index 928c19a5..073490a8 100644 --- a/custom_components/better_thermostat/const.py +++ b/custom_components/better_thermostat/const.py @@ -44,6 +44,7 @@ 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" @@ -60,6 +61,8 @@ 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" @@ -93,3 +96,4 @@ class CalibrationMode(StrEnum): DEFAULT = "default" FIX_CALIBRATION = "fix_calibration" HEATING_POWER_CALIBRATION = "heating_power_calibration" + NO_CALIBRATION = "no_calibration" diff --git a/custom_components/better_thermostat/events/trv.py b/custom_components/better_thermostat/events/trv.py index 2b6d21d1..e92bc5e3 100644 --- a/custom_components/better_thermostat/events/trv.py +++ b/custom_components/better_thermostat/events/trv.py @@ -1,3 +1,4 @@ +import asyncio from datetime import datetime import logging from typing import Union @@ -6,10 +7,10 @@ from homeassistant.components.climate.const import ( HVACMode, ATTR_HVAC_ACTION, - HVACAction, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, ) from homeassistant.core import State, callback -from homeassistant.components.group.util import find_state_attributes from ..utils.helpers import ( calculate_local_setpoint_delta, calculate_setpoint_override, @@ -29,7 +30,7 @@ async def trigger_trv_change(self, event): return if self.control_queue_task is None: return - update_hvac_action(self) + asyncio.create_task(update_hvac_action(self)) _main_change = False old_state = event.data.get("old_state") new_state = event.data.get("new_state") @@ -46,9 +47,13 @@ async def trigger_trv_change(self, event): 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 - # if new_state == old_state: - # return + # 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") @@ -190,45 +195,31 @@ async def trigger_trv_change(self, event): return -def update_hvac_action(self): - """Update hvac action.""" - # return the most common action if it is not off - states = [ - state - for entity_id in self.real_trvs - if (state := self.hass.states.get(entity_id)) is not None - ] - - # check if trv has pi_heating_demand - pi_heating_demands = list(find_state_attributes(states, "pi_heating_demand")) - if pi_heating_demands: - pi_heating_demand = max(pi_heating_demands) - if pi_heating_demand > 1: - self.attr_hvac_action = HVACAction.HEATING - self.async_write_ha_state() - return - else: - self.attr_hvac_action = HVACAction.IDLE - self.async_write_ha_state() - return - - hvac_actions = list(find_state_attributes(states, ATTR_HVAC_ACTION)) - if not hvac_actions: - self.attr_hvac_action = None - self.async_write_ha_state() +async def update_hvac_action(self): + """Update the hvac action.""" + if self.startup_running or self.control_queue_task is None: return - # return action off if all are off - if all(a == HVACAction.OFF for a in hvac_actions): - self.attr_hvac_action = HVACAction.OFF - # else check if is heating - elif self.bt_target_temp > self.cur_temp: - self.attr_hvac_action = HVACAction.HEATING - else: - self.attr_hvac_action = HVACAction.IDLE - - self.async_write_ha_state() - 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: diff --git a/custom_components/better_thermostat/events/window.py b/custom_components/better_thermostat/events/window.py index 5c2b9650..b88c7aef 100644 --- a/custom_components/better_thermostat/events/window.py +++ b/custom_components/better_thermostat/events/window.py @@ -32,7 +32,7 @@ async def trigger_window_change(self, event) -> None: old_window_open = self.window_open - if new_state in ("on", "unknown"): + if new_state in ("on", "unknown", "unavailable"): new_window_open = True if new_state == "unknown": _LOGGER.warning( diff --git a/custom_components/better_thermostat/manifest.json b/custom_components/better_thermostat/manifest.json index b81bb160..a7ca72bd 100644 --- a/custom_components/better_thermostat/manifest.json +++ b/custom_components/better_thermostat/manifest.json @@ -16,5 +16,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues", "requirements": [], - "version": "1.0.2" -} + "version": "1.2.2" +} \ No newline at end of file diff --git a/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py index 5107bf0e..6eba1577 100644 --- a/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py +++ b/custom_components/better_thermostat/model_fixes/TV02-Zigbee.py @@ -32,8 +32,7 @@ async def override_set_hvac_mode(self, entity_id, hvac_mode): "set_hvac_mode", {"entity_id": entity_id, "hvac_mode": hvac_mode}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) model = self.real_trvs[entity_id]["model"] if model == "TV02-Zigbee" and hvac_mode != HVACMode.OFF: @@ -45,8 +44,7 @@ async def override_set_hvac_mode(self, entity_id, hvac_mode): "set_preset_mode", {"entity_id": entity_id, "preset_mode": "manual"}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) return True @@ -75,8 +73,7 @@ async def override_set_temperature(self, entity_id, temperature): "set_preset_mode", {"entity_id": entity_id, "preset_mode": "manual"}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) await self.hass.services.async_call( @@ -84,7 +81,6 @@ async def override_set_temperature(self, entity_id, temperature): "set_temperature", {"entity_id": entity_id, "temperature": temperature}, blocking=True, - limit=None, - context=self._context, + context=self.context, ) return True diff --git a/custom_components/better_thermostat/strings.json b/custom_components/better_thermostat/strings.json index a9270339..65cf63ef 100644 --- a/custom_components/better_thermostat/strings.json +++ b/custom_components/better_thermostat/strings.json @@ -1,91 +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 themostat", - "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." - } + "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" }, - "confirm": { - "title": "Confirm adding a Better Thermostat", - "description": "You are about to add `{name}` to Home Assistant.\nWith {trv} as the real Thermostat" + "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." } }, - "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." + "confirm":{ + "title":"Confirm adding a Better Thermostat", + "description":"You are about to add `{name}` to Home Assistant.\nWith {trv} as the real Thermostat" + } }, - "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" + "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": { - "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", - "calibration_round": "Should the calibration be rounded to the nearest", - "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" - } + "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" }, - "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." - } + "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 index 4cda3150..46f0fc0d 100644 --- a/custom_components/better_thermostat/translations/da.json +++ b/custom_components/better_thermostat/translations/da.json @@ -1,80 +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": { - "calibration_round": "Skal kalibreringen afrundes til nærmeste", - "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - }, - "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": { - "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" + "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?" }, - "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" + "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" }, - "options": { + "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": { - "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", - "calibration_round": "Skal kalibreringen afrundes til nærmeste", - "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": { - "calibration_round": "Skal kalibreringen afrundes til nærmeste", - "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - } + "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 index 80c26a61..f5af602b 100644 --- a/custom_components/better_thermostat/translations/de.json +++ b/custom_components/better_thermostat/translations/de.json @@ -1,88 +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": { - "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." - } + "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." }, - "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." - } - }, - "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)" - } - } - } + "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 index a3362a36..73b055da 100644 --- a/custom_components/better_thermostat/translations/en.json +++ b/custom_components/better_thermostat/translations/en.json @@ -1,92 +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." - }, - "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" - } + "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" }, - "options": { - "step": { - "user": { - "description": "Update your Better Thermostat settings", - "data": { - "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", - "calibration_round": "Should the calibration be rounded to the nearest", - "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." - } - } - } + "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 index 36f6656c..64d27a24 100644 --- a/custom_components/better_thermostat/translations/fr.json +++ b/custom_components/better_thermostat/translations/fr.json @@ -1,80 +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": { - "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", - "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - }, - "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": "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" - } + "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 ?" }, - "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", - "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", - "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": { - "calibration_round": "L'étalonnage doit-il être arrondi à la valeur la plus proche", - "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", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - } - } + "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 index 03c27cd1..3816c390 100644 --- a/custom_components/better_thermostat/translations/pl.json +++ b/custom_components/better_thermostat/translations/pl.json @@ -1,80 +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": { - "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", - "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ć", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - }, - "confirm": { - "title": "Potwierdź dodanie Better Thermostat", - "description": "Zamierzasz dodać `{name}` do Home Assistant.\nUżywając {trv} jako termostatu" - } - }, - "error": { - "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ą", - "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", - "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": { - "calibration_round": "Kalibrację należy zaokrąglić do najbliższej wartości", - "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ć", - "fix_calibration": "If your TRV has trouble with not recheating the target temperature or overheating, this option can help" - } - } - } - } +{ + "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/controlling.py b/custom_components/better_thermostat/utils/controlling.py index dfed705e..1d606537 100644 --- a/custom_components/better_thermostat/utils/controlling.py +++ b/custom_components/better_thermostat/utils/controlling.py @@ -109,8 +109,31 @@ async def control_trv(self, heater_entity_id=None): 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: + 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}" ) @@ -162,7 +185,11 @@ async def control_trv(self, heater_entity_id=None): self.real_trvs[heater_entity_id]["calibration_received"] = False # set new target temperature - if _temperature is not None and _new_hvac_mode != HVACMode.OFF: + 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( @@ -244,10 +271,10 @@ async def check_target_temperature(self, heater_entity_id=None): self.name, "check_target_temperature()", ) - - _LOGGER.debug( - f"better_thermostat {self.name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_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 diff --git a/custom_components/better_thermostat/utils/helpers.py b/custom_components/better_thermostat/utils/helpers.py index 8fac4b59..51b8e6dd 100644 --- a/custom_components/better_thermostat/utils/helpers.py +++ b/custom_components/better_thermostat/utils/helpers.py @@ -459,6 +459,8 @@ async def find_valve_entity(self, entity_id): """ 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 ) @@ -478,6 +480,26 @@ async def find_valve_entity(self, 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. @@ -499,6 +521,8 @@ async def find_local_calibration_entity(self, entity_id): """ 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 ) diff --git a/custom_components/better_thermostat/utils/weather.py b/custom_components/better_thermostat/utils/weather.py index f0c3924e..f5ec0de1 100644 --- a/custom_components/better_thermostat/utils/weather.py +++ b/custom_components/better_thermostat/utils/weather.py @@ -38,6 +38,7 @@ def check_weather(self) -> bool: 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): @@ -50,10 +51,11 @@ def check_weather(self) -> bool: else: _call_for_heat_outdoor = self.last_avg_outdoor_temp < self.off_temperature - self.call_for_heat = _call_for_heat_weather or _call_for_heat_outdoor + 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 @@ -189,6 +191,11 @@ async def check_ambient_air_temperature(self): 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 From 37755ee33d9a8c6817fd01604a9af1c8a8166c28 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:27:36 +0100 Subject: [PATCH 094/158] Octopus energy v7.5.2 --- custom_components/octopus_energy/__init__.py | 182 ++--- .../octopus_energy/api_client.py | 632 ++++++++++++++---- .../octopus_energy/binary_sensor.py | 51 +- .../octopus_energy/config_flow.py | 24 +- custom_components/octopus_energy/const.py | 10 + .../octopus_energy/coordinators/__init__.py | 79 +++ .../octopus_energy/coordinators/account.py | 70 ++ .../coordinators/current_consumption.py | 78 +++ .../coordinators/electricity_rates.py | 91 +++ .../octopus_energy/coordinators/gas_rates.py | 56 ++ .../coordinators/intelligent_dispatches.py | 94 +++ .../coordinators/intelligent_settings.py | 78 +++ .../previous_consumption_and_rates.py | 135 ++++ .../coordinators/saving_sessions.py | 50 ++ .../octopus_energy/electricity/__init__.py | 136 ++++ .../octopus_energy/electricity/base.py | 42 ++ .../electricity/current_consumption.py | 93 +++ .../electricity/current_demand.py | 90 +++ .../electricity/current_rate.py | 122 ++++ .../octopus_energy/electricity/next_rate.py | 119 ++++ .../previous_accumulative_consumption.py | 155 +++++ ...vious_accumulative_consumption_off_peak.py | 131 ++++ .../previous_accumulative_consumption_peak.py | 131 ++++ .../electricity/previous_accumulative_cost.py | 156 +++++ .../previous_accumulative_cost_off_peak.py | 128 ++++ .../previous_accumulative_cost_override.py | 168 +++++ ...vious_accumulative_cost_override_tariff.py | 111 +++ .../previous_accumulative_cost_peak.py | 128 ++++ .../electricity/previous_rate.py | 119 ++++ .../electricity/standing_charge.py | 100 +++ .../octopus_energy/gas/__init__.py | 99 +++ custom_components/octopus_energy/gas/base.py | 37 + .../octopus_energy/gas/current_consumption.py | 92 +++ .../octopus_energy/gas/current_rate.py | 114 ++++ .../gas/previous_accumulative_consumption.py | 160 +++++ .../previous_accumulative_consumption_kwh.py | 158 +++++ .../gas/previous_accumulative_cost.py | 161 +++++ .../previous_accumulative_cost_override.py | 169 +++++ ...vious_accumulative_cost_override_tariff.py | 106 +++ .../octopus_energy/gas/standing_charge.py | 106 +++ .../octopus_energy/intelligent/__init__.py | 119 ++++ .../octopus_energy/intelligent/base.py | 19 + .../octopus_energy/intelligent/bump_charge.py | 80 +++ .../intelligent/charge_limit.py | 88 +++ .../octopus_energy/intelligent/dispatching.py | 92 +++ .../octopus_energy/intelligent/ready_time.py | 77 +++ .../intelligent/smart_charge.py | 79 +++ .../octopus_energy/manifest.json | 5 +- custom_components/octopus_energy/number.py | 55 ++ .../saving_sessions/__init__.py | 28 + .../octopus_energy/saving_sessions/points.py | 78 +++ .../saving_sessions/saving_sessions.py | 105 +++ custom_components/octopus_energy/sensor.py | 250 +++---- .../octopus_energy/statistics/__init__.py | 181 +++++ .../octopus_energy/statistics/consumption.py | 78 +++ .../octopus_energy/statistics/cost.py | 78 +++ custom_components/octopus_energy/switch.py | 59 ++ .../octopus_energy/target_rates/__init__.py | 280 ++++++++ .../target_rates/target_rate.py | 252 +++++++ custom_components/octopus_energy/text.py | 77 +++ custom_components/octopus_energy/time.py | 55 ++ .../octopus_energy/translations/en.json | 18 +- .../octopus_energy/utils/__init__.py | 111 +-- .../octopus_energy/utils/tariff_check.py | 39 ++ 64 files changed, 6529 insertions(+), 535 deletions(-) create mode 100644 custom_components/octopus_energy/coordinators/__init__.py create mode 100644 custom_components/octopus_energy/coordinators/account.py create mode 100644 custom_components/octopus_energy/coordinators/current_consumption.py create mode 100644 custom_components/octopus_energy/coordinators/electricity_rates.py create mode 100644 custom_components/octopus_energy/coordinators/gas_rates.py create mode 100644 custom_components/octopus_energy/coordinators/intelligent_dispatches.py create mode 100644 custom_components/octopus_energy/coordinators/intelligent_settings.py create mode 100644 custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py create mode 100644 custom_components/octopus_energy/coordinators/saving_sessions.py create mode 100644 custom_components/octopus_energy/electricity/__init__.py create mode 100644 custom_components/octopus_energy/electricity/base.py create mode 100644 custom_components/octopus_energy/electricity/current_consumption.py create mode 100644 custom_components/octopus_energy/electricity/current_demand.py create mode 100644 custom_components/octopus_energy/electricity/current_rate.py create mode 100644 custom_components/octopus_energy/electricity/next_rate.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_consumption.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_cost.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py create mode 100644 custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py create mode 100644 custom_components/octopus_energy/electricity/previous_rate.py create mode 100644 custom_components/octopus_energy/electricity/standing_charge.py create mode 100644 custom_components/octopus_energy/gas/__init__.py create mode 100644 custom_components/octopus_energy/gas/base.py create mode 100644 custom_components/octopus_energy/gas/current_consumption.py create mode 100644 custom_components/octopus_energy/gas/current_rate.py create mode 100644 custom_components/octopus_energy/gas/previous_accumulative_consumption.py create mode 100644 custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py create mode 100644 custom_components/octopus_energy/gas/previous_accumulative_cost.py create mode 100644 custom_components/octopus_energy/gas/previous_accumulative_cost_override.py create mode 100644 custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py create mode 100644 custom_components/octopus_energy/gas/standing_charge.py create mode 100644 custom_components/octopus_energy/intelligent/__init__.py create mode 100644 custom_components/octopus_energy/intelligent/base.py create mode 100644 custom_components/octopus_energy/intelligent/bump_charge.py create mode 100644 custom_components/octopus_energy/intelligent/charge_limit.py create mode 100644 custom_components/octopus_energy/intelligent/dispatching.py create mode 100644 custom_components/octopus_energy/intelligent/ready_time.py create mode 100644 custom_components/octopus_energy/intelligent/smart_charge.py create mode 100644 custom_components/octopus_energy/number.py create mode 100644 custom_components/octopus_energy/saving_sessions/__init__.py create mode 100644 custom_components/octopus_energy/saving_sessions/points.py create mode 100644 custom_components/octopus_energy/saving_sessions/saving_sessions.py create mode 100644 custom_components/octopus_energy/statistics/__init__.py create mode 100644 custom_components/octopus_energy/statistics/consumption.py create mode 100644 custom_components/octopus_energy/statistics/cost.py create mode 100644 custom_components/octopus_energy/switch.py create mode 100644 custom_components/octopus_energy/target_rates/__init__.py create mode 100644 custom_components/octopus_energy/target_rates/target_rate.py create mode 100644 custom_components/octopus_energy/text.py create mode 100644 custom_components/octopus_energy/time.py create mode 100644 custom_components/octopus_energy/utils/tariff_check.py diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 5671fcdc..f964cc29 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -1,14 +1,14 @@ import logging -from datetime import timedelta import asyncio +from datetime import timedelta -from homeassistant.util.dt import (now, as_utc) from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator -) -from homeassistant.helpers import issue_registry as ir +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, @@ -22,23 +22,16 @@ DATA_CLIENT, DATA_ELECTRICITY_RATES_COORDINATOR, - DATA_RATES, DATA_ACCOUNT_ID, - DATA_ACCOUNT, - DATA_SAVING_SESSIONS, - DATA_SAVING_SESSIONS_COORDINATOR + DATA_ACCOUNT ) from .api_client import OctopusEnergyApiClient -from .utils import ( - get_active_tariff_code -) - -from .utils.check_tariff import (async_check_valid_tariff) - _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, {}) @@ -59,6 +52,22 @@ async def async_setup_entry(hass, entry): 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 @@ -72,45 +81,6 @@ async def async_setup_entry(hass, entry): return True -async def async_get_current_electricity_agreement_tariff_codes(hass, client: OctopusEnergyApiClient, account_id: str): - account_info = None - try: - account_info = await client.async_get_account(account_id) - except: - # count exceptions as failure to retrieve account - _LOGGER.debug('Failed to retrieve account') - - 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: - ir.async_delete_issue(hass, DOMAIN, f"account_not_found_{account_id}") - - tariff_codes = {} - current = now() - 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 != None: - key = (point["mpan"], is_smart_meter) - if key not in tariff_codes: - tariff_codes[(point["mpan"], is_smart_meter)] = active_tariff_code - await async_check_valid_tariff(hass, client, active_tariff_code, True) - - return tariff_codes - async def async_setup_dependencies(hass, config): """Setup the coordinator and api client which will be shared by various entities""" @@ -129,84 +99,19 @@ async def async_setup_dependencies(hass, config): hass.data[DOMAIN][DATA_CLIENT] = client hass.data[DOMAIN][DATA_ACCOUNT_ID] = config[CONFIG_MAIN_ACCOUNT_ID] - setup_rates_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - - setup_saving_sessions_coordinators(hass) - account_info = await client.async_get_account(config[CONFIG_MAIN_ACCOUNT_ID]) hass.data[DOMAIN][DATA_ACCOUNT] = account_info -def setup_rates_coordinator(hass, account_id: str): - # Reset data rates as we might have new information - hass.data[DOMAIN][DATA_RATES] = [] + await async_setup_account_info_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - if DATA_ELECTRICITY_RATES_COORDINATOR in hass.data[DOMAIN]: - _LOGGER.info("Rates coordinator has already been configured, so skipping") - return + await async_setup_intelligent_dispatches_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) + + await async_setup_intelligent_settings_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - async def async_update_electricity_rates_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_RATES not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][DATA_RATES]) == 0): - - tariff_codes = await async_get_current_electricity_agreement_tariff_codes(hass, client, account_id) - _LOGGER.debug(f'tariff_codes: {tariff_codes}') - - period_from = as_utc(current.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 = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) - if new_rates != None: - rates[key] = new_rates - elif (DATA_RATES in hass.data[DOMAIN] and key in hass.data[DOMAIN][DATA_RATES]): - _LOGGER.debug(f"Failed to retrieve new rates for {tariff_code}, so using cached rates") - rates[key] = hass.data[DOMAIN][DATA_RATES][key] - - hass.data[DOMAIN][DATA_RATES] = rates - - return hass.data[DOMAIN][DATA_RATES] - - hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( - hass, - _LOGGER, - name="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), - ) - -def setup_saving_sessions_coordinators(hass): - if DATA_SAVING_SESSIONS_COORDINATOR in hass.data[DOMAIN]: - return - - 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: - savings = await client.async_get_saving_sessions(hass.data[DOMAIN][DATA_ACCOUNT_ID]) - - hass.data[DOMAIN][DATA_SAVING_SESSIONS] = savings - - 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 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.""" @@ -214,15 +119,26 @@ async def options_update_listener(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" + + unload_ok = False if CONFIG_MAIN_API_KEY in entry.data: - target_domain = "sensor" + 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: - target_domain = "binary_sensor" - - unload_ok = all( + unload_ok = all( await asyncio.gather( - *[hass.config_entries.async_forward_entry_unload(entry, target_domain)] + *[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 index 5b00aa82..50d8c110 100644 --- a/custom_components/octopus_energy/api_client.py +++ b/custom_components/octopus_energy/api_client.py @@ -1,13 +1,12 @@ import logging import json import aiohttp -from datetime import (timedelta) +from datetime import (datetime, timedelta, time) + from homeassistant.util.dt import (as_utc, now, as_local, parse_datetime) from .utils import ( get_tariff_parts, - get_valid_from, - rates_to_thirty_minute_increments ) _LOGGER = logging.getLogger(__name__) @@ -27,6 +26,7 @@ makeAndType serialNumber makeAndType + meterType smartExportElectricityMeter {{ deviceId manufacturer @@ -75,6 +75,7 @@ serialNumber consumptionUnits modelName + mechanism smartGasMeter {{ deviceId manufacturer @@ -126,11 +127,173 @@ }} }}''' +intelligent_dispatches_query = '''query {{ + plannedDispatches(accountNumber: "{account_id}") {{ + startDt + endDt + meta {{ + source + }} + }} + completedDispatches(accountNumber: "{account_id}") {{ + startDt + endDt + meta {{ + source + }} + }} +}}''' + +intelligent_device_query = '''query {{ + registeredKrakenflexDevice(accountNumber: "{account_id}") {{ + krakenflexDeviceId + vehicleMake + vehicleModel + chargePointMake + chargePointModel + }} +}}''' + +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 == None): + if (api_key is None): raise Exception('API KEY is not set') self._api_key = api_key @@ -153,7 +316,7 @@ async def async_refresh_token(self): 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) + 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 @@ -175,7 +338,7 @@ async def async_get_account(self, account_id): 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) + account_response_body = await self.__async_read_response__(account_response, url) _LOGGER.debug(f'account: {account_response_body}') @@ -189,7 +352,7 @@ async def async_get_account(self, account_id): "meters": list(map(lambda m: { "serial_number": m["serialNumber"], "is_export": m["smartExportElectricityMeter"] is not None, - "is_smart_meter": m["smartImportElectricityMeter"] is not None or 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 @@ -231,7 +394,7 @@ async def async_get_account(self, account_id): "meters": list(map(lambda m: { "serial_number": m["serialNumber"], "consumption_units": m["consumptionUnits"], - "is_smart_meter": m["smartGasMeter"] is not None, + "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 @@ -278,7 +441,7 @@ async def async_get_saving_sessions(self, account_id): 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) + response_body = await self.__async_read_response__(account_response, url) if (response_body is not None and "data" in response_body): return { @@ -303,7 +466,7 @@ async def async_get_smart_meter_consumption(self, device_id, period_from, period payload = { "query": live_consumption_query.format(device_id=device_id, period_from=period_from, period_to=period_to) } 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) + 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: { @@ -323,15 +486,14 @@ async def async_get_electricity_standard_rates(self, product_code, tariff_code, 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: - try: - data = await self.__async_read_response(response, url) - if data == None: + data = await self.__async_read_response__(response, url) + if data is None: + if await self.__async_is_tracker_tariff_or_product__(tariff_code): return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) + return None + else: results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._electricity_price_cap) - except: - _LOGGER.error(f'Failed to extract standard rates: {url}') - raise return results @@ -342,35 +504,30 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code, 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: - try: - data = await self.__async_read_response(response, url) - if data == None: + data = await self.__async_read_response__(response, url) + if data is None: + if await self.__async_is_tracker_tariff_or_product__(tariff_code): return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) - + + 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) - except: - _LOGGER.error(f'Failed to extract day rates: {url}') - raise 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: - try: - data = await self.__async_read_response(response, url) - if data == 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) - except: - _LOGGER.error(f'Failed to extract night rates: {url}') - raise + 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) @@ -384,11 +541,11 @@ async def async_get_electricity_rates(self, tariff_code, is_smart_meter, period_ if tariff_parts is None: return None - product_code = tariff_parts["product_code"] + product_code = tariff_parts.product_code - if (self.__async_is_tracker_tariff(tariff_code)): + if (self.__is_tracker_tariff__(tariff_code)): return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._electricity_price_cap) - elif (tariff_parts["rate"].startswith("1")): + elif (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) @@ -400,7 +557,7 @@ async def async_get_electricity_consumption(self, mpan, serial_number, period_fr 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) + data = await self.__async_read_response__(response, url) if (data is not None and "results" in data): data = data["results"] results = [] @@ -423,9 +580,9 @@ async def async_get_gas_rates(self, tariff_code, period_from, period_to): if tariff_parts is None: return None - product_code = tariff_parts["product_code"] + product_code = tariff_parts.product_code - if (self.__async_is_tracker_tariff(tariff_code)): + if (self.__is_tracker_tariff__(tariff_code)): return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._gas_price_cap) results = [] @@ -433,15 +590,14 @@ async def async_get_gas_rates(self, tariff_code, period_from, period_to): 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: - try: - data = await self.__async_read_response(response, url) - if data == None: + data = await self.__async_read_response__(response, url) + if data is None: + if await self.__async_is_tracker_tariff_or_product__(tariff_code): return await self.__async_get_tracker_rates__(tariff_code, period_from, period_to, self._gas_price_cap) + return None + else: results = rates_to_thirty_minute_increments(data, period_from, period_to, tariff_code, self._gas_price_cap) - except: - _LOGGER.error(f'Failed to extract standard gas rates: {url}') - raise return results @@ -451,7 +607,7 @@ async def async_get_gas_consumption(self, mprn, serial_number, period_from, peri 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) + data = await self.__async_read_response__(response, url) if (data is not None and "results" in data): data = data["results"] results = [] @@ -474,9 +630,7 @@ async def async_get_product(self, product_code): 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) - - return None + 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""" @@ -484,9 +638,9 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from, if tariff_parts is None: return None - product_code = tariff_parts["product_code"] + product_code = tariff_parts.product_code - if self.__async_is_tracker_tariff(tariff_code): + if self.__is_tracker_tariff__(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) result = None @@ -494,18 +648,14 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from, 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: - try: - data = await self.__async_read_response(response, url) - if data is None: + data = await self.__async_read_response__(response, url) + if data is None: + if await self.__async_is_tracker_tariff_or_product__(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) - - if ("results" in data and len(data["results"]) > 0): - result = { - "value_inc_vat": float(data["results"][0]["value_inc_vat"]) - } - except: - _LOGGER.error(f'Failed to extract electricity standing charges: {url}') - raise + elif ("results" in data and len(data["results"]) > 0): + result = { + "value_inc_vat": float(data["results"][0]["value_inc_vat"]) + } return result @@ -515,9 +665,9 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t if tariff_parts is None: return None - product_code = tariff_parts["product_code"] + product_code = tariff_parts.product_code - if self.__async_is_tracker_tariff(tariff_code): + if self.__is_tracker_tariff__(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) result = None @@ -525,32 +675,248 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t 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: - try: - data = await self.__async_read_response(response, url) - if data is None: + data = await self.__async_read_response__(response, url) + if data is None: + if await self.__async_is_tracker_tariff_or_product__(tariff_code): return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) - - if ("results" in data and len(data["results"]) > 0): - result = { - "value_inc_vat": float(data["results"][0]["value_inc_vat"]) - } - except: - _LOGGER.error(f'Failed to extract gas standing charges: {url}') - raise + elif ("results" in data and len(data["results"]) > 0): + result = { + "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() - def __async_is_tracker_tariff(self, tariff_code): + 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"])), + "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"])), + "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"]): + return response_body["data"]["registeredKrakenflexDevice"] + 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"] + product_code = tariff_parts.product_code if product_code in self._product_tracker_cache: return self._product_tracker_cache[product_code] - + return False + + async def __async_is_tracker_tariff_or_product__(self, tariff_code): + 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 True + + async with aiohttp.ClientSession() as client: + auth = aiohttp.BasicAuth(self._api_key, '') + url = f'https://api.octopus.energy/v1/products/{product_code}' + async with client.get(url, auth=auth) as response: + data = await self.__async_read_response__(response, url) + if data == None: + return False + + # Just because a product states its a tracker, it may go through the normal APIs or it may go through + # the bespoke tracker api, so we can't assume anything from a tracker cache perspective + is_tracker = "is_tracker" in data and data["is_tracker"] + return is_tracker async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to, price_cap: float = None): """Get the tracker rates""" @@ -558,7 +924,7 @@ async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to, if tariff_parts is None: return None - product_code = tariff_parts["product_code"] + product_code = tariff_parts.product_code # If we know our tariff is not a tracker rate, then don't bother asking if product_code in self._product_tracker_cache and self._product_tracker_cache[product_code] == False: @@ -570,55 +936,59 @@ async def __async_get_tracker_rates__(self, tariff_code, period_from, period_to, url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' async with client.get(url, auth=auth) as response: try: - data = await self.__async_read_response(response, url) - if data == None: - return None - - items = [] - for period in data["periods"]: - valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') - valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) - - if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): - items.append( - { - "valid_from": valid_from.strftime("%Y-%m-%dT%H:%M:%SZ"), - "valid_to": valid_to.strftime("%Y-%m-%dT%H:%M:%SZ"), - "value_inc_vat": float(period["unit_rate"]), - } - ) - - results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code, price_cap) - self._product_tracker_cache[product_code] = True - except: - _LOGGER.error(f'Failed to extract tracker gas rates: {url}') - raise + data = await self.__async_read_response__(response, url) + except RequestError: + # This is thrown when the tariff isn't present + self._product_tracker_cache[product_code] = False + return None + + if data is None: + # This is thrown when the tariff isn't present + self._product_tracker_cache[product_code] = False + return None + + items = [] + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + items.append( + { + "valid_from": valid_from.strftime("%Y-%m-%dT%H:%M:%SZ"), + "valid_to": valid_to.strftime("%Y-%m-%dT%H:%M:%SZ"), + "value_inc_vat": float(period["unit_rate"]), + } + ) + + results = rates_to_thirty_minute_increments({ "results": items }, period_from, period_to, tariff_code, price_cap) + self._product_tracker_cache[product_code] = True return results async def __async_get_tracker_standing_charge__(self, tariff_code, period_from, period_to): """Get the tracker standing charge""" - results = [] async with aiohttp.ClientSession() as client: auth = aiohttp.BasicAuth(self._api_key, '') url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' async with client.get(url, auth=auth) as response: try: - data = await self.__async_read_response(response, url) - if data == None: - return None - - for period in data["periods"]: - valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') - valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) - if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): - return { - "value_inc_vat": float(period["standing_charge"]) - } - except: - _LOGGER.error(f'Failed to extract tracker gas rates: {url}') - raise + data = await self.__async_read_response__(response, url) + except RequestError: + # This is thrown when the tariff isn't present + return None + + if data is None: + return None + + for period in data["periods"]: + valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') + valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) + if ((valid_from >= period_from and valid_from <= period_to) or (valid_to >= period_from and valid_to <= period_to)): + return { + "value_inc_vat": float(period["standing_charge"]) + } return None @@ -663,19 +1033,31 @@ def __process_consumption(self, item): "interval_end": as_utc(parse_datetime(item["interval_end"])) } - async def __async_read_response(self, response, url): + 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: - _LOGGER.error(f'Octopus Energy server error ({url}): {response.status}; {text}') - else: - _LOGGER.error(f'Failed to send request ({url}): {response.status}; {text}') + 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: - return json.loads(text) + 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 index 696e57e9..969b14b0 100644 --- a/custom_components/octopus_energy/binary_sensor.py +++ b/custom_components/octopus_energy/binary_sensor.py @@ -1,12 +1,22 @@ from datetime import timedelta import logging -from .binary_sensors.saving_sessions import OctopusEnergySavingSessions -from .binary_sensors.target_rate import OctopusEnergyTargetRate 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 +from .utils import get_active_tariff_code + from .const import ( + DATA_ACCOUNT_ID, + DATA_CLIENT, + DATA_INTELLIGENT_DISPATCHES_COORDINATOR, DOMAIN, CONFIG_MAIN_API_KEY, @@ -20,13 +30,12 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=1) - 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_season_sensors(hass, entry, async_add_entities) + 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) @@ -52,8 +61,8 @@ async def async_setup_entry(hass, entry, async_add_entities): return True -async def async_setup_season_sensors(hass, entry, async_add_entities): - _LOGGER.debug('Setting up Season Saving entity') +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: @@ -63,7 +72,31 @@ async def async_setup_season_sensors(hass, entry, async_add_entities): await saving_session_coordinator.async_config_entry_first_refresh() - async_add_entities([OctopusEnergySavingSessions(saving_session_coordinator)], True) + 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 + + if has_intelligent_tariff or await async_mock_intelligent_data(hass): + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + + device = await client.async_get_intelligent_device(hass.data[DOMAIN][DATA_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) @@ -83,5 +116,5 @@ async def async_setup_target_sensors(hass, entry, async_add_entities): for meter in point["meters"]: is_export = meter["is_export"] - entities = [OctopusEnergyTargetRate(coordinator, config, is_export)] + entities = [OctopusEnergyTargetRate(hass, coordinator, config, is_export)] async_add_entities(entities, True) diff --git a/custom_components/octopus_energy/config_flow.py b/custom_components/octopus_energy/config_flow.py index 05b125f8..cd02cc12 100644 --- a/custom_components/octopus_energy/config_flow.py +++ b/custom_components/octopus_energy/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol import logging - from homeassistant.util.dt import (utcnow) from homeassistant.config_entries import (ConfigFlow, OptionsFlow) from homeassistant.core import callback @@ -28,6 +27,7 @@ CONFIG_TARGET_MPAN, CONFIG_TARGET_OFFSET, CONFIG_TARGET_ROLLING_TARGET, + CONFIG_TARGET_LAST_RATES, DATA_SCHEMA_ACCOUNT, DATA_CLIENT, @@ -49,12 +49,12 @@ def validate_target_rate_sensor(data): errors = {} matches = re.search(REGEX_ENTITY_NAME, data[CONFIG_TARGET_NAME]) - if matches == None: + if matches is None: errors[CONFIG_TARGET_NAME] = "invalid_target_name" # For some reason float type isn't working properly - reporting user input malformed matches = re.search(REGEX_HOURS, data[CONFIG_TARGET_HOURS]) - if matches == None: + if matches is None: errors[CONFIG_TARGET_HOURS] = "invalid_target_hours" else: data[CONFIG_TARGET_HOURS] = float(data[CONFIG_TARGET_HOURS]) @@ -63,17 +63,17 @@ def validate_target_rate_sensor(data): if CONFIG_TARGET_START_TIME in data: matches = re.search(REGEX_TIME, data[CONFIG_TARGET_START_TIME]) - if matches == None: + 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 == None: + 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 == None: + if matches is None: errors[CONFIG_TARGET_OFFSET] = "invalid_offset" return errors @@ -97,7 +97,7 @@ async def async_setup_initial_account(self, user_input): 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 == None): + 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 @@ -118,7 +118,7 @@ async def async_setup_target_rate_schema(self): 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 != None: + if active_tariff_code is not None: meters.append(point["mpan"]) return vol.Schema({ @@ -135,6 +135,7 @@ async def async_setup_target_rate_schema(self): 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, }) async def async_step_target_rate(self, user_input): @@ -205,7 +206,7 @@ async def __async_setup_target_rate_schema(self, config, errors): 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 != None: + if active_tariff_code is not None: meters.append(point["mpan"]) if (CONFIG_TARGET_MPAN not in config): @@ -227,6 +228,10 @@ async def __async_setup_target_rate_schema(self, config, errors): 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] return self.async_show_form( step_id="target_rate", @@ -244,6 +249,7 @@ async def __async_setup_target_rate_schema(self, config, errors): 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, }), errors=errors ) diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index f97985e9..ba9d32be 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -20,6 +20,7 @@ CONFIG_TARGET_MPAN = "MPAN" CONFIG_TARGET_OFFSET = "offset" CONFIG_TARGET_ROLLING_TARGET = "rolling_target" +CONFIG_TARGET_LAST_RATES = "last_rates" DATA_CONFIG = "CONFIG" DATA_ELECTRICITY_RATES_COORDINATOR = "ELECTRICITY_RATES_COORDINATOR" @@ -28,10 +29,19 @@ 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 = "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" + +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]$" diff --git a/custom_components/octopus_energy/coordinators/__init__.py b/custom_components/octopus_energy/coordinators/__init__.py new file mode 100644 index 00000000..ad2b52dd --- /dev/null +++ b/custom_components/octopus_energy/coordinators/__init__.py @@ -0,0 +1,79 @@ +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}'") + +async def async_get_current_electricity_agreement_tariff_codes(hass, client: OctopusEnergyApiClient, account_id: str): + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + tariff_codes = {} + current = now() + 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 != None: + key = (point["mpan"], is_smart_meter) + if key not in tariff_codes: + tariff_codes[(point["mpan"], is_smart_meter)] = active_tariff_code + await async_check_valid_tariff(hass, client, active_tariff_code, True) + + 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..9f610145 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/account.py @@ -0,0 +1,70 @@ +import logging +from datetime import timedelta + +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): + if DATA_ACCOUNT_COORDINATOR in hass.data[DOMAIN]: + _LOGGER.info("Account coordinator has already been configured, so skipping") + return + + 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: + ir.async_delete_issue(hass, DOMAIN, f"account_not_found_{account_id}") + hass.data[DOMAIN][DATA_ACCOUNT] = account_info + + 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..630d2cb6 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/current_consumption.py @@ -0,0 +1,78 @@ +from datetime import (datetime, timedelta) +import logging + +from homeassistant.util.dt import (parse_datetime, utcnow, 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, last_retrieval_date: datetime): + 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") + elif (current_date - last_retrieval_date).days >= 5: + period_from = (parse_datetime(period_to) - timedelta(days=5)).strftime("%Y-%m-%dT00:00:00Z") + else: + period_from = (last_retrieval_date + timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:00Z") + + try: + 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 + } + + 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): + """Create current consumption coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + previous_current_consumption_date_key = f'{device_id}_previous_current_consumption_date' + last_date = None + if previous_current_consumption_date_key in hass.data[DOMAIN]: + last_date = hass.data[DOMAIN][previous_current_consumption_date_key] + elif is_electricity == False: + last_date = (now() - timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) + + data = await async_get_live_consumption(client, device_id, utcnow(), last_date) + if data is not None: + hass.data[DOMAIN][previous_current_consumption_date_key] = data["startAt"] + + return data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"current_consumption_{device_id}", + 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/electricity_rates.py b/custom_components/octopus_energy/coordinators/electricity_rates.py new file mode 100644 index 00000000..2083b6de --- /dev/null +++ b/custom_components/octopus_energy/coordinators/electricity_rates.py @@ -0,0 +1,91 @@ +import logging +from datetime import 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_RATES, + DATA_ACCOUNT, + DATA_INTELLIGENT_DISPATCHES, +) + +from ..api_client import OctopusEnergyApiClient + +from . import async_get_current_electricity_agreement_tariff_codes +from ..intelligent import adjust_intelligent_rates + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_electricity_rates_coordinator(hass, account_id: str): + # Reset data rates as we might have new information + hass.data[DOMAIN][DATA_RATES] = [] + + if DATA_ELECTRICITY_RATES_COORDINATOR in hass.data[DOMAIN]: + _LOGGER.info("Rates coordinator has already been configured, so skipping") + return + + async def async_update_electricity_rates_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 in hass.data[DOMAIN]): + + tariff_codes = await async_get_current_electricity_agreement_tariff_codes(hass, client, account_id) + _LOGGER.debug(f'tariff_codes: {tariff_codes}') + + period_from = as_utc(current.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 = {} + dispatches = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN] else None + for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): + key = meter_point + + new_rates = None + if ((current.minute % 30) == 0 or + DATA_RATES not in hass.data[DOMAIN] or + hass.data[DOMAIN][DATA_RATES] is None or + key not in hass.data[DOMAIN][DATA_RATES] or + hass.data[DOMAIN][DATA_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) + except: + _LOGGER.debug('Failed to retrieve electricity rates') + else: + new_rates = hass.data[DOMAIN][DATA_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 (DATA_RATES in hass.data[DOMAIN] and key in hass.data[DOMAIN][DATA_RATES]): + _LOGGER.debug(f"Failed to retrieve new rates for {tariff_code}, so using cached rates") + rates[key] = hass.data[DOMAIN][DATA_RATES][key] + + hass.data[DOMAIN][DATA_RATES] = rates + + return hass.data[DOMAIN][DATA_RATES] + + hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( + hass, + _LOGGER, + name="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/gas_rates.py b/custom_components/octopus_energy/coordinators/gas_rates.py new file mode 100644 index 00000000..d75c01c5 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/gas_rates.py @@ -0,0 +1,56 @@ +from datetime import timedelta +import logging + +from homeassistant.util.dt import (utcnow, as_utc, parse_datetime) +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator +) + +from ..const import ( + DOMAIN, + + DATA_GAS_RATES +) + +from ..api_client import (OctopusEnergyApiClient) + +from . import async_check_valid_tariff + +_LOGGER = logging.getLogger(__name__) + +async def async_create_gas_rate_coordinator(hass, client: OctopusEnergyApiClient, tariff_code: str): + """Create gas rate coordinator""" + + async def async_update_data(): + """Fetch data from API endpoint.""" + current = utcnow() + + rate_key = f'{DATA_GAS_RATES}_{tariff_code}' + period_from = as_utc(parse_datetime(current.strftime("%Y-%m-%dT00:00:00Z"))) + + if (rate_key not in hass.data[DOMAIN] or + (current.minute % 30) == 0 or + hass.data[DOMAIN][rate_key] is None or + len(hass.data[DOMAIN][rate_key]) == 0 or + hass.data[DOMAIN][rate_key][-1]["valid_from"] < period_from): + period_to = as_utc(parse_datetime((current + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00Z"))) + + try: + hass.data[DOMAIN][rate_key] = await client.async_get_gas_rates(tariff_code, period_from, period_to) + await async_check_valid_tariff(hass, client, tariff_code, False) + except: + _LOGGER.debug('Failed to retrieve gas rates') + + return hass.data[DOMAIN][rate_key] + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"gas_rates_{tariff_code}", + 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/intelligent_dispatches.py b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py new file mode 100644 index 00000000..1de5be75 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py @@ -0,0 +1,94 @@ +import logging +from datetime import timedelta + +from ..coordinators import async_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 + + if DATA_INTELLIGENT_DISPATCHES_COORDINATOR in hass.data[DOMAIN]: + _LOGGER.info("Intelligent coordinator has already been configured, so skipping") + return + + 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 = await async_get_current_electricity_agreement_tariff_codes(hass, client, account_id) + _LOGGER.debug(f'tariff_codes: {tariff_codes}') + + 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) + 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..1904681b --- /dev/null +++ b/custom_components/octopus_energy/coordinators/intelligent_settings.py @@ -0,0 +1,78 @@ +import logging +from datetime import timedelta + +from . import async_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 + + if DATA_INTELLIGENT_SETTINGS_COORDINATOR in hass.data[DOMAIN]: + _LOGGER.info("Intelligent coordinator has already been configured, so skipping") + return + + 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() + + # 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 = await async_get_current_electricity_agreement_tariff_codes(hass, client, account_id) + _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) + 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..659b726a --- /dev/null +++ b/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py @@ -0,0 +1,135 @@ +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) + 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) + + 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..020ed381 --- /dev/null +++ b/custom_components/octopus_energy/coordinators/saving_sessions.py @@ -0,0 +1,50 @@ +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): + if DATA_SAVING_SESSIONS_COORDINATOR in hass.data[DOMAIN]: + return + + 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/electricity/__init__.py b/custom_components/octopus_energy/electricity/__init__.py new file mode 100644 index 00000000..2533ad07 --- /dev/null +++ b/custom_components/octopus_energy/electricity/__init__.py @@ -0,0 +1,136 @@ +from datetime import (datetime, timedelta) + +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 + +minimum_consumption_records = 2 + +async def async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + last_reset, + tariff_code + ): + 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": f'£{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_rate_information(rates, target: datetime): + min_target = target.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 + + if rates is not None: + for period in rates: + if target >= period["valid_from"] and target <= 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 current_rate is not None: + return { + "rates": list(map(lambda x: { + "from": x["valid_from"], + "to": x["valid_to"], + "rate": 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)), + "current_rate": current_rate, + "min_rate_today": min_rate_value, + "max_rate_today": max_rate_value, + "average_rate_today": total_rate_value / total_rates + } + + return None + +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..15b52390 --- /dev/null +++ b/custom_components/octopus_energy/electricity/base.py @@ -0,0 +1,42 @@ +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}")}, + 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/electricity/current_consumption.py b/custom_components/octopus_energy/electricity/current_consumption.py new file mode 100644 index 00000000..b50c4298 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_consumption.py @@ -0,0 +1,93 @@ +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) + +_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 + + @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 + self._attributes["last_updated_timestamp"] = consumption_result["startAt"] + + 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..e503f3ad --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_demand.py @@ -0,0 +1,90 @@ +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 + + @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): + """Handle updated data from the coordinator.""" + _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"] + self._attributes["last_updated_timestamp"] = consumption_result["startAt"] + + 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..9d9a9fc2 --- /dev/null +++ b/custom_components/octopus_energy/electricity/current_rate.py @@ -0,0 +1,122 @@ +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 . import (get_rate_information) + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor): + """Sensor for displaying the current rate.""" + + def __init__(self, hass: HomeAssistant, coordinator, meter, point, 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 + + @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_rate_information(self.coordinator.data[self._mpan] if 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, + "rates": rate_information["rates"], + "rate": rate_information["current_rate"], + "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"] + } + + 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 + } + + 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..0fcccd19 --- /dev/null +++ b/custom_components/octopus_energy/electricity/next_rate.py @@ -0,0 +1,119 @@ +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 . import (get_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 + + @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 + timedelta(minutes=30) + + rate_information = get_rate_information(self.coordinator.data[self._mpan] if 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, + "rates": rate_information["rates"], + "rate": rate_information["current_rate"], + "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"] + } + + 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, + } + + 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..879c913e --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py @@ -0,0 +1,155 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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..577ebfd1 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py @@ -0,0 +1,131 @@ +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 ( + async_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""" + return self._state + + @property + def should_poll(self) -> bool: + return True + + async def async_update(self): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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"] + + 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..d6bee4ec --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py @@ -0,0 +1,131 @@ +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 ( + async_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""" + return self._state + + @property + def should_poll(self) -> bool: + return True + + async def async_update(self): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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"] + + 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..92b23d94 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py @@ -0,0 +1,156 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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', + "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 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..16c31ecd --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_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 . import ( + async_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 should_poll(self): + return True + + @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 + + async def async_update(self): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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"] + + 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..1c7832cb --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py @@ -0,0 +1,168 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if 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 = await async_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 + ) + + 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..b0a1a367 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py @@ -0,0 +1,111 @@ +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}")}, + default_name=f"Electricity Meter{self._export_name_addition}", + 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..264a23e1 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_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 . import ( + async_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""" + return self._state + + @property + def should_poll(self): + return True + + async def async_update(self): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_electricity_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code + ) + + 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"] + + 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..bdfd0e23 --- /dev/null +++ b/custom_components/octopus_energy/electricity/previous_rate.py @@ -0,0 +1,119 @@ +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 . import (get_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 + + @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 - timedelta(minutes=30) + + rate_information = get_rate_information(self.coordinator.data[self._mpan] if 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, + "rates": rate_information["rates"], + "rate": rate_information["current_rate"], + "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"] + } + + 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, + } + + 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..596f9bc1 --- /dev/null +++ b/custom_components/octopus_energy/electricity/standing_charge.py @@ -0,0 +1,100 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +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, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + OctopusEnergyElectricitySensor.__init__(self, hass, 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 + + current = now() + if (self._latest_date is None or (self._latest_date + timedelta(days=1)) < current): + _LOGGER.debug('Updating OctopusEnergyElectricityCurrentStandingCharge') + + period_from = current.replace(hour=0, minute=0, second=0, microsecond=0) + period_to = period_from + timedelta(days=1) + + standard_charge_result = await self._client.async_get_electricity_standing_charge(self._tariff_code, period_from, period_to) + + if standard_charge_result is not 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/gas/__init__.py b/custom_components/octopus_energy/gas/__init__.py new file mode 100644 index 00000000..9f1b710e --- /dev/null +++ b/custom_components/octopus_energy/gas/__init__.py @@ -0,0 +1,99 @@ +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 + +# 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 + +async def async_calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + last_reset, + tariff_code, + consumption_units, + calorific_value + ): + 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": f'£{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..8201fd9b --- /dev/null +++ b/custom_components/octopus_energy/gas/base.py @@ -0,0 +1,37 @@ +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}")}, + 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/gas/current_consumption.py b/custom_components/octopus_energy/gas/current_consumption.py new file mode 100644 index 00000000..ed7a7475 --- /dev/null +++ b/custom_components/octopus_energy/gas/current_consumption.py @@ -0,0 +1,92 @@ +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) + +_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 + + @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 (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/gas/current_rate.py b/custom_components/octopus_energy/gas/current_rate.py new file mode 100644 index 00000000..464859d2 --- /dev/null +++ b/custom_components/octopus_energy/gas/current_rate.py @@ -0,0 +1,114 @@ +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) + +_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._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 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 latest gas price""" + + current = now() + if (self._latest_date is None or (self._latest_date + timedelta(days=1)) < current) 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 current >= period["valid_from"] and current <= 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/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py new file mode 100644 index 00000000..44babea3 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py @@ -0,0 +1,160 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value + ) + + 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..382c2ad0 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py @@ -0,0 +1,158 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value + ) + + 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..9b76cc43 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost.py @@ -0,0 +1,161 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if self.coordinator.data is not None and "consumption" in self.coordinator.data else None + rate_data = self.coordinator.data["rates"] if self.coordinator.data is not None and "rates" in self.coordinator.data else None + standing_charge = self.coordinator.data["standing_charge"] if self.coordinator.data is not None and "standing_charge" in self.coordinator.data else None + + consumption_and_cost = await async_calculate_gas_consumption_and_cost( + consumption_data, + rate_data, + standing_charge, + self._last_reset, + self._tariff_code, + self._native_consumption_units, + self._calorific_value + ) + + 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', + "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 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..e8cf594f --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py @@ -0,0 +1,169 @@ +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 ( + async_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): + consumption_data = self.coordinator.data["consumption"] if 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 = await async_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 + ) + + 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..5299bc84 --- /dev/null +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py @@ -0,0 +1,106 @@ +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}")}, + default_name="Gas Meter", + 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/standing_charge.py b/custom_components/octopus_energy/gas/standing_charge.py new file mode 100644 index 00000000..1277e607 --- /dev/null +++ b/custom_components/octopus_energy/gas/standing_charge.py @@ -0,0 +1,106 @@ +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant + +from homeassistant.util.dt import (now) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass +) + +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, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): + """Init sensor.""" + OctopusEnergyGasSensor.__init__(self, hass, 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 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""" + return self._state + + async def async_update(self): + """Get the current price.""" + # Find the current rate. We only need to do this every day + + current = now() + if (self._latest_date is None or (self._latest_date + timedelta(days=1)) < current): + _LOGGER.debug('Updating OctopusEnergyGasCurrentStandingCharge') + + period_from = current.replace(hour=0, minute=0, second=0, microsecond=0) + period_to = period_from + timedelta(days=1) + + standard_charge_result = await self._client.async_get_gas_standing_charge(self._tariff_code, period_from, period_to) + + if standard_charge_result is not 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/intelligent/__init__.py b/custom_components/octopus_energy/intelligent/__init__.py new file mode 100644 index 00000000..10598dc2 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/__init__.py @@ -0,0 +1,119 @@ +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(): + return { + "planned": [ + { + "start": utcnow().replace(hour=19, minute=0, second=0, microsecond=0), + "end": utcnow().replace(hour=20, minute=0, second=0, microsecond=0), + "source": "smart-charge" + } + ], + "completed": [ + { + "start": utcnow().replace(hour=6, minute=0, second=0, microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ"), + "end": utcnow().replace(hour=7, minute=0, second=0, microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ"), + "source": None + }, + { + "start": utcnow().replace(hour=7, minute=0, second=0, microsecond=0), + "end": utcnow().replace(hour=8, minute=0, second=0, microsecond=0), + "source": None + } + ] + } + +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 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..fb4d6705 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/base.py @@ -0,0 +1,19 @@ +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") + }, + default_name="Charger", + 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..a9be5456 --- /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.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..80fdcdf8 --- /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 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.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.data is not None else time(9,0), + self.coordinator.data["ready_time_weekend"] if 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..01e2b5d6 --- /dev/null +++ b/custom_components/octopus_energy/intelligent/dispatching.py @@ -0,0 +1,92 @@ +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_retrieved": 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_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 (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..ea4bd7c4 --- /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.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"] + + 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.data is not None else 100, + self.coordinator.data["charge_limit_weekend"] if 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..f1476db9 --- /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.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 index 7f0d4b64..3d0d458e 100644 --- a/custom_components/octopus_energy/manifest.json +++ b/custom_components/octopus_energy/manifest.json @@ -6,13 +6,14 @@ ], "config_flow": true, "dependencies": [ - "repairs" + "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": "6.6.1", + "version": "7.5.2", "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..c1ec522f --- /dev/null +++ b/custom_components/octopus_energy/number.py @@ -0,0 +1,55 @@ +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 +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 + + if has_intelligent_tariff or await async_mock_intelligent_data(hass): + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + 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..c6f5af28 --- /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 (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..da0cdd14 --- /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 (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 index 173904be..25547155 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,33 +1,34 @@ from datetime import timedelta import logging - -from .utils.check_tariff import async_check_valid_tariff -from .sensors.electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption -from .sensors.electricity.current_demand import OctopusEnergyCurrentElectricityDemand -from .sensors.electricity.current_rate import OctopusEnergyElectricityCurrentRate -from .sensors.electricity.next_rate import OctopusEnergyElectricityNextRate -from .sensors.electricity.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeElectricityConsumption -from .sensors.electricity.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeElectricityCost -from .sensors.electricity.previous_rate import OctopusEnergyElectricityPreviousRate -from .sensors.electricity.standing_charge import OctopusEnergyElectricityCurrentStandingCharge -from .sensors.gas.current_rate import OctopusEnergyGasCurrentRate -from .sensors.gas.previous_accumulative_consumption import OctopusEnergyPreviousAccumulativeGasConsumption -from .sensors.gas.previous_accumulative_consumption_kwh import OctopusEnergyPreviousAccumulativeGasConsumptionKwh -from .sensors.gas.previous_accumulative_cost import OctopusEnergyPreviousAccumulativeGasCost -from .sensors.gas.current_consumption import OctopusEnergyCurrentGasConsumption -from .sensors.gas.standing_charge import OctopusEnergyGasCurrentStandingCharge - -from .sensors.saving_sessions.points import OctopusEnergySavingSessionPoints - -from homeassistant.util.dt import (utcnow, now, as_utc, parse_datetime) -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator -) - -from .sensors import ( - async_get_consumption_data, - async_get_live_consumption -) +from homeassistant.util.dt import (utcnow) +from homeassistant.core import HomeAssistant + +from .electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption +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.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.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 .saving_sessions.points import OctopusEnergySavingSessionPoints from .utils import (get_active_tariff_code) from .const import ( @@ -42,123 +43,18 @@ DATA_ELECTRICITY_RATES_COORDINATOR, DATA_SAVING_SESSIONS_COORDINATOR, DATA_CLIENT, - DATA_ACCOUNT, - DATA_GAS_RATES + DATA_ACCOUNT ) -from .api_client import (OctopusEnergyApiClient) - _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=1) - -def create_reading_coordinator(hass, client: OctopusEnergyApiClient, is_electricity: bool, identifier: str, serial_number: str): - """Create reading coordinator""" - - async def async_update_data(): - """Fetch data from API endpoint.""" - - previous_consumption_key = f'{identifier}_{serial_number}_previous_consumption' - previous_data = None - if previous_consumption_key in hass.data[DOMAIN]: - previous_data = hass.data[DOMAIN][previous_consumption_key] - - 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)) - - data = await async_get_consumption_data( - client, - previous_data, - utcnow(), - period_from, - period_to, - identifier, - serial_number, - is_electricity - ) - - if data != None and len(data) > 0: - hass.data[DOMAIN][previous_consumption_key] = data - return data - - return [] - - 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}_consumption_coordinator'] = coordinator - - return coordinator - -def create_current_consumption_coordinator(hass, client: OctopusEnergyApiClient, device_id: str, is_electricity: bool): - """Create current consumption coordinator""" - - async def async_update_data(): - """Fetch data from API endpoint.""" - previous_current_consumption_date_key = f'{device_id}_previous_current_consumption_date' - last_date = None - if previous_current_consumption_date_key in hass.data[DOMAIN]: - last_date = hass.data[DOMAIN][previous_current_consumption_date_key] - elif is_electricity == False: - last_date = (now() - timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) - - data = await async_get_live_consumption(client, device_id, utcnow(), last_date) - if data is not None: - hass.data[DOMAIN][previous_current_consumption_date_key] = data["startAt"] - - return data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"current_consumption_{device_id}", - update_method=async_update_data, - update_interval=timedelta(minutes=1), - ) - - return coordinator - -def create_gas_rate_coordinator(hass, client: OctopusEnergyApiClient, tariff_code: str): - """Create gas rate coordinator""" - - async def async_update_data(): - """Fetch data from API endpoint.""" - current = utcnow() - - rate_key = f'{DATA_GAS_RATES}_{tariff_code}' - if (rate_key not in hass.data[DOMAIN] or (current.minute % 30) == 0 or len(hass.data[DOMAIN][rate_key]) == 0): - period_from = as_utc(parse_datetime(current.strftime("%Y-%m-%dT00:00:00Z"))) - period_to = as_utc(parse_datetime((current + timedelta(days=1)).strftime("%Y-%m-%dT00:00:00Z"))) - - hass.data[DOMAIN][rate_key] = await client.async_get_gas_rates(tariff_code, period_from, period_to) - await async_check_valid_tariff(hass, client, tariff_code, False) - - return hass.data[DOMAIN][rate_key] - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"gas_rates_{tariff_code}", - update_method=async_update_data, - update_interval=timedelta(minutes=1), - ) - - return coordinator - 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, entry, async_add_entities): +async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_entities): config = dict(entry.data) if entry.options: @@ -166,15 +62,15 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): client = hass.data[DOMAIN][DATA_CLIENT] - rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] + electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] - await rate_coordinator.async_config_entry_first_refresh() + await electricity_rate_coordinator.async_config_entry_first_refresh() saving_session_coordinator = hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR] await saving_session_coordinator.async_config_entry_first_refresh() - entities = [OctopusEnergySavingSessionPoints(saving_session_coordinator)] + entities = [OctopusEnergySavingSessionPoints(hass, saving_session_coordinator)] account_info = hass.data[DOMAIN][DATA_ACCOUNT] @@ -188,23 +84,35 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): 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 != None: + 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(rate_coordinator, meter, point, electricity_price_cap)) - entities.append(OctopusEnergyElectricityPreviousRate(rate_coordinator, meter, point)) - entities.append(OctopusEnergyElectricityNextRate(rate_coordinator, meter, point)) - entities.append(OctopusEnergyElectricityCurrentStandingCharge(client, electricity_tariff_code, meter, point)) - - if meter["is_smart_meter"] == True: - coordinator = create_reading_coordinator(hass, client, True, point["mpan"], meter["serial_number"]) - entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(coordinator, meter, point)) - entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(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: - consumption_coordinator = create_current_consumption_coordinator(hass, client, meter["device_id"], True) - entities.append(OctopusEnergyCurrentElectricityConsumption(consumption_coordinator, meter, point)) - entities.append(OctopusEnergyCurrentElectricityDemand(consumption_coordinator, meter, point)) + entities.append(OctopusEnergyElectricityCurrentRate(hass, electricity_rate_coordinator, meter, point, 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, client, 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: + consumption_coordinator = await async_create_current_consumption_coordinator(hass, client, meter["device_id"], True) + entities.append(OctopusEnergyCurrentElectricityConsumption(hass, consumption_coordinator, 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"]}') @@ -225,22 +133,30 @@ async def async_setup_default_sensors(hass, entry, async_add_entities): 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 != None: + 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"]}') - rate_coordinator = create_gas_rate_coordinator(hass, client, gas_tariff_code) - entities.append(OctopusEnergyGasCurrentRate(rate_coordinator, gas_tariff_code, meter, point, gas_price_cap)) - entities.append(OctopusEnergyGasCurrentStandingCharge(client, gas_tariff_code, meter, point)) - - if meter["is_smart_meter"] == True: - previous_consumption_coordinator = create_reading_coordinator(hass, client, False, point["mprn"], meter["serial_number"]) - entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(previous_consumption_coordinator, meter, point, calorific_value)) - entities.append(OctopusEnergyPreviousAccumulativeGasConsumptionKwh(previous_consumption_coordinator, meter, point, calorific_value)) - entities.append(OctopusEnergyPreviousAccumulativeGasCost(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: - consumption_coordinator = create_current_consumption_coordinator(hass, client, meter["device_id"], False) - entities.append(OctopusEnergyCurrentGasConsumption(consumption_coordinator, meter, point)) + rate_coordinator = await async_create_gas_rate_coordinator(hass, client, gas_tariff_code) + entities.append(OctopusEnergyGasCurrentRate(hass, rate_coordinator, gas_tariff_code, meter, point, gas_price_cap)) + entities.append(OctopusEnergyGasCurrentStandingCharge(hass, client, 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: + consumption_coordinator = await async_create_current_consumption_coordinator(hass, client, meter["device_id"], False) + entities.append(OctopusEnergyCurrentGasConsumption(hass, consumption_coordinator, 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"]}') 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..c131bbe7 --- /dev/null +++ b/custom_components/octopus_energy/switch.py @@ -0,0 +1,59 @@ +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 +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 + + if has_intelligent_tariff or await async_mock_intelligent_data(hass): + 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] + 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..7e20cd55 --- /dev/null +++ b/custom_components/octopus_energy/target_rates/__init__.py @@ -0,0 +1,280 @@ +from datetime import datetime, timedelta +import math +import re + +from homeassistant.util.dt import (as_utc, parse_datetime) + +from ..const import REGEX_OFFSET_PARTS +import logging + +_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, + } 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..2120da63 --- /dev/null +++ b/custom_components/octopus_energy/target_rates/target_rate.py @@ -0,0 +1,252 @@ +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 ..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, + CONFIG_TARGET_LAST_RATES, + + 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, 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.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 + + # 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 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]) + + 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, + 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, + self._is_export, + 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 + 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 is 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 is 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 is 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 is 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/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..33d255ba --- /dev/null +++ b/custom_components/octopus_energy/time.py @@ -0,0 +1,55 @@ +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 +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 + + if has_intelligent_tariff or await async_mock_intelligent_data(hass): + coordinator = hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR] + client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] + account_id = hass.data[DOMAIN][DATA_ACCOUNT_ID] + 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 index 19b78df9..aebcbf7e 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -7,7 +7,7 @@ "data": { "Api key": "Api key", "Account Id": "Your account Id (e.g. A-AAAA1111)", - "supports_live_consumption": "I have a Home Mini", + "supports_live_consumption": "I have a Home Mini - https://octopus.energy/blog/octopus-home-mini/", "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" @@ -17,19 +17,20 @@ "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.", "data": { "entity_id": "The name of your target", - "Hours": "The hours you require.", + "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" + "rolling_target": "Re-evaluate multiple times a day", + "last_rates": "Find last applicable rates" } } }, "error": { "account_not_found": "Account information was not found", - "invalid_target_hours": "Target hours must be in half hour increments.", + "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" @@ -45,7 +46,7 @@ "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", + "supports_live_consumption": "I have a Home Mini - https://octopus.energy/blog/octopus-home-mini/", "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", @@ -57,17 +58,18 @@ "title": "Update Target Rate", "description": "Update the settings for your target rate sensor, which can be used to help you save energy and money.", "data": { - "Hours": "The hours you require.", + "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" + "rolling_target": "Re-evaluate multiple times a day", + "last_rates": "Find last applicable rates" } } }, "error": { - "invalid_target_hours": "Target hours must be in half hour increments.", + "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" }, diff --git a/custom_components/octopus_energy/utils/__init__.py b/custom_components/octopus_energy/utils/__init__.py index 328fd353..ebd807d7 100644 --- a/custom_components/octopus_energy/utils/__init__.py +++ b/custom_components/octopus_energy/utils/__init__.py @@ -5,12 +5,23 @@ from ..const import ( REGEX_TARIFF_PARTS, - REGEX_OFFSET_PARTS, ) -def get_tariff_parts(tariff_code): +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 == None: + if matches is None: return None # If our energy or rate isn't extracted, then assume is electricity and "single" rate as that's @@ -20,12 +31,7 @@ def get_tariff_parts(tariff_code): product_code =matches.groupdict()["product_code"] region = matches.groupdict()["region"] - return { - "energy": energy, - "rate": rate, - "product_code": product_code, - "region": region - } + return TariffParts(energy, rate, product_code, region) def get_active_tariff_code(utcnow: datetime, agreements): latest_agreement = None @@ -33,93 +39,34 @@ def get_active_tariff_code(utcnow: datetime, agreements): # Find our latest agreement for agreement in agreements: - if agreement["tariff_code"] == None: + if agreement["tariff_code"] is 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): + 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"] != 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 == None or latest_valid_to >= utcnow: + if latest_valid_to is None or latest_valid_to >= utcnow: latest_agreement = agreement latest_valid_from = valid_from - if latest_agreement != None: + if latest_agreement is not 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}') +def get_off_peak_cost(rates): + off_peak_cost = None - symbol = matches[1] - hours = float(matches[2]) - minutes = float(matches[3]) - seconds = float(matches[4]) + 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"] - 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, 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"] != 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_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 \ No newline at end of file + 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/tariff_check.py b/custom_components/octopus_energy/utils/tariff_check.py new file mode 100644 index 00000000..c91d9196 --- /dev/null +++ b/custom_components/octopus_energy/utils/tariff_check.py @@ -0,0 +1,39 @@ +from . import get_tariff_parts +from ..api_client import (OctopusEnergyApiClient) + +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 From 7ad879dc09fb117d1b3244492a4c8181d4a1697b Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:28:41 +0100 Subject: [PATCH 095/158] Include missing file --- .../better_thermostat/utils/watcher.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 custom_components/better_thermostat/utils/watcher.py 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 From 50a67fdd7a38fdf2c41c4474f9bfad3ab372e3a8 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:30:30 +0100 Subject: [PATCH 096/158] LocalTuya 5.2.1 --- custom_components/localtuya/config_flow.py | 27 ++++++++++-------- custom_components/localtuya/const.py | 1 + custom_components/localtuya/manifest.json | 10 +++---- .../localtuya/pytuya/__init__.py | 28 +++++++++++++------ .../localtuya/translations/en.json | 1 + 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index d4272d41..5c87e254 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -43,6 +43,7 @@ CONF_RESET_DPIDS, CONF_SETUP_CLOUD, CONF_USER_ID, + CONF_ENABLE_ADD_ENTITIES, DATA_CLOUD, DATA_DISCOVERY, DOMAIN, @@ -146,6 +147,7 @@ def options_schema(entities): vol.Required( CONF_ENTITIES, description={"suggested_value": entity_names} ): cv.multi_select(entity_names), + vol.Required(CONF_ENABLE_ADD_ENTITIES, default=False): bool, } ) @@ -554,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, @@ -608,6 +621,7 @@ async def async_step_configure_device(self, user_input=None): 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" @@ -647,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)) @@ -740,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 3a6c2529..630d630a 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -35,6 +35,7 @@ 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/manifest.json b/custom_components/localtuya/manifest.json index 3f1e00bd..28e36fa0 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,14 +1,14 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "5.0.0", - "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/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 746cc547..bcc8bbed 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -449,7 +449,7 @@ async def wait_for(self, seqno, cmd, timeout=5): try: await asyncio.wait_for(self.listeners[seqno].acquire(), timeout=timeout) except asyncio.TimeoutError: - self.warning( + self.debug( "Command %d timed out waiting for sequence number %d", cmd, seqno ) del self.listeners[seqno] @@ -887,8 +887,10 @@ def _decode_payload(self, payload): try: # self.debug("decrypting=%r", payload) payload = cipher.decrypt(payload, False, decode_text=False) - except Exception: - self.debug("incomplete payload=%r (len:%d)", payload, len(payload)) + 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) @@ -913,8 +915,13 @@ def _decode_payload(self, payload): try: # self.debug("decrypting=%r", payload) payload = cipher.decrypt(payload, False) - except Exception: - self.debug("incomplete payload=%r (len:%d)", payload, len(payload)) + 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) @@ -944,8 +951,10 @@ def _decode_payload(self, payload): self.debug("Deciphered data = %r", payload) try: json_payload = json.loads(payload) - except Exception: - raise DecodeError("could not decrypt data: wrong local_key?") + 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}}, ...} @@ -980,11 +989,12 @@ async def _negotiate_session_key(self): # 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: + except Exception as ex: self.debug( - "session key step 2 decrypt failed, payload=%r (len:%d)", + "session key step 2 decrypt failed, payload=%r with len:%d (%s)", payload, len(payload), + ex, ) return False diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 947141cb..b9beee47 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -99,6 +99,7 @@ "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)" } From bf146b87506da2857b6932ecc9495f9c49522343 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:32:40 +0100 Subject: [PATCH 097/158] Frigate v4.0.0 --- custom_components/frigate/__init__.py | 9 +- custom_components/frigate/api.py | 32 +- custom_components/frigate/binary_sensor.py | 7 +- custom_components/frigate/camera.py | 162 +++++++++- custom_components/frigate/config_flow.py | 11 + custom_components/frigate/const.py | 1 + custom_components/frigate/manifest.json | 18 +- custom_components/frigate/media_source.py | 279 +++++++++--------- custom_components/frigate/sensor.py | 159 +++++++++- .../frigate/translations/en.json | 1 + .../frigate/translations/pt-BR.json | 1 + .../frigate/translations/pt_br.json | 1 + custom_components/frigate/views.py | 30 +- custom_components/frigate/ws_api.py | 28 +- 14 files changed, 550 insertions(+), 189 deletions(-) diff --git a/custom_components/frigate/__init__.py b/custom_components/frigate/__init__.py index 030702f8..88011612 100644 --- a/custom_components/frigate/__init__.py +++ b/custom_components/frigate/__init__.py @@ -170,8 +170,10 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: 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() @@ -208,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 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 f8991250..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.""" @@ -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 36a84e99..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.1", - "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 365fed57..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__: - if getattr(self, attribute.name) is None: # type: ignore[attr-defined] - return {attribute.name: data} # type: ignore[attr-defined] - 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/views.py b/custom_components/frigate/views.py index 60db1471..2a4d4c9e 100644 --- a/custom_components/frigate/views.py +++ b/custom_components/frigate/views.py @@ -101,6 +101,8 @@ 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)) @@ -479,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, ), ) From d06ef2f19ebff67714b62ce0986cbf65c2ab0cb6 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:34:26 +0100 Subject: [PATCH 098/158] Alexa Media 4.6.5 --- custom_components/alexa_media/__init__.py | 49 ++++++++++++------- .../alexa_media/alarm_control_panel.py | 8 ++- .../alexa_media/binary_sensor.py | 9 +++- custom_components/alexa_media/const.py | 2 +- custom_components/alexa_media/helpers.py | 4 +- custom_components/alexa_media/light.py | 9 +++- custom_components/alexa_media/manifest.json | 4 +- custom_components/alexa_media/media_player.py | 8 ++- custom_components/alexa_media/notify.py | 2 + custom_components/alexa_media/sensor.py | 21 ++++++-- custom_components/alexa_media/strings.json | 2 +- custom_components/alexa_media/switch.py | 8 ++- .../alexa_media/translations/ar.json | 2 +- .../alexa_media/translations/en.json | 2 +- .../alexa_media/translations/pt-BR.json | 2 +- 15 files changed, 96 insertions(+), 36 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 323af03e..929dbea2 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -317,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) @@ -672,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( @@ -907,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 a1b0262e..c5ae861d 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -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] diff --git a/custom_components/alexa_media/binary_sensor.py b/custom_components/alexa_media/binary_sensor.py index b9cca0b3..73296b7f 100644 --- a/custom_components/alexa_media/binary_sensor.py +++ b/custom_components/alexa_media/binary_sensor.py @@ -14,6 +14,7 @@ BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ( @@ -33,7 +34,13 @@ 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] + 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, []) diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 8bb27043..c8bf4237 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -14,7 +14,7 @@ PERCENTAGE, ) -__version__ = "4.6.1" +__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" diff --git a/custom_components/alexa_media/helpers.py b/custom_components/alexa_media/helpers.py index cdd9a66c..69434af8 100644 --- a/custom_components/alexa_media/helpers.py +++ b/custom_components/alexa_media/helpers.py @@ -255,8 +255,8 @@ async def calculate_uuid(hass, email: str, url: str) -> dict: 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 diff --git a/custom_components/alexa_media/light.py b/custom_components/alexa_media/light.py index d996a5a6..b8bd529c 100644 --- a/custom_components/alexa_media/light.py +++ b/custom_components/alexa_media/light.py @@ -21,6 +21,7 @@ SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.exceptions import ConfigEntryNotReady try: from homeassistant.components.light import ( @@ -69,7 +70,13 @@ 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] + 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, []) diff --git a/custom_components/alexa_media/manifest.json b/custom_components/alexa_media/manifest.json index a9d6596f..7700856b 100644 --- a/custom_components/alexa_media/manifest.json +++ b/custom_components/alexa_media/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", "loggers": ["alexapy", "authcaptureproxy"], - "requirements": ["alexapy==1.26.5", "packaging>=20.3", "wrapt>=1.12.1"], - "version": "4.6.1" + "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 ebaf40ba..31b42a13 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -91,7 +91,13 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """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 diff --git a/custom_components/alexa_media/notify.py b/custom_components/alexa_media/notify.py index 26e290ae..d220b9a5 100644 --- a/custom_components/alexa_media/notify.py +++ b/custom_components/alexa_media/notify.py @@ -149,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( diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py index 4b341772..c1910ec3 100644 --- a/custom_components/alexa_media/sensor.py +++ b/custom_components/alexa_media/sensor.py @@ -17,7 +17,7 @@ SensorStateClass, ) from homeassistant.const import UnitOfTemperature, __version__ as HA_VERSION -from homeassistant.core import callback +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 @@ -64,7 +64,13 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf "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] @@ -549,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: @@ -557,9 +563,9 @@ 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) @@ -626,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.""" 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 ba62d7cd..9a572057 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -41,7 +41,13 @@ async def async_setup_platform(hass, config, add_devices_callback, discovery_inf ("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] 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/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%]", From 890c3c3534eb9eb2df5e3205f2b876fd160cc0b2 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:36:17 +0100 Subject: [PATCH 099/158] Adaptive lighting 1.16.3 --- .../adaptive_lighting/__init__.py | 15 +- .../adaptive_lighting/adaptation_utils.py | 203 ++++++ custom_components/adaptive_lighting/const.py | 16 + .../adaptive_lighting/hass_utils.py | 64 ++ .../adaptive_lighting/manifest.json | 4 +- .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 576 ++++++++++++++---- .../adaptive_lighting/translations/de.json | 3 +- .../adaptive_lighting/translations/en.json | 3 +- 9 files changed, 745 insertions(+), 142 deletions(-) create mode 100644 custom_components/adaptive_lighting/adaptation_utils.py create mode 100644 custom_components/adaptive_lighting/hass_utils.py diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index dc928a6b..f985e8c5 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service import voluptuous as vol from .const import ( @@ -36,10 +35,13 @@ 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.""" - # This will reload any changes the user made to any YAML configurations. - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if DOMAIN in config: for entry in config[DOMAIN]: @@ -55,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: @@ -83,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/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 64620927..241689fc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -176,6 +176,17 @@ "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" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" @@ -271,6 +282,11 @@ def int_between(min_int, max_int): DEFAULT_AUTORESET_CONTROL, int_between(0, 365 * 24 * 60 * 60), # 1 year max ), + ( + CONF_SKIP_REDUNDANT_COMMANDS, + DEFAULT_SKIP_REDUNDANT_COMMANDS, + bool, + ), ] 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 2dec4c83..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", - "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1"], + "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], "config_flow": true, "dependencies": [], "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.11.0" + "version": "1.16.3" } diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 54af5e11..7ad84741 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -47,7 +47,8 @@ "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. ⏲️" + "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 98747a17..fbf6c79c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -17,12 +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_KELVIN, - ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -32,6 +27,7 @@ COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, COLOR_MODE_XY, ) from homeassistant.components.light import ( @@ -40,6 +36,7 @@ SUPPORT_COLOR_TEMP, SUPPORT_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 @@ -53,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, @@ -91,6 +90,13 @@ 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, @@ -116,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, @@ -147,6 +154,7 @@ apply_service_schema, replace_none_str, ) +from .hass_utils import setup_service_call_interceptor _SUPPORT_OPTS = { "brightness": SUPPORT_BRIGHTNESS, @@ -162,25 +170,16 @@ 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_KELVIN, - ATTR_HS_COLOR, - 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 = "al" @@ -256,33 +255,17 @@ def create_context( 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 f":{_DOMAIN_SHORT}:" in 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 + return is_our_context_id(context.id) def _get_switches_with_lights( @@ -300,15 +283,18 @@ def _get_switches_with_lights( 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): + 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], - is_on: bool = False, ) -> AdaptiveSwitch: """Find the switch that controls the lights in 'lights'.""" switches = _get_switches_with_lights(hass, lights) @@ -319,13 +305,13 @@ def find_switch_for_lights( if len(on_switches) == 1: # Of the multiple switches, only one is on return on_switches[0] - raise ValueError( + 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 ValueError( + 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'." @@ -365,7 +351,7 @@ def _get_switches_from_service_call( return switches if lights: - switch = find_switch_for_lights(hass, lights, service_call) + switch = find_switch_for_lights(hass, lights) return [switch] raise ValueError( @@ -396,17 +382,19 @@ async def handle_change_switch_settings( defaults=defaults, ) + switch._update_time_interval_listener() + _LOGGER.debug( "Called 'adaptive_lighting.change_switch_settings' service with '%s'", data, ) - all_lights = switch._lights # pylint: disable=protected-access + 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, + transition=switch.initial_transition, force=True, context=switch.create_context("service", parent=service_call.context), ) @@ -440,8 +428,8 @@ 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] + 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 ) @@ -469,7 +457,7 @@ async def async_setup_entry( 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, ) @@ -485,7 +473,7 @@ async def handle_apply(service_call: ServiceCall): lights = data[CONF_LIGHTS] for switch in switches: if not lights: - all_lights = switch._lights # pylint: disable=protected-access + all_lights = switch.lights else: all_lights = _expand_light_groups(switch.hass, lights) switch.turn_on_off_listener.lights.update(all_lights) @@ -497,7 +485,6 @@ async def handle_apply(service_call: ServiceCall): data[ATTR_ADAPT_BRIGHTNESS], data[ATTR_ADAPT_COLOR], data[CONF_PREFER_RGB_COLOR], - force=True, context=switch.create_context( "service", parent=service_call.context ), @@ -515,7 +502,7 @@ async def handle_set_manual_control(service_call: ServiceCall): lights = data[CONF_LIGHTS] for switch in switches: if not lights: - all_lights = switch._lights # pylint: disable=protected-access + all_lights = switch.lights else: all_lights = _expand_light_groups(switch.hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: @@ -527,7 +514,7 @@ async def handle_set_manual_control(service_call: ServiceCall): # pylint: disable=protected-access await switch._update_attrs_and_maybe_adapt_lights( all_lights, - transition=switch._initial_transition, + transition=switch.initial_transition, force=True, context=switch.create_context( "service", parent=service_call.context @@ -540,7 +527,7 @@ async def handle_set_manual_control(service_call: ServiceCall): service=SERVICE_APPLY, service_func=handle_apply, schema=apply_service_schema( - switch._initial_transition + switch.initial_transition ), # pylint: disable=protected-access ) @@ -638,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 @@ -799,8 +789,8 @@ def __init__( data = validate(config_entry) self._name = data[CONF_NAME] - self._interval = data[CONF_INTERVAL] - 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) @@ -827,12 +817,14 @@ def __init__( # 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, + self.lights, config_entry.data, config_entry.options, data, @@ -865,7 +857,7 @@ def _set_changeable_settings( attrdata[k] = v.total_seconds() self._config.update(attrdata) - self._initial_transition = data[CONF_INITIAL_TRANSITION] + self.initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] @@ -884,6 +876,7 @@ def _set_changeable_settings( ) 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): @@ -917,7 +910,7 @@ def _set_changeable_settings( _LOGGER.debug( "%s: Set switch settings for lights '%s'. now using data: '%s'", self._name, - self._lights, + self.lights, data, ) @@ -957,12 +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.turn_on_off_listener.set_auto_reset_manual_control_times( all_lights, self._auto_reset_manual_control_time ) - self._lights = list(all_lights) + self.lights = list(all_lights) async def _setup_listeners(self, _=None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) @@ -972,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() @@ -1010,14 +1031,14 @@ def extra_state_attributes(self) -> dict[str, Any]: 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) ] 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 + for light in self.lights if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0 } return extra_state_attributes @@ -1050,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"), ) @@ -1065,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( @@ -1074,20 +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 + ) -> AdaptationData | None: if transition is None: transition = self._transition if adapt_brightness is None: @@ -1097,6 +1113,15 @@ async def _adapt_light( if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color + 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 @@ -1107,7 +1132,8 @@ async def _adapt_light( features = _supported_features(self.hass, light) # Check transition == 0 to fix #378 - if "transition" in features and transition > 0: + 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) @@ -1136,48 +1162,118 @@ async def _adapt_light( context = context or self.create_context("adapt_lights") - # See #80. Doesn't check if transitions differ but it does the job. - last_service_data = self.turn_on_off_listener.last_service_data - if not force and last_service_data.get(light) == service_data: + 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: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", - self._name, - light, - context.id, + "%s: Skipping reactive adaptation of %s", self._name, context.parent_id ) return - else: - 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, @@ -1188,9 +1284,13 @@ 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.update( @@ -1201,7 +1301,7 @@ async def _update_attrs_and_maybe_adapt_lights( self.async_write_ha_state() if lights is None: - lights = self._lights + lights = self.lights filtered_lights = [] if not force: @@ -1283,7 +1383,7 @@ async def _update_manual_control_and_maybe_adapt( else: _fire_manual_control_event(self, light, context) else: - await self._adapt_light(light, transition, force=force, context=context) + 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)): @@ -1293,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, @@ -1316,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) @@ -1351,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), ) @@ -1632,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 @@ -1649,6 +1758,9 @@ def __init__(self, hass: HomeAssistant): 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] = {} @@ -1657,11 +1769,183 @@ def __init__(self, hass: HomeAssistant): # Track light transitions self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} - self.remove_listener = self.hass.bus.async_listen( - EVENT_CALL_SERVICE, self.turn_on_off_event_listener + 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.remove_listener2 = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self.state_changed_event_listener + + 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() + + if not first_service_data: + return + + # 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) ) def _handle_timer( @@ -1740,7 +2024,7 @@ async def reset(): continue await switch._update_attrs_and_maybe_adapt_lights( [light], - transition=switch._initial_transition, + transition=switch.initial_transition, force=True, context=switch.create_context("autoreset"), ) @@ -1754,6 +2038,32 @@ async def reset(): 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: @@ -1764,20 +2074,15 @@ def reset(self, *lights, reset_manual_control=True) -> None: timer.cancel() self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) + 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: @@ -1790,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 @@ -1875,10 +2191,6 @@ async def state_changed_event_listener(self, event: Event) -> None: entity_id, ) self.last_state_change[entity_id] = [new_state] - _LOGGER.debug( - "Last transition: %s", - self.last_service_data[entity_id].get(ATTR_TRANSITION), - ) self.start_transition_timer(entity_id) elif old_state is not None: self.last_state_change[entity_id].append(new_state) 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 38fb7b0e..e6d1f9a9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -48,7 +48,8 @@ "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. ⏲️" + "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." } } }, From 505bc502c09e6e30223a4b8161a186e0167b8c73 Mon Sep 17 00:00:00 2001 From: Kyle Gordon Date: Sun, 23 Jul 2023 10:49:02 +0100 Subject: [PATCH 100/158] Include Frigate hass card --- www/frigate-card/_commonjsHelpers-1789f0cf.js | 1 + www/frigate-card/audio-557099cb.js | 1 + www/frigate-card/card-555679fd.js | 565 ++++++++ www/frigate-card/editor-7b16019d.js | 381 +++++ www/frigate-card/endpoint-aa68fc9e.js | 1 + www/frigate-card/engine-e412e9a0.js | 1 + www/frigate-card/engine-frigate-2c5e3aa9.js | 1 + www/frigate-card/engine-generic-395b8c68.js | 1 + www/frigate-card/engine-motioneye-ae70fe08.js | 1 + www/frigate-card/frigate-hass-card.js | 1 + www/frigate-card/gallery-6281c347.js | 165 +++ www/frigate-card/ha-hls-player-aef987da.js | 33 + www/frigate-card/image-0b99ab11.js | 11 + www/frigate-card/index-52dee8bb.js | 1 + www/frigate-card/index-af8cf05c.js | 1 + www/frigate-card/lang-it-0e2e946c.js | 1 + www/frigate-card/lang-pt-BR-1648942c.js | 1 + www/frigate-card/lang-pt-PT-440b6dfd.js | 1 + www/frigate-card/lazyload-c2d6254a.js | 44 + www/frigate-card/live-e0c9196c.js | 127 ++ www/frigate-card/live-go2rtc-0795a62f.js | 1 + www/frigate-card/live-ha-df63bfc8.js | 61 + www/frigate-card/live-image-c8850fc4.js | 7 + www/frigate-card/live-jsmpeg-9c767737.js | 12 + www/frigate-card/live-webrtc-card-dfc8f852.js | 1 + www/frigate-card/media-b0eb3f2a.js | 1 + www/frigate-card/media-layout-8e0c974f.js | 1 + www/frigate-card/timeline-6aa9e747.js | 182 +++ www/frigate-card/uniqWith-12b3ff8a.js | 1 + www/frigate-card/viewer-b95bc789.js | 104 ++ www/frigate-card/zoomer-1857311a.js | 11 + www/frigate-hass-card.js | 1244 ----------------- 32 files changed, 1721 insertions(+), 1244 deletions(-) create mode 100644 www/frigate-card/_commonjsHelpers-1789f0cf.js create mode 100644 www/frigate-card/audio-557099cb.js create mode 100644 www/frigate-card/card-555679fd.js create mode 100644 www/frigate-card/editor-7b16019d.js create mode 100644 www/frigate-card/endpoint-aa68fc9e.js create mode 100644 www/frigate-card/engine-e412e9a0.js create mode 100644 www/frigate-card/engine-frigate-2c5e3aa9.js create mode 100644 www/frigate-card/engine-generic-395b8c68.js create mode 100644 www/frigate-card/engine-motioneye-ae70fe08.js create mode 100644 www/frigate-card/frigate-hass-card.js create mode 100644 www/frigate-card/gallery-6281c347.js create mode 100644 www/frigate-card/ha-hls-player-aef987da.js create mode 100644 www/frigate-card/image-0b99ab11.js create mode 100644 www/frigate-card/index-52dee8bb.js create mode 100644 www/frigate-card/index-af8cf05c.js create mode 100644 www/frigate-card/lang-it-0e2e946c.js create mode 100644 www/frigate-card/lang-pt-BR-1648942c.js create mode 100644 www/frigate-card/lang-pt-PT-440b6dfd.js create mode 100644 www/frigate-card/lazyload-c2d6254a.js create mode 100644 www/frigate-card/live-e0c9196c.js create mode 100644 www/frigate-card/live-go2rtc-0795a62f.js create mode 100644 www/frigate-card/live-ha-df63bfc8.js create mode 100644 www/frigate-card/live-image-c8850fc4.js create mode 100644 www/frigate-card/live-jsmpeg-9c767737.js create mode 100644 www/frigate-card/live-webrtc-card-dfc8f852.js create mode 100644 www/frigate-card/media-b0eb3f2a.js create mode 100644 www/frigate-card/media-layout-8e0c974f.js create mode 100644 www/frigate-card/timeline-6aa9e747.js create mode 100644 www/frigate-card/uniqWith-12b3ff8a.js create mode 100644 www/frigate-card/viewer-b95bc789.js create mode 100644 www/frigate-card/zoomer-1857311a.js delete mode 100644 www/frigate-hass-card.js 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"